main.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package hints
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "os"
  8. "strconv"
  9. "strings"
  10. "unicode"
  11. "kitty/tools/cli"
  12. "kitty/tools/tty"
  13. "kitty/tools/tui"
  14. "kitty/tools/tui/loop"
  15. "kitty/tools/utils"
  16. "kitty/tools/utils/style"
  17. "kitty/tools/wcswidth"
  18. )
  19. var _ = fmt.Print
  20. func convert_text(text string, cols int) string {
  21. lines := make([]string, 0, 64)
  22. empty_line := strings.Repeat("\x00", cols) + "\n"
  23. s1 := utils.NewLineScanner(text)
  24. for s1.Scan() {
  25. full_line := s1.Text()
  26. if full_line == "" {
  27. lines = append(lines, empty_line)
  28. continue
  29. }
  30. if strings.TrimRight(full_line, "\r") == "" {
  31. for i := 0; i < len(full_line); i++ {
  32. lines = append(lines, empty_line)
  33. }
  34. continue
  35. }
  36. appended := false
  37. s2 := utils.NewSeparatorScanner(full_line, "\r")
  38. for s2.Scan() {
  39. line := s2.Text()
  40. if line != "" {
  41. line_sz := wcswidth.Stringwidth(line)
  42. extra := cols - line_sz
  43. if extra > 0 {
  44. line += strings.Repeat("\x00", extra)
  45. }
  46. lines = append(lines, line)
  47. lines = append(lines, "\r")
  48. appended = true
  49. }
  50. }
  51. if appended {
  52. lines[len(lines)-1] = "\n"
  53. }
  54. }
  55. ans := strings.Join(lines, "")
  56. return strings.TrimRight(ans, "\r\n")
  57. }
  58. func parse_input(text string) string {
  59. cols, err := strconv.Atoi(os.Getenv("OVERLAID_WINDOW_COLS"))
  60. if err == nil {
  61. return convert_text(text, cols)
  62. }
  63. term, err := tty.OpenControllingTerm()
  64. if err == nil {
  65. sz, err := term.GetSize()
  66. term.Close()
  67. if err == nil {
  68. return convert_text(text, int(sz.Col))
  69. }
  70. }
  71. return convert_text(text, 80)
  72. }
  73. type Result struct {
  74. Match []string `json:"match"`
  75. Programs []string `json:"programs"`
  76. Multiple_joiner string `json:"multiple_joiner"`
  77. Customize_processing string `json:"customize_processing"`
  78. Type string `json:"type"`
  79. Groupdicts []map[string]any `json:"groupdicts"`
  80. Extra_cli_args []string `json:"extra_cli_args"`
  81. Linenum_action string `json:"linenum_action"`
  82. Cwd string `json:"cwd"`
  83. }
  84. func encode_hint(num int, alphabet string) (res string) {
  85. runes := []rune(alphabet)
  86. d := len(runes)
  87. for res == "" || num > 0 {
  88. res = string(runes[num%d]) + res
  89. num /= d
  90. }
  91. return
  92. }
  93. func decode_hint(x string, alphabet string) (ans int) {
  94. base := len(alphabet)
  95. index_map := make(map[rune]int, len(alphabet))
  96. for i, c := range alphabet {
  97. index_map[c] = i
  98. }
  99. for _, char := range x {
  100. ans = ans*base + index_map[char]
  101. }
  102. return
  103. }
  104. func as_rgb(c uint32) [3]float32 {
  105. return [3]float32{float32((c>>16)&255) / 255.0, float32((c>>8)&255) / 255.0, float32(c&255) / 255.0}
  106. }
  107. func hints_text_color(confval string) (ans string) {
  108. ans = confval
  109. if ans == "auto" {
  110. ans = "bright-gray"
  111. if bc, err := tui.ReadBasicColors(); err == nil {
  112. bg := as_rgb(bc.Background)
  113. c15 := as_rgb(bc.Color15)
  114. c8 := as_rgb(bc.Color8)
  115. if utils.RGBContrast(bg[0], bg[1], bg[2], c8[0], c8[1], c8[2]) > utils.RGBContrast(bg[0], bg[1], bg[2], c15[0], c15[1], c15[2]) {
  116. ans = "bright-black"
  117. }
  118. }
  119. }
  120. return
  121. }
  122. func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
  123. o.HintsTextColor = hints_text_color(o.HintsTextColor)
  124. output := tui.KittenOutputSerializer()
  125. if tty.IsTerminal(os.Stdin.Fd()) {
  126. return 1, fmt.Errorf("You must pass the text to be hinted on STDIN")
  127. }
  128. stdin, err := io.ReadAll(os.Stdin)
  129. if err != nil {
  130. return 1, fmt.Errorf("Failed to read from STDIN with error: %w", err)
  131. }
  132. if len(args) > 0 && o.CustomizeProcessing == "" && o.Type != "linenum" {
  133. return 1, fmt.Errorf("Extra command line arguments present: %s", strings.Join(args, " "))
  134. }
  135. input_text := parse_input(utils.UnsafeBytesToString(stdin))
  136. text, all_marks, index_map, err := find_marks(input_text, o, os.Args[2:]...)
  137. if err != nil {
  138. return 1, err
  139. }
  140. result := Result{
  141. Programs: o.Program, Multiple_joiner: o.MultipleJoiner, Customize_processing: o.CustomizeProcessing, Type: o.Type,
  142. Extra_cli_args: args, Linenum_action: o.LinenumAction,
  143. }
  144. result.Cwd, _ = os.Getwd()
  145. alphabet := o.Alphabet
  146. if alphabet == "" {
  147. alphabet = DEFAULT_HINT_ALPHABET
  148. }
  149. ignore_mark_indices := utils.NewSet[int](8)
  150. window_title := o.WindowTitle
  151. if window_title == "" {
  152. switch o.Type {
  153. case "url":
  154. window_title = "Choose URL"
  155. default:
  156. window_title = "Choose text"
  157. }
  158. }
  159. current_text := ""
  160. current_input := ""
  161. match_suffix := ""
  162. switch o.AddTrailingSpace {
  163. case "always":
  164. match_suffix = " "
  165. case "never":
  166. default:
  167. if o.Multiple {
  168. match_suffix = " "
  169. }
  170. }
  171. chosen := []*Mark{}
  172. lp, err := loop.New(loop.NoAlternateScreen) // no alternate screen reduces flicker on exit
  173. if err != nil {
  174. return
  175. }
  176. fctx := style.Context{AllowEscapeCodes: true}
  177. faint := fctx.SprintFunc("dim")
  178. hint_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsForegroundColor, o.HintsBackgroundColor))
  179. text_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bold", o.HintsTextColor))
  180. highlight_mark := func(m *Mark, mark_text string) string {
  181. hint := encode_hint(m.Index, alphabet)
  182. if current_input != "" && !strings.HasPrefix(hint, current_input) {
  183. return faint(mark_text)
  184. }
  185. hint = hint[len(current_input):]
  186. if hint == "" {
  187. hint = " "
  188. }
  189. if len(mark_text) <= len(hint) {
  190. mark_text = ""
  191. } else {
  192. mark_text = mark_text[len(hint):]
  193. }
  194. ans := hint_style(hint) + text_style(mark_text)
  195. return fmt.Sprintf("\x1b]8;;mark:%d\a%s\x1b]8;;\a", m.Index, ans)
  196. }
  197. render := func() string {
  198. ans := text
  199. for i := len(all_marks) - 1; i >= 0; i-- {
  200. mark := &all_marks[i]
  201. if ignore_mark_indices.Has(mark.Index) {
  202. continue
  203. }
  204. mtext := highlight_mark(mark, ans[mark.Start:mark.End])
  205. ans = ans[:mark.Start] + mtext + ans[mark.End:]
  206. }
  207. ans = strings.ReplaceAll(ans, "\x00", "")
  208. return strings.TrimRightFunc(strings.NewReplacer("\r", "\r\n", "\n", "\r\n").Replace(ans), unicode.IsSpace)
  209. }
  210. draw_screen := func() {
  211. lp.StartAtomicUpdate()
  212. defer lp.EndAtomicUpdate()
  213. if current_text == "" {
  214. current_text = render()
  215. }
  216. lp.ClearScreen()
  217. lp.QueueWriteString(current_text)
  218. }
  219. reset := func() {
  220. current_input = ""
  221. current_text = ""
  222. }
  223. lp.OnInitialize = func() (string, error) {
  224. lp.SetCursorVisible(false)
  225. lp.SetWindowTitle(window_title)
  226. lp.AllowLineWrapping(false)
  227. draw_screen()
  228. lp.SendOverlayReady()
  229. return "", nil
  230. }
  231. lp.OnFinalize = func() string {
  232. lp.SetCursorVisible(true)
  233. return ""
  234. }
  235. lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
  236. draw_screen()
  237. return nil
  238. }
  239. lp.OnRCResponse = func(data []byte) error {
  240. var r struct {
  241. Type string
  242. Mark int
  243. }
  244. if err := json.Unmarshal(data, &r); err != nil {
  245. return err
  246. }
  247. if r.Type == "mark_activated" {
  248. if m, ok := index_map[r.Mark]; ok {
  249. chosen = append(chosen, m)
  250. if o.Multiple {
  251. ignore_mark_indices.Add(m.Index)
  252. reset()
  253. } else {
  254. lp.Quit(0)
  255. return nil
  256. }
  257. }
  258. }
  259. return nil
  260. }
  261. lp.OnText = func(text string, _, _ bool) error {
  262. changed := false
  263. for _, ch := range text {
  264. if strings.ContainsRune(alphabet, ch) {
  265. current_input += string(ch)
  266. changed = true
  267. }
  268. }
  269. if changed {
  270. matches := []*Mark{}
  271. for idx, m := range index_map {
  272. if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
  273. matches = append(matches, m)
  274. }
  275. }
  276. if len(matches) == 1 {
  277. chosen = append(chosen, matches[0])
  278. if o.Multiple {
  279. ignore_mark_indices.Add(matches[0].Index)
  280. reset()
  281. } else {
  282. lp.Quit(0)
  283. return nil
  284. }
  285. }
  286. current_text = ""
  287. draw_screen()
  288. }
  289. return nil
  290. }
  291. lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
  292. if ev.MatchesPressOrRepeat("backspace") {
  293. ev.Handled = true
  294. r := []rune(current_input)
  295. if len(r) > 0 {
  296. r = r[:len(r)-1]
  297. current_input = string(r)
  298. current_text = ""
  299. }
  300. draw_screen()
  301. } else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
  302. ev.Handled = true
  303. if current_input != "" {
  304. idx := decode_hint(current_input, alphabet)
  305. if m := index_map[idx]; m != nil {
  306. chosen = append(chosen, m)
  307. ignore_mark_indices.Add(idx)
  308. if o.Multiple {
  309. reset()
  310. draw_screen()
  311. } else {
  312. lp.Quit(0)
  313. }
  314. } else {
  315. current_input = ""
  316. current_text = ""
  317. draw_screen()
  318. }
  319. }
  320. } else if ev.MatchesPressOrRepeat("esc") {
  321. if o.Multiple {
  322. lp.Quit(0)
  323. } else {
  324. lp.Quit(1)
  325. }
  326. }
  327. return nil
  328. }
  329. err = lp.Run()
  330. if err != nil {
  331. return 1, err
  332. }
  333. ds := lp.DeathSignalName()
  334. if ds != "" {
  335. fmt.Println("Killed by signal: ", ds)
  336. lp.KillIfSignalled()
  337. return 1, nil
  338. }
  339. if lp.ExitCode() != 0 {
  340. return lp.ExitCode(), nil
  341. }
  342. result.Match = make([]string, len(chosen))
  343. result.Groupdicts = make([]map[string]any, len(chosen))
  344. for i, m := range chosen {
  345. result.Match[i] = m.Text + match_suffix
  346. result.Groupdicts[i] = m.Groupdict
  347. }
  348. fmt.Println(output(result))
  349. return
  350. }
  351. func EntryPoint(parent *cli.Command) {
  352. create_cmd(parent, main)
  353. }