mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-27 05:46:34 +02:00
Single Go module, four top-level packages under packages/. Import
paths become github.com/patriceckhart/zot/packages/<name>; downstream
consumers can depend on individual packages without pulling the rest.
Layout:
packages/provider/ LLM clients + catalog
packages/provider/auth/ credential store + OAuth + login server
packages/core/ agent loop, sessions, cost
packages/tui/ terminal toolkit + chat view
packages/agent/ CLI wiring, system prompt
extensions/ extproto/ modes/ tools/ skills/ swarm/
sdk/ (was pkg/zotcore, package renamed zotcore -> sdk)
ext/ (was pkg/zotext, package renamed zotext -> ext)
internal/ and pkg/ removed. The internal/assets logo moved into
packages/provider/auth/assets.
Public Go SDK identifiers renamed:
pkg/zotcore (package zotcore) -> packages/agent/sdk (package sdk)
pkg/zotext (package zotext) -> packages/agent/ext (package ext)
This breaks Go-based extensions and embedders; the JSON wire protocol
for extensions and RPC is unchanged, so non-Go extensions, already-
built extension binaries, and zot rpc consumers are unaffected.
Docs, examples, and the built-in write-zot-extension skill updated
for the new paths and identifiers. Shadow-bug fixes in code samples
(ext := ext.New -> e := ext.New).
306 lines
6.8 KiB
Go
306 lines
6.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Key is a parsed keypress.
|
|
type Key struct {
|
|
Kind KeyKind
|
|
Rune rune // for KeyRune
|
|
Paste string // for KeyPaste
|
|
Ctrl bool
|
|
Alt bool
|
|
}
|
|
|
|
type KeyKind int
|
|
|
|
const (
|
|
KeyRune KeyKind = iota
|
|
KeyEnter
|
|
KeyBackspace
|
|
KeyTab
|
|
KeyShiftTab
|
|
KeyEsc
|
|
KeyUp
|
|
KeyDown
|
|
KeyLeft
|
|
KeyRight
|
|
KeyHome
|
|
KeyEnd
|
|
KeyPageUp
|
|
KeyPageDown
|
|
KeyDelete
|
|
KeyCtrlC
|
|
KeyCtrlD
|
|
KeyCtrlL
|
|
KeyCtrlU
|
|
KeyCtrlK
|
|
KeyCtrlA
|
|
KeyCtrlE
|
|
KeyCtrlW
|
|
KeyCtrlO
|
|
KeyPaste
|
|
KeyMouseWheelUp
|
|
KeyMouseWheelDown
|
|
KeyUnknown
|
|
)
|
|
|
|
// Reader parses a byte stream into Key events. It understands basic
|
|
// xterm escape sequences and bracketed paste.
|
|
type Reader struct {
|
|
src func() (byte, error)
|
|
peek func(time.Duration) (byte, bool, error) // optional; may be nil
|
|
}
|
|
|
|
// NewReader returns a Reader that pulls bytes from read.
|
|
func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
|
|
|
|
// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
|
|
// peek to disambiguate bare Esc from the start of an escape sequence.
|
|
func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
|
|
return &Reader{src: read, peek: peek}
|
|
}
|
|
|
|
// Read returns the next parsed Key.
|
|
func (r *Reader) Read() (Key, error) {
|
|
b, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
switch {
|
|
case b == 0x03:
|
|
return Key{Kind: KeyCtrlC}, nil
|
|
case b == 0x04:
|
|
return Key{Kind: KeyCtrlD}, nil
|
|
case b == 0x0c:
|
|
return Key{Kind: KeyCtrlL}, nil
|
|
case b == 0x15:
|
|
return Key{Kind: KeyCtrlU}, nil
|
|
case b == 0x0b:
|
|
return Key{Kind: KeyCtrlK}, nil
|
|
case b == 0x01:
|
|
return Key{Kind: KeyCtrlA}, nil
|
|
case b == 0x05:
|
|
return Key{Kind: KeyCtrlE}, nil
|
|
case b == 0x17:
|
|
return Key{Kind: KeyCtrlW}, nil
|
|
case b == 0x0f:
|
|
return Key{Kind: KeyCtrlO}, nil
|
|
case b == '\r', b == '\n':
|
|
return Key{Kind: KeyEnter}, nil
|
|
case b == '\t':
|
|
return Key{Kind: KeyTab}, nil
|
|
case b == 0x7f, b == 0x08:
|
|
return Key{Kind: KeyBackspace}, nil
|
|
case b == 0x1b:
|
|
return r.readEscape()
|
|
case b < 0x20:
|
|
return Key{Kind: KeyUnknown}, nil
|
|
}
|
|
// UTF-8 multibyte?
|
|
if b < 0x80 {
|
|
return Key{Kind: KeyRune, Rune: rune(b)}, nil
|
|
}
|
|
// Decode UTF-8 (up to 4 bytes).
|
|
n := utf8Len(b)
|
|
buf := []byte{b}
|
|
for i := 1; i < n; i++ {
|
|
bb, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
buf = append(buf, bb)
|
|
}
|
|
rn, _ := decodeRune(buf)
|
|
return Key{Kind: KeyRune, Rune: rn}, nil
|
|
}
|
|
|
|
func utf8Len(b byte) int {
|
|
switch {
|
|
case b&0xe0 == 0xc0:
|
|
return 2
|
|
case b&0xf0 == 0xe0:
|
|
return 3
|
|
case b&0xf8 == 0xf0:
|
|
return 4
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func decodeRune(b []byte) (rune, int) {
|
|
// Minimal decoder; invalid runes become U+FFFD.
|
|
if len(b) == 1 {
|
|
return rune(b[0]), 1
|
|
}
|
|
var r rune
|
|
switch len(b) {
|
|
case 2:
|
|
r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
|
|
case 3:
|
|
r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
|
|
case 4:
|
|
r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
|
|
default:
|
|
r = 0xFFFD
|
|
}
|
|
return r, len(b)
|
|
}
|
|
|
|
// readEscape handles sequences starting with 0x1b.
|
|
func (r *Reader) readEscape() (Key, error) {
|
|
// Bare ESC: maybe followed by another byte within a short window.
|
|
b, have, err := r.readEscapeNext(50 * time.Millisecond)
|
|
if err != nil || !have {
|
|
return Key{Kind: KeyEsc}, nil
|
|
}
|
|
switch b {
|
|
case '[':
|
|
return r.readCSI()
|
|
case 'O':
|
|
// SS3 sequences (function keys in some terminals).
|
|
c, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
switch c {
|
|
case 'H':
|
|
return Key{Kind: KeyHome}, nil
|
|
case 'F':
|
|
return Key{Kind: KeyEnd}, nil
|
|
}
|
|
return Key{Kind: KeyUnknown}, nil
|
|
case 0x7f, 0x08:
|
|
// Alt+Backspace (Option+Delete on macOS) — most terminals send
|
|
// ESC + DEL for this. Surface as a dedicated "alt backspace"
|
|
// so the editor can map it to delete-word.
|
|
return Key{Kind: KeyBackspace, Alt: true}, nil
|
|
case 'b':
|
|
// Emacs-style word-left, also emitted by some terminals for
|
|
// Option+LeftArrow.
|
|
return Key{Kind: KeyLeft, Alt: true}, nil
|
|
case 'f':
|
|
// Emacs-style word-right, also emitted for Option+RightArrow.
|
|
return Key{Kind: KeyRight, Alt: true}, nil
|
|
default:
|
|
// Alt+<char>
|
|
if b < 0x80 {
|
|
return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}, nil
|
|
}
|
|
|
|
// readEscapeNext tries to read one byte within d. If peek is available
|
|
// we use it (true non-blocking). Otherwise we fall back to a blocking
|
|
// read, which means bare Esc is only detected after the next keystroke.
|
|
func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
|
|
if r.peek != nil {
|
|
return r.peek(d)
|
|
}
|
|
b, err := r.src()
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
return b, true, nil
|
|
}
|
|
|
|
// readCSI parses a CSI sequence after ESC [.
|
|
func (r *Reader) readCSI() (Key, error) {
|
|
var params []byte
|
|
for {
|
|
c, err := r.src()
|
|
if err != nil {
|
|
return Key{}, err
|
|
}
|
|
if c >= 0x30 && c <= 0x3f {
|
|
params = append(params, c)
|
|
continue
|
|
}
|
|
// Final byte.
|
|
return r.dispatchCSI(string(params), c), nil
|
|
}
|
|
}
|
|
|
|
func (r *Reader) dispatchCSI(params string, final byte) Key {
|
|
// SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
|
|
// button codes 64 (up) and 65 (down). We ignore coordinates for
|
|
// now; the chat view only needs scroll direction.
|
|
if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
|
|
parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
|
|
if len(parts) >= 1 {
|
|
switch parts[0] {
|
|
case "64":
|
|
return Key{Kind: KeyMouseWheelUp}
|
|
case "65":
|
|
return Key{Kind: KeyMouseWheelDown}
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}
|
|
}
|
|
|
|
// Modified arrow keys come in as CSI 1;<mod><final>. Modifier values
|
|
// we care about: 2=Shift, 3=Alt/Option, 5=Ctrl. We only extract Alt.
|
|
var alt bool
|
|
if params != "" {
|
|
if i := strings.IndexByte(params, ';'); i >= 0 {
|
|
mod := params[i+1:]
|
|
if mod == "3" || mod == "4" || mod == "7" || mod == "8" {
|
|
// 3=Alt, 4=Shift+Alt, 7=Ctrl+Alt, 8=Ctrl+Shift+Alt
|
|
alt = true
|
|
}
|
|
}
|
|
}
|
|
switch final {
|
|
case 'A':
|
|
return Key{Kind: KeyUp, Alt: alt}
|
|
case 'B':
|
|
return Key{Kind: KeyDown, Alt: alt}
|
|
case 'C':
|
|
return Key{Kind: KeyRight, Alt: alt}
|
|
case 'D':
|
|
return Key{Kind: KeyLeft, Alt: alt}
|
|
case 'H':
|
|
return Key{Kind: KeyHome}
|
|
case 'F':
|
|
return Key{Kind: KeyEnd}
|
|
case 'Z':
|
|
return Key{Kind: KeyShiftTab}
|
|
case '~':
|
|
switch params {
|
|
case "3":
|
|
return Key{Kind: KeyDelete}
|
|
case "5":
|
|
return Key{Kind: KeyPageUp}
|
|
case "6":
|
|
return Key{Kind: KeyPageDown}
|
|
case "200":
|
|
// Start of bracketed paste.
|
|
return r.readPaste()
|
|
}
|
|
}
|
|
return Key{Kind: KeyUnknown}
|
|
}
|
|
|
|
// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
|
|
func (r *Reader) readPaste() Key {
|
|
var sb strings.Builder
|
|
const end = "\x1b[201~"
|
|
tail := make([]byte, 0, len(end))
|
|
for {
|
|
b, err := r.src()
|
|
if err != nil {
|
|
break
|
|
}
|
|
tail = append(tail, b)
|
|
if len(tail) > len(end) {
|
|
sb.WriteByte(tail[0])
|
|
tail = tail[1:]
|
|
}
|
|
if string(tail) == end {
|
|
break
|
|
}
|
|
}
|
|
return Key{Kind: KeyPaste, Paste: sb.String()}
|
|
}
|