🎉 live server seems to be working now
This commit is contained in:
115
third_party/sendspin-go/internal/ui/hotkey.go
vendored
Normal file
115
third_party/sendspin-go/internal/ui/hotkey.go
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
// ABOUTME: Hotkey label rendering helper — marks the trigger character with reverse video
|
||||
// ABOUTME: Used by all TUI labels that advertise a keybinding so the cue is uniform
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ANSI escape sequences for SGR reverse-video. Kept here rather than in a
|
||||
// styling library because we only need two codes — pulling in lipgloss for
|
||||
// this would be overkill.
|
||||
const (
|
||||
ansiReverse = "\x1b[7m"
|
||||
ansiReset = "\x1b[0m"
|
||||
)
|
||||
|
||||
// hotkey renders a human-readable label with its trigger key reverse-
|
||||
// highlighted. The visual cue — a single inverted character embedded in the
|
||||
// label — is uniform across every keybinding in the TUI so users learn the
|
||||
// pattern once.
|
||||
//
|
||||
// Resolution rules:
|
||||
// - Printable letter trigger with a case-insensitive match in the label:
|
||||
// highlight the first occurrence in place.
|
||||
// hotkey('o', "Output") -> "\x1b[7mO\x1b[0mutput"
|
||||
// - Printable letter trigger NOT present in the label:
|
||||
// fall back to a bracket prefix.
|
||||
// hotkey('q', "Bye") -> "[\x1b[7mQ\x1b[0m] Bye"
|
||||
// - Non-letter triggers (space, arrow sentinels, etc.) always use the
|
||||
// bracket form with a friendly name for the key.
|
||||
// hotkey(KeySpace, "Play/Pause") -> "[\x1b[7mSpace\x1b[0m] Play/Pause"
|
||||
//
|
||||
// Empty labels still render the bracket prefix so the binding is visible.
|
||||
func hotkey(trigger rune, label string) string {
|
||||
if name, ok := specialKeyName(trigger); ok {
|
||||
return bracketPrefix(name, label)
|
||||
}
|
||||
if !unicode.IsLetter(trigger) && !unicode.IsDigit(trigger) {
|
||||
// Punctuation or symbols — use bracket form with the literal rune.
|
||||
return bracketPrefix(string(trigger), label)
|
||||
}
|
||||
if idx := firstCaseFoldIndex(label, trigger); idx >= 0 {
|
||||
r, width := runeAt(label, idx)
|
||||
return label[:idx] + ansiReverse + string(r) + ansiReset + label[idx+width:]
|
||||
}
|
||||
return bracketPrefix(strings.ToUpper(string(trigger)), label)
|
||||
}
|
||||
|
||||
// Special-key sentinels. These aren't valid Unicode code points that could
|
||||
// appear in a label, so overloading a rune-sized value is safe. Any value
|
||||
// outside the printable range works; using specific private-use-area code
|
||||
// points keeps them self-documenting in stack traces.
|
||||
const (
|
||||
KeySpace rune = '\uE000' // "Space"
|
||||
KeyUp rune = '\uE001' // "↑"
|
||||
KeyDown rune = '\uE002' // "↓"
|
||||
KeyUpDown rune = '\uE003' // "↑↓"
|
||||
KeyEnter rune = '\uE004' // "Enter"
|
||||
KeyEsc rune = '\uE005' // "Esc"
|
||||
)
|
||||
|
||||
func specialKeyName(r rune) (string, bool) {
|
||||
switch r {
|
||||
case KeySpace:
|
||||
return "Space", true
|
||||
case KeyUp:
|
||||
return "\u2191", true
|
||||
case KeyDown:
|
||||
return "\u2193", true
|
||||
case KeyUpDown:
|
||||
return "\u2191\u2193", true
|
||||
case KeyEnter:
|
||||
return "Enter", true
|
||||
case KeyEsc:
|
||||
return "Esc", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// bracketPrefix renders "[<highlighted keyName>] label" with the key name
|
||||
// reverse-highlighted. When label is empty, emits just the bracket (still
|
||||
// useful so the binding is visible in compact hint rows).
|
||||
func bracketPrefix(keyName, label string) string {
|
||||
prefix := "[" + ansiReverse + keyName + ansiReset + "]"
|
||||
if label == "" {
|
||||
return prefix
|
||||
}
|
||||
return prefix + " " + label
|
||||
}
|
||||
|
||||
// firstCaseFoldIndex returns the byte index of the first rune in s that
|
||||
// case-folds equal to needle, or -1 if not found. Works for any letter —
|
||||
// multi-byte ones included, though this codebase only uses ASCII triggers.
|
||||
func firstCaseFoldIndex(s string, needle rune) int {
|
||||
target := unicode.ToLower(needle)
|
||||
for i, r := range s {
|
||||
if unicode.ToLower(r) == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// runeAt returns the rune at byte index i and its encoded byte width. Only
|
||||
// called after firstCaseFoldIndex has confirmed a match, so the index is
|
||||
// guaranteed to be on a rune boundary.
|
||||
func runeAt(s string, i int) (rune, int) {
|
||||
for j, r := range s {
|
||||
if j == i {
|
||||
return r, len(string(r))
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
Reference in New Issue
Block a user