Infrastructure for simple internal hyperlink handling

This commit is contained in:
Kovid Goyal 2024-05-04 13:44:09 +05:30
parent 9e688720a6
commit 51472e1e88
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 217 additions and 20 deletions

View File

@ -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 @@ def generate_constants() -> str:
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)}

View File

@ -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)

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}

120
tools/tui/render_lines.go Normal file
View File

@ -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()
}

View File

@ -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)
}