diff --git a/gen/go_code.py b/gen/go_code.py index 4267f2a7b..6757c19e0 100755 --- a/gen/go_code.py +++ b/gen/go_code.py @@ -572,7 +572,7 @@ def generate_constants() -> str: from kittens.hints.main import DEFAULT_REGEX from kitty.config import option_names_for_completion from kitty.fast_data_types import FILE_TRANSFER_CODE - from kitty.options.utils import allowed_shell_integration_values + from kitty.options.utils import allowed_shell_integration_values, url_style_map del sys.modules['kittens.hints.main'] ref_map = load_ref_map() with open('kitty/data-types.h') as dt: @@ -582,6 +582,7 @@ def generate_constants() -> str: dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help)) url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes) option_names = '`' + '\n'.join(option_names_for_completion()) + '`' + url_style = {v:k for k, v in url_style_map.items()}[Options.url_style] return f'''\ package kitty @@ -600,6 +601,8 @@ var IsStandaloneBuild string = "" const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]} const HintsDefaultRegex = `{DEFAULT_REGEX}` const DefaultTermName = `{Options.term}` +const DefaultUrlStyle = `{url_style}` +const DefaultUrlColor = `{Options.url_color.as_sharp}` var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}} var DefaultPager []string = []string{{ {dp} }} var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)} diff --git a/tools/cmd/list_fonts/main.go b/tools/cmd/list_fonts/main.go index fd9206500..513c91623 100644 --- a/tools/cmd/list_fonts/main.go +++ b/tools/cmd/list_fonts/main.go @@ -57,6 +57,7 @@ func main() (rc int, err error) { if err != nil { return 1, err } + lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING) h := &handler{lp: lp} lp.OnInitialize = func() (string, error) { lp.AllowLineWrapping(false) diff --git a/tools/cmd/list_fonts/ui.go b/tools/cmd/list_fonts/ui.go index 001b57866..8609d01d7 100644 --- a/tools/cmd/list_fonts/ui.go +++ b/tools/cmd/list_fonts/ui.go @@ -254,13 +254,15 @@ func (h *handler) finalize() { func (h *handler) draw_screen() (err error) { h.lp.StartAtomicUpdate() + defer h.mouse_state.UpdateHoveredIds() + defer h.mouse_state.ApplyHoverStyles(h.lp) defer h.lp.EndAtomicUpdate() h.lp.ClearScreen() h.lp.AllowLineWrapping(false) + h.mouse_state.ClearCellRegions() switch h.state { case SCANNING_FAMILIES: h.lp.Println("Scanning system for fonts, please wait...") - return nil case LISTING_FAMILIES: return h.draw_listing_screen() } @@ -281,7 +283,9 @@ func (h *handler) on_wakeup() (err error) { } func (h *handler) on_mouse_event(event *loop.MouseEvent) (err error) { - return h.mouse_state.UpdateState(event) + err = h.mouse_state.UpdateState(event) + h.mouse_state.ApplyHoverStyles(h.lp) + return } func (h *handler) on_key_event(event *loop.KeyEvent) (err error) { diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 92df19b9a..ff2d0a65c 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -217,6 +217,24 @@ func (self *Loop) Println(args ...any) { self.QueueWriteString("\r") } +func (self *Loop) style_region(style string, start_x, start_y, end_x, end_y int) string { + sgr := self.SprintStyled(style, "|")[2:] + sgr = sgr[:strings.IndexByte(sgr, 'm')] + return fmt.Sprintf("\x1b[%d;%d;%d;%d%s$r", start_y+1, start_x+1, end_y+1, end_x+1, sgr) +} + +// Apply the specified style to the specified region of the screen (0-based +// indexing). The region is all cells from the start cell to the end cell. See +// StyleRectangle to apply style to a rectangular area. +func (self *Loop) StyleRegion(style string, start_x, start_y, end_x, end_y int) IdType { + return self.QueueWriteString(self.style_region(style, start_x, start_y, end_x, end_y)) +} + +// Apply the specified style to the specified rectangle of the screen (0-based indexing). +func (self *Loop) StyleRectangle(style string, start_x, start_y, end_x, end_y int) IdType { + return self.QueueWriteString("\x1b[2*x" + self.style_region(style, start_x, start_y, end_x, end_y) + "\x1b[*x") +} + func (self *Loop) SprintStyled(style string, args ...any) string { f := self.style_cache[style] if f == nil { diff --git a/tools/tui/mouse.go b/tools/tui/mouse.go index 2b414e7da..58ec22ca1 100644 --- a/tools/tui/mouse.go +++ b/tools/tui/mouse.go @@ -4,9 +4,13 @@ package tui import ( "fmt" + "path/filepath" "time" + "kitty" + "kitty/tools/config" "kitty/tools/tui/loop" + "kitty/tools/utils" ) var _ = fmt.Print @@ -204,7 +208,6 @@ func (ms *MouseSelection) DragScroll(ev *loop.MouseEvent, lp *loop.Loop, callbac type CellRegion struct { TopLeft, BottomRight struct{ X, Y int } - Hovered bool Id string OnClick []func(id string) error } @@ -220,18 +223,75 @@ type MouseState struct { Cell, Pixel struct{ X, Y int } Pressed struct{ Left, Right, Middle, Fourth, Fifth, Sixth, Seventh bool } - regions []*CellRegion + regions []*CellRegion + region_id_map map[string][]*CellRegion + hovered_ids *utils.Set[string] + default_url_style struct { + value string + loaded bool + } } func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion { cr := CellRegion{TopLeft: struct{ X, Y int }{start_x, start_y}, BottomRight: struct{ X, Y int }{end_x, end_y}, Id: id, OnClick: on_click} m.regions = append(m.regions, &cr) - cr.Hovered = cr.Contains(m.Cell.X, m.Cell.Y) + m.region_id_map[id] = append(m.region_id_map[id], &cr) return &cr } func (m *MouseState) ClearCellRegions() { m.regions = nil + m.region_id_map = nil + m.hovered_ids = nil +} + +func (m *MouseState) UpdateHoveredIds() { + if m.hovered_ids == nil { + m.hovered_ids = utils.NewSet[string]() + } else { + m.hovered_ids.Clear() + } + for _, r := range m.regions { + if r.Contains(m.Cell.X, m.Cell.Y) { + m.hovered_ids.Add(r.Id) + } + } +} + +func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) { + if m.hovered_ids == nil { + return + } + hs := "" + if len(style) == 0 { + if !m.default_url_style.loaded { + m.default_url_style.loaded = true + conf := filepath.Join(utils.ConfigDir(), "kitty.conf") + color, style := kitty.DefaultUrlColor, kitty.DefaultUrlStyle + cp := config.ConfigParser{LineHandler: func(key, val string) error { + switch key { + case "url_color": + color = val + case "url_style": + style = val + } + return nil + }, + } + _ = cp.ParseFiles(conf) // ignore errors and use defaults + if style != "none" && style != "" { + m.default_url_style.value = fmt.Sprintf("u=%s uc=%s", style, color) + } + } + hs = m.default_url_style.value + } else { + hs = style[0] + } + for id := range m.hovered_ids.Iterable() { + for _, r := range m.region_id_map[id] { + lp.StyleRegion(hs, r.TopLeft.X, r.TopLeft.Y, r.BottomRight.X, r.BottomRight.Y) + } + } } func (m *MouseState) UpdateState(ev *loop.MouseEvent) error { @@ -261,19 +321,6 @@ func (m *MouseState) UpdateState(ev *loop.MouseEvent) error { m.Pressed.Seventh = pressed } } - for _, r := range m.regions { - if r.Contains(m.Cell.X, m.Cell.Y) { - r.Hovered = true - if ev.Event_type == loop.MOUSE_CLICK { - for _, f := range r.OnClick { - if err := f(r.Id); err != nil { - return err - } - } - } - } else { - r.Hovered = false - } - } + m.UpdateHoveredIds() return nil } diff --git a/tools/tui/render_lines.go b/tools/tui/render_lines.go new file mode 100644 index 000000000..4026b9dc7 --- /dev/null +++ b/tools/tui/render_lines.go @@ -0,0 +1,120 @@ +package tui + +import ( + "fmt" + "regexp" + "strings" + "sync" + + "kitty/tools/tui/loop" + "kitty/tools/utils/style" + "kitty/tools/wcswidth" +) + +var _ = fmt.Print + +const KittyInternalHyperlinkProtocol = "kitty-ih" + +func InternalHyperlink(text, id string) string { + return fmt.Sprintf("\x1b]8;;%s:%s\x1b\\%s\x1b]8;;\x1b\\", KittyInternalHyperlinkProtocol, id, text) +} + +type RenderLines struct { + WrapOptions style.WrapOptions +} + +var hyperlink_pat = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile("\x1b]8;([^;]*);.*?(\x1b\\\\|\a)") +}) + +// Render lines in the specified rectangle. If width > 0 then lines are wrapped +// to fit in the width. A string containing rendered lines with escape codes to +// move cursor is returned. Any internal hyperlinks are added to the +// MouseState. +func (r RenderLines) InRectangle( + lines []string, start_x, start_y, width, height int, mouse_state *MouseState, +) (all_rendered bool, final_y int, ans string) { + end_y := start_y + height - 1 + if end_y < start_y { + return len(lines) == 0, start_y, "" + } + x, y := start_x, start_y + buf := strings.Builder{} + buf.Grow(len(lines) * max(1, width) * 3) + move_cursor := func(x, y int) { buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+1, x+1)) } + var hyperlink_state struct { + action string + start_x, start_y int + } + + start_hyperlink := func(action string) { + hyperlink_state.action = action + hyperlink_state.start_x, hyperlink_state.start_y = x, y + } + + add_chunk := func(text string) { + if text != "" { + buf.WriteString(text) + x += wcswidth.Stringwidth(text) + } + } + + commit_hyperlink := func() { + mouse_state.AddCellRegion(hyperlink_state.action, hyperlink_state.start_x, hyperlink_state.start_y, x, y) + hyperlink_state.action = `` + } + + add_hyperlink := func(id, url string) { + is_closer := id == "" && url == "" + if is_closer { + if hyperlink_state.action != "" { + commit_hyperlink() + } else { + buf.WriteString("\x1b]8;;\x1b\\") + } + } else { + if hyperlink_state.action != "" { + commit_hyperlink() + } + if strings.HasPrefix(url, KittyInternalHyperlinkProtocol+":") { + start_hyperlink(url[len(KittyInternalHyperlinkProtocol)+1:]) + } else { + buf.WriteString(fmt.Sprintf("\x1b]8;%s;%s\x1b\\", id, url)) + } + } + + } + + add_line := func(line string) { + x = start_x + indices := hyperlink_pat().FindAllStringSubmatchIndex(line, -1) + start := 0 + for _, index := range indices { + full_hyperlink_start, full_hyperlink_end := index[0], index[1] + add_chunk(line[start:full_hyperlink_start]) + start = full_hyperlink_end + add_hyperlink(line[index[2]:index[3]], line[index[4]:index[5]]) + } + add_chunk(line[start:]) + } + + all_rendered = true + for _, line := range lines { + lines := []string{line} + if width > 0 { + lines = style.WrapTextAsLines(line, width, r.WrapOptions) + } + for _, line := range lines { + if y > end_y { + all_rendered = false + goto end + } + move_cursor(start_x, y) + add_line(line) + y += 1 + } + } +end: + commit_hyperlink() + return all_rendered, y, buf.String() +} diff --git a/tools/utils/set.go b/tools/utils/set.go index 13daf694f..892329abc 100644 --- a/tools/utils/set.go +++ b/tools/utils/set.go @@ -41,6 +41,10 @@ func (self *Set[T]) Has(val T) bool { return ok } +func (self *Set[T]) Clear() { + clear(self.items) +} + func (self *Set[T]) Len() int { return len(self.items) }