mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-09-11 14:45:31 +03:00
Infrastructure for simple internal hyperlink handling
This commit is contained in:
parent
9e688720a6
commit
51472e1e88
@ -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)}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
120
tools/tui/render_lines.go
Normal 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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user