render_lines.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package tui
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strings"
  6. "sync"
  7. "kitty/tools/tui/loop"
  8. "kitty/tools/utils"
  9. "kitty/tools/utils/style"
  10. "kitty/tools/wcswidth"
  11. )
  12. var _ = fmt.Print
  13. var _ = utils.Repr
  14. const KittyInternalHyperlinkProtocol = "kitty-ih"
  15. func InternalHyperlink(text, id string) string {
  16. return fmt.Sprintf("\x1b]8;;%s:%s\x1b\\%s\x1b]8;;\x1b\\", KittyInternalHyperlinkProtocol, id, text)
  17. }
  18. type RenderLines struct {
  19. }
  20. var hyperlink_pat = sync.OnceValue(func() *regexp.Regexp {
  21. return regexp.MustCompile("\x1b]8;([^;]*);(.*?)(?:\x1b\\\\|\a)")
  22. })
  23. // Render lines in the specified rectangle. If width > 0 then lines are wrapped
  24. // to fit in the width. A string containing rendered lines with escape codes to
  25. // move cursor is returned. Any internal hyperlinks are added to the
  26. // MouseState.
  27. func (r RenderLines) InRectangle(
  28. lines []string, start_x, start_y, width, height int, mouse_state *MouseState, on_click ...func(id string) error,
  29. ) (all_rendered bool, y_after_last_line int, ans string) {
  30. end_y := start_y + height - 1
  31. if end_y < start_y {
  32. return len(lines) == 0, start_y + 1, ""
  33. }
  34. x, y := start_x, start_y
  35. buf := strings.Builder{}
  36. buf.Grow(len(lines) * max(1, width) * 3)
  37. move_cursor := func(x, y int) { buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+1, x+1)) }
  38. var hyperlink_state struct {
  39. action string
  40. start_x, start_y int
  41. }
  42. start_hyperlink := func(action string) {
  43. hyperlink_state.action = action
  44. hyperlink_state.start_x, hyperlink_state.start_y = x, y
  45. }
  46. add_chunk := func(text string) {
  47. if text != "" {
  48. buf.WriteString(text)
  49. x += wcswidth.Stringwidth(text)
  50. }
  51. }
  52. commit_hyperlink := func() bool {
  53. if hyperlink_state.action == "" {
  54. return false
  55. }
  56. if y == hyperlink_state.start_y && x <= hyperlink_state.start_x {
  57. return false
  58. }
  59. mouse_state.AddCellRegion(hyperlink_state.action, hyperlink_state.start_x, hyperlink_state.start_y, max(0, x-1), y, on_click...)
  60. hyperlink_state.action = ``
  61. return true
  62. }
  63. add_hyperlink := func(id, url string) {
  64. is_closer := id == "" && url == ""
  65. if is_closer {
  66. if !commit_hyperlink() {
  67. buf.WriteString("\x1b]8;;\x1b\\")
  68. }
  69. } else {
  70. commit_hyperlink()
  71. if strings.HasPrefix(url, KittyInternalHyperlinkProtocol+":") {
  72. start_hyperlink(url[len(KittyInternalHyperlinkProtocol)+1:])
  73. } else {
  74. buf.WriteString(fmt.Sprintf("\x1b]8;%s;%s\x1b\\", id, url))
  75. }
  76. }
  77. }
  78. add_line := func(line string) {
  79. x = start_x
  80. indices := hyperlink_pat().FindAllStringSubmatchIndex(line, -1)
  81. start := 0
  82. for _, index := range indices {
  83. full_hyperlink_start, full_hyperlink_end := index[0], index[1]
  84. add_chunk(line[start:full_hyperlink_start])
  85. start = full_hyperlink_end
  86. add_hyperlink(line[index[2]:index[3]], line[index[4]:index[5]])
  87. }
  88. add_chunk(line[start:])
  89. }
  90. all_rendered = true
  91. wo := style.WrapOptions{Trim_whitespace: true}
  92. for _, line := range lines {
  93. wrapped_lines := []string{line}
  94. if width > 0 {
  95. wrapped_lines = style.WrapTextAsLines(line, width, wo)
  96. }
  97. for _, line := range wrapped_lines {
  98. move_cursor(start_x, y)
  99. add_line(line)
  100. y += 1
  101. if y > end_y {
  102. all_rendered = false
  103. goto end
  104. }
  105. }
  106. }
  107. end:
  108. commit_hyperlink()
  109. return all_rendered, y, buf.String()
  110. }