ux improvements

This commit is contained in:
Luxferre
2026-09-10 10:16:35 +03:00
parent ed5eb88ba9
commit c0350f5d9b
+103 -2
View File
@@ -1631,6 +1631,29 @@ func (e *editor) histNav(up bool) {
e.pos = len(e.buf)
}
// wordBack moves the cursor back to the start of the current/previous word.
func (e *editor) wordBack() {
for e.pos > 0 && unicode.IsSpace(e.buf[e.pos-1]) { e.pos-- }
for e.pos > 0 && !unicode.IsSpace(e.buf[e.pos-1]) { e.pos-- }
}
// wordFwd moves the cursor forward to the end of the current/next word.
func (e *editor) wordFwd() {
n := len(e.buf)
for e.pos < n && unicode.IsSpace(e.buf[e.pos]) { e.pos++ }
for e.pos < n && !unicode.IsSpace(e.buf[e.pos]) { e.pos++ }
}
// delWordBack deletes the word preceding the cursor (Emacs Ctrl+W).
func (e *editor) delWordBack() {
if e.pos == 0 { return }
start := e.pos
for start > 0 && unicode.IsSpace(e.buf[start-1]) { start-- }
for start > 0 && !unicode.IsSpace(e.buf[start-1]) { start-- }
e.buf = append(e.buf[:start], e.buf[e.pos:]...)
e.pos = start
}
func readPlain(prompt string) (string, bool) {
fmt.Print(prompt)
line, err := stdin.ReadString('\n')
@@ -1681,6 +1704,25 @@ func readLine(prompt string) (string, bool) {
fmt.Print("\r\n")
return "", false
}
// Ctrl+D with non-empty buffer: delete character under cursor
if e.pos < len(e.buf) {
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
}
case 0x01:
e.pos = 0 // Ctrl+A: beginning of line
case 0x05:
e.pos = len(e.buf) // Ctrl+E: end of line
case 0x02:
if e.pos > 0 { e.pos-- } // Ctrl+B: backward char
case 0x06:
if e.pos < len(e.buf) { e.pos++ } // Ctrl+F: forward char
case 0x17:
e.delWordBack() // Ctrl+W: delete previous word
case 0x0b:
e.buf = e.buf[:e.pos] // Ctrl+K: kill to end of line
case 0x15:
e.buf = e.buf[e.pos:] // Ctrl+U: kill to start of line
e.pos = 0
case 0x7f, 0x08:
if e.pos > 0 {
e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...)
@@ -1688,10 +1730,56 @@ func readLine(prompt string) (string, bool) {
}
case 0x1b:
b1, err1 := stdin.ReadByte()
b2, err2 := stdin.ReadByte()
if err1 != nil || err2 != nil { continue }
if err1 != nil { continue }
if b1 == '[' {
// Read the full CSI sequence (terminated by a byte in 0x40-0x7e).
var seq []byte
for {
b, err := stdin.ReadByte()
if err != nil { break }
seq = append(seq, b)
if b >= 0x40 && b <= 0x7e { break }
}
if len(seq) == 0 { continue }
last := seq[len(seq)-1]
switch last {
case 'A':
e.histNav(true)
case 'B':
e.histNav(false)
case 'C':
if strings.Contains(string(seq), "5") {
e.wordFwd() // Ctrl+Right
} else if e.pos < len(e.buf) {
e.pos++
}
case 'D':
if strings.Contains(string(seq), "5") {
e.wordBack() // Ctrl+Left
} else if e.pos > 0 {
e.pos--
}
case 'H':
e.pos = 0 // Home (ESC[H)
case 'F':
e.pos = len(e.buf) // End (ESC[F)
case '~':
// Home/End on some terminals: ESC[1~ / ESC[4~
if len(seq) >= 1 && seq[0] == '1' {
e.pos = 0
} else if len(seq) >= 1 && seq[0] == '4' {
e.pos = len(e.buf)
}
}
} else if b1 == 'O' {
// xterm application cursor keys: ESC O H/F (Home/End), A-D (arrows)
b2, err2 := stdin.ReadByte()
if err2 != nil { continue }
switch b2 {
case 'H':
e.pos = 0
case 'F':
e.pos = len(e.buf)
case 'A':
e.histNav(true)
case 'B':
@@ -1808,6 +1896,17 @@ func main() {
u = strings.TrimSpace(u)
if u == "" { continue }
addHistory(u)
// Command aliases: /model -> /cfg model, /endpoint -> /cfg endpoint.
// (Note: /models is a distinct command and is intentionally not matched.)
if u == "/model" {
u = "/cfg model"
} else if strings.HasPrefix(u, "/model ") {
u = "/cfg model " + strings.TrimSpace(strings.TrimPrefix(u, "/model "))
} else if u == "/endpoint" {
u = "/cfg endpoint"
} else if strings.HasPrefix(u, "/endpoint ") {
u = "/cfg endpoint " + strings.TrimSpace(strings.TrimPrefix(u, "/endpoint "))
}
switch {
case u == "/quit":
goto done
@@ -1911,6 +2010,8 @@ func main() {
{"/load <id>", "load session"},
{"/compact", "compact context"},
{"/cfg <k> [v]", "get/set config"},
{"/model [m]", "alias for /cfg model"},
{"/endpoint [e]", "alias for /cfg endpoint"},
{"!<cmd>", "run shell command directly"},
{"/models", "list models at endpoint"},
{"/help", "show help"},