From d07eeaeda40e045ba7e482ce7d8bc67bf0b72d69 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Aug 2024 16:20:24 +0200 Subject: [PATCH 1/6] Cleanup: add empty default cases to silence linter warnings It annoys me that VS Code shows me yellow warnings for these whenever I edit gocui files as part of the lazygit project (I'm actually not sure why it does this, I thought it's not supposed to look into the vendor directory). Anyway, trying to silence these by adding empty default cases. I'm not sure this is the best or most idiomatic way to silence the warnings. --- escape.go | 1 + gui.go | 2 ++ tcell_driver.go | 3 +++ 3 files changed, 6 insertions(+) diff --git a/escape.go b/escape.go index b52c214..94056f9 100644 --- a/escape.go +++ b/escape.go @@ -78,6 +78,7 @@ func (ei *escapeInterpreter) runes() []rune { ret = append(ret, ';') } return append(ret, ei.curch) + default: } return nil } diff --git a/gui.go b/gui.go index c1ee93c..16b6076 100644 --- a/gui.go +++ b/gui.go @@ -1381,6 +1381,8 @@ func (g *Gui) onKey(ev *GocuiEvent) error { if _, err := g.execKeybindings(v, ev); err != nil { return err } + + default: } return nil diff --git a/tcell_driver.go b/tcell_driver.go index 96f2439..6665432 100644 --- a/tcell_driver.go +++ b/tcell_driver.go @@ -363,6 +363,7 @@ func (g *Gui) pollEvent() GocuiEvent { mouseKey = MouseRight case tcell.ButtonMiddle: mouseKey = MouseMiddle + default: } } @@ -374,11 +375,13 @@ func (g *Gui) pollEvent() GocuiEvent { dragState = NOT_DRAGGING case tcell.ButtonSecondary: case tcell.ButtonMiddle: + default: } mouseMod = Modifier(lastMouseMod) lastMouseMod = tcell.ModNone lastMouseKey = tcell.ButtonNone } + default: } if !wheeling { From fbf9cd191f7340bb32912dfd9fc934ff8906a8a5 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Aug 2024 16:24:22 +0200 Subject: [PATCH 2/6] Cleanup: remove unused `matched` return value of execKeybindings This fixes another linter warning. --- gui.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/gui.go b/gui.go index 16b6076..d702b07 100644 --- a/gui.go +++ b/gui.go @@ -1302,7 +1302,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { switch ev.Type { case eventKey: - _, err := g.execKeybindings(g.currentView, ev) + err := g.execKeybindings(g.currentView, ev) if err != nil { return err } @@ -1378,7 +1378,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } } - if _, err := g.execKeybindings(v, ev); err != nil { + if err := g.execKeybindings(v, ev); err != nil { return err } @@ -1442,25 +1442,25 @@ func IsMouseScrollKey(key interface{}) bool { } // execKeybindings executes the keybinding handlers that match the passed view -// and event. The value of matched is true if there is a match and no errors. -func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) { +// and event. +func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { var globalKb *keybinding var matchingParentViewKb *keybinding // if we're searching, and we've hit n/N/Esc, we ignore the default keybinding if v != nil && v.IsSearching() && ev.Mod == ModNone { if eventMatchesKey(ev, g.NextSearchMatchKey) { - return true, v.gotoNextMatch() + return v.gotoNextMatch() } else if eventMatchesKey(ev, g.PrevSearchMatchKey) { - return true, v.gotoPreviousMatch() + return v.gotoPreviousMatch() } else if eventMatchesKey(ev, g.SearchEscapeKey) { v.searcher.clearSearch() if g.OnSearchEscape != nil { if err := g.OnSearchEscape(); err != nil { - return true, err + return err } } - return true, nil + return nil } } @@ -1488,26 +1488,26 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil { matched := g.currentView.Editor.Edit(g.currentView, ev.Key, ev.Ch, ev.Mod) if matched { - return true, nil + return nil } } if globalKb != nil { return g.execKeybinding(v, globalKb) } - return false, nil + return nil } // execKeybinding executes a given keybinding -func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) { +func (g *Gui) execKeybinding(v *View, kb *keybinding) error { if g.isBlacklisted(kb.key) { - return true, nil + return nil } if err := kb.handler(g, v); err != nil { - return false, err + return err } - return true, nil + return nil } func (g *Gui) onFocus(ev *GocuiEvent) error { From 884a226ec889992d84388181bd37e430f644f233 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Aug 2024 16:26:57 +0200 Subject: [PATCH 3/6] Cleanup: change naked returns to explicit returns I dislike naked returns anyway, but this fixes the last linter warnings in gocui. --- escape.go | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/escape.go b/escape.go index 94056f9..fb2d41d 100644 --- a/escape.go +++ b/escape.go @@ -268,58 +268,48 @@ func (ei *escapeInterpreter) outputCSI() error { func (ei *escapeInterpreter) csiColor(param []string) (color Attribute, skip int, err error) { if len(param) < 2 { - err = errCSIParseError - return + return 0, 0, errCSIParseError } switch param[1] { case "2": // 24-bit color if ei.mode < OutputTrue { - err = errCSIParseError - return + return 0, 0, errCSIParseError } if len(param) < 5 { - err = errCSIParseError - return + return 0, 0, errCSIParseError } var red, green, blue int red, err = strconv.Atoi(param[2]) if err != nil { - err = errCSIParseError - return + return 0, 0, errCSIParseError } green, err = strconv.Atoi(param[3]) if err != nil { - err = errCSIParseError - return + return 0, 0, errCSIParseError } blue, err = strconv.Atoi(param[4]) if err != nil { - err = errCSIParseError - return + return 0, 0, errCSIParseError } return NewRGBColor(int32(red), int32(green), int32(blue)), 5, nil case "5": // 8-bit color if ei.mode < Output256 { - err = errCSIParseError - return + return 0, 0, errCSIParseError } if len(param) < 3 { - err = errCSIParseError - return + return 0, 0, errCSIParseError } var hex int hex, err = strconv.Atoi(param[2]) if err != nil { - err = errCSIParseError - return + return 0, 0, errCSIParseError } return Get256Color(int32(hex)), 3, nil default: - err = errCSIParseError - return + return 0, 0, errCSIParseError } } From 90c4fdb2896cbb88126c03f7ac3e65f707410378 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Aug 2024 17:40:35 +0200 Subject: [PATCH 4/6] Parse hyperlink OSC sequences and store link in cell struct --- escape.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- view.go | 8 +++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/escape.go b/escape.go index fb2d41d..f559bbb 100644 --- a/escape.go +++ b/escape.go @@ -17,6 +17,7 @@ type escapeInterpreter struct { curFgColor, curBgColor Attribute mode OutputMode instruction instruction + hyperlink string } type ( @@ -40,7 +41,11 @@ const ( stateCSI stateParams stateOSC - stateOSCEscape + stateOSCWaitForParams + stateOSCParams + stateOSCHyperlink + stateOSCEndEscape + stateOSCSkipUnknown bold fontEffect = 1 faint fontEffect = 2 @@ -60,6 +65,7 @@ var ( errNotCSI = errors.New("Not a CSI escape sequence") errCSIParseError = errors.New("CSI escape sequence parsing error") errCSITooLong = errors.New("CSI escape sequence is too long") + errOSCParseError = errors.New("OSC escape sequence parsing error") ) // runes in case of error will output the non-parsed runes as a string. @@ -192,15 +198,47 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) { return false, errCSIParseError } case stateOSC: + if ch == '8' { + ei.state = stateOSCWaitForParams + ei.hyperlink = "" + return true, nil + } + + ei.state = stateOSCSkipUnknown + return true, nil + case stateOSCWaitForParams: + if ch != ';' { + return true, errOSCParseError + } + + ei.state = stateOSCParams + return true, nil + case stateOSCParams: + if ch == ';' { + ei.state = stateOSCHyperlink + } + return true, nil + case stateOSCHyperlink: switch ch { + case 0x07: + ei.state = stateNone case 0x1b: - ei.state = stateOSCEscape - return true, nil + ei.state = stateOSCEndEscape + default: + ei.hyperlink += string(ch) } return true, nil - case stateOSCEscape: + case stateOSCEndEscape: ei.state = stateNone return true, nil + case stateOSCSkipUnknown: + switch ch { + case 0x07: + ei.state = stateNone + case 0x1b: + ei.state = stateOSCEndEscape + } + return true, nil } return false, nil } diff --git a/view.go b/view.go index e12991e..965ae7f 100644 --- a/view.go +++ b/view.go @@ -378,6 +378,7 @@ type viewLine struct { type cell struct { chr rune bgColor, fgColor Attribute + hyperlink string } type lineType []cell @@ -851,9 +852,10 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) { repeatCount = tabStop - (x % tabStop) } c := cell{ - fgColor: v.ei.curFgColor, - bgColor: v.ei.curBgColor, - chr: ch, + fgColor: v.ei.curFgColor, + bgColor: v.ei.curBgColor, + hyperlink: v.ei.hyperlink, + chr: ch, } for i := 0; i < repeatCount; i++ { cells = append(cells, c) From 45e4f00747d206dd2c2c95d60ce5311679281449 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 16 Aug 2024 09:36:55 +0200 Subject: [PATCH 5/6] Underline hyperlinks --- view.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/view.go b/view.go index 965ae7f..0752a5e 100644 --- a/view.go +++ b/view.go @@ -1190,6 +1190,9 @@ func (v *View) draw() error { if bgColor == ColorDefault { bgColor = v.BgColor } + if c.hyperlink != "" { + fgColor |= AttrUnderline + } if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil { return err From dd2d8869a598875969597f6c501f90df8276877f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 14 Aug 2024 12:53:54 +0200 Subject: [PATCH 6/6] Support clicking on hyperlinks --- gui.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gui.go b/gui.go index d702b07..9d848d9 100644 --- a/gui.go +++ b/gui.go @@ -130,6 +130,7 @@ type Gui struct { managers []Manager keybindings []*keybinding focusHandler func(bool) error + openHyperlink func(string) error maxX, maxY int outputMode OutputMode stop chan struct{} @@ -624,6 +625,10 @@ func (g *Gui) SetFocusHandler(handler func(bool) error) { g.focusHandler = handler } +func (g *Gui) SetOpenHyperlinkFunc(openHyperlinkFunc func(string) error) { + g.openHyperlink = openHyperlinkFunc +} + // getKey takes an empty interface with a key and returns the corresponding // typed Key or rune. func getKey(key interface{}) (Key, rune, error) { @@ -1367,6 +1372,14 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } } + if ev.Key == MouseLeft && !v.Editable && g.openHyperlink != nil { + if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 { + if link := v.viewLines[newY].line[newX].hyperlink; link != "" { + return g.openHyperlink(link) + } + } + } + if IsMouseKey(ev.Key) { opts := ViewMouseBindingOpts{X: newX, Y: newY} matched, err := g.execMouseKeybindings(v, ev, opts)