main.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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 main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
  105. output := tui.KittenOutputSerializer()
  106. if tty.IsTerminal(os.Stdin.Fd()) {
  107. return 1, fmt.Errorf("You must pass the text to be hinted on STDIN")
  108. }
  109. stdin, err := io.ReadAll(os.Stdin)
  110. if err != nil {
  111. return 1, fmt.Errorf("Failed to read from STDIN with error: %w", err)
  112. }
  113. if len(args) > 0 && o.CustomizeProcessing == "" && o.Type != "linenum" {
  114. return 1, fmt.Errorf("Extra command line arguments present: %s", strings.Join(args, " "))
  115. }
  116. input_text := parse_input(utils.UnsafeBytesToString(stdin))
  117. text, all_marks, index_map, err := find_marks(input_text, o, os.Args[2:]...)
  118. if err != nil {
  119. return 1, err
  120. }
  121. result := Result{
  122. Programs: o.Program, Multiple_joiner: o.MultipleJoiner, Customize_processing: o.CustomizeProcessing, Type: o.Type,
  123. Extra_cli_args: args, Linenum_action: o.LinenumAction,
  124. }
  125. result.Cwd, _ = os.Getwd()
  126. alphabet := o.Alphabet
  127. if alphabet == "" {
  128. alphabet = DEFAULT_HINT_ALPHABET
  129. }
  130. ignore_mark_indices := utils.NewSet[int](8)
  131. window_title := o.WindowTitle
  132. if window_title == "" {
  133. switch o.Type {
  134. case "url":
  135. window_title = "Choose URL"
  136. default:
  137. window_title = "Choose text"
  138. }
  139. }
  140. current_text := ""
  141. current_input := ""
  142. match_suffix := ""
  143. switch o.AddTrailingSpace {
  144. case "always":
  145. match_suffix = " "
  146. case "never":
  147. default:
  148. if o.Multiple {
  149. match_suffix = " "
  150. }
  151. }
  152. chosen := []*Mark{}
  153. lp, err := loop.New(loop.NoAlternateScreen) // no alternate screen reduces flicker on exit
  154. if err != nil {
  155. return
  156. }
  157. fctx := style.Context{AllowEscapeCodes: true}
  158. faint := fctx.SprintFunc("dim")
  159. hint_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsForegroundColor, o.HintsBackgroundColor))
  160. text_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bold", o.HintsTextColor))
  161. highlight_mark := func(m *Mark, mark_text string) string {
  162. hint := encode_hint(m.Index, alphabet)
  163. if current_input != "" && !strings.HasPrefix(hint, current_input) {
  164. return faint(mark_text)
  165. }
  166. hint = hint[len(current_input):]
  167. if hint == "" {
  168. hint = " "
  169. }
  170. if len(mark_text) <= len(hint) {
  171. mark_text = ""
  172. } else {
  173. mark_text = mark_text[len(hint):]
  174. }
  175. ans := hint_style(hint) + text_style(mark_text)
  176. return fmt.Sprintf("\x1b]8;;mark:%d\a%s\x1b]8;;\a", m.Index, ans)
  177. }
  178. render := func() string {
  179. ans := text
  180. for i := len(all_marks) - 1; i >= 0; i-- {
  181. mark := &all_marks[i]
  182. if ignore_mark_indices.Has(mark.Index) {
  183. continue
  184. }
  185. mtext := highlight_mark(mark, ans[mark.Start:mark.End])
  186. ans = ans[:mark.Start] + mtext + ans[mark.End:]
  187. }
  188. ans = strings.ReplaceAll(ans, "\x00", "")
  189. return strings.TrimRightFunc(strings.NewReplacer("\r", "\r\n", "\n", "\r\n").Replace(ans), unicode.IsSpace)
  190. }
  191. draw_screen := func() {
  192. lp.StartAtomicUpdate()
  193. defer lp.EndAtomicUpdate()
  194. if current_text == "" {
  195. current_text = render()
  196. }
  197. lp.ClearScreen()
  198. lp.QueueWriteString(current_text)
  199. }
  200. reset := func() {
  201. current_input = ""
  202. current_text = ""
  203. }
  204. lp.OnInitialize = func() (string, error) {
  205. lp.SendOverlayReady()
  206. lp.SetCursorVisible(false)
  207. lp.SetWindowTitle(window_title)
  208. lp.AllowLineWrapping(false)
  209. draw_screen()
  210. return "", nil
  211. }
  212. lp.OnFinalize = func() string {
  213. lp.SetCursorVisible(true)
  214. return ""
  215. }
  216. lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
  217. draw_screen()
  218. return nil
  219. }
  220. lp.OnRCResponse = func(data []byte) error {
  221. var r struct {
  222. Type string
  223. Mark int
  224. }
  225. if err := json.Unmarshal(data, &r); err != nil {
  226. return err
  227. }
  228. if r.Type == "mark_activated" {
  229. if m, ok := index_map[r.Mark]; ok {
  230. chosen = append(chosen, m)
  231. if o.Multiple {
  232. ignore_mark_indices.Add(m.Index)
  233. reset()
  234. } else {
  235. lp.Quit(0)
  236. return nil
  237. }
  238. }
  239. }
  240. return nil
  241. }
  242. lp.OnText = func(text string, _, _ bool) error {
  243. changed := false
  244. for _, ch := range text {
  245. if strings.ContainsRune(alphabet, ch) {
  246. current_input += string(ch)
  247. changed = true
  248. }
  249. }
  250. if changed {
  251. matches := []*Mark{}
  252. for idx, m := range index_map {
  253. if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
  254. matches = append(matches, m)
  255. }
  256. }
  257. if len(matches) == 1 {
  258. chosen = append(chosen, matches[0])
  259. if o.Multiple {
  260. ignore_mark_indices.Add(matches[0].Index)
  261. reset()
  262. } else {
  263. lp.Quit(0)
  264. return nil
  265. }
  266. }
  267. current_text = ""
  268. draw_screen()
  269. }
  270. return nil
  271. }
  272. lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
  273. if ev.MatchesPressOrRepeat("backspace") {
  274. ev.Handled = true
  275. r := []rune(current_input)
  276. if len(r) > 0 {
  277. r = r[:len(r)-1]
  278. current_input = string(r)
  279. current_text = ""
  280. }
  281. draw_screen()
  282. } else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
  283. ev.Handled = true
  284. if current_input != "" {
  285. idx := decode_hint(current_input, alphabet)
  286. if m := index_map[idx]; m != nil {
  287. chosen = append(chosen, m)
  288. ignore_mark_indices.Add(idx)
  289. if o.Multiple {
  290. reset()
  291. draw_screen()
  292. } else {
  293. lp.Quit(0)
  294. }
  295. } else {
  296. current_input = ""
  297. current_text = ""
  298. draw_screen()
  299. }
  300. }
  301. } else if ev.MatchesPressOrRepeat("esc") {
  302. if o.Multiple {
  303. lp.Quit(0)
  304. } else {
  305. lp.Quit(1)
  306. }
  307. }
  308. return nil
  309. }
  310. err = lp.Run()
  311. if err != nil {
  312. return 1, err
  313. }
  314. ds := lp.DeathSignalName()
  315. if ds != "" {
  316. fmt.Println("Killed by signal: ", ds)
  317. lp.KillIfSignalled()
  318. return 1, nil
  319. }
  320. if lp.ExitCode() != 0 {
  321. return lp.ExitCode(), nil
  322. }
  323. result.Match = make([]string, len(chosen))
  324. result.Groupdicts = make([]map[string]any, len(chosen))
  325. for i, m := range chosen {
  326. result.Match[i] = m.Text + match_suffix
  327. result.Groupdicts[i] = m.Groupdict
  328. }
  329. fmt.Println(output(result))
  330. return
  331. }
  332. func EntryPoint(parent *cli.Command) {
  333. create_cmd(parent, main)
  334. }