v0.1.0 initial commit

This commit is contained in:
2026-07-12 17:01:37 -04:00
commit c3ca2ebb63
64 changed files with 10453 additions and 0 deletions
+601
View File
@@ -0,0 +1,601 @@
package terminal
import (
"fmt"
"os"
"runtime/debug"
"sync"
"time"
)
// EventType distinguishes input event categories
type EventType uint8
const (
EventKey EventType = iota
EventResize
EventPaste // Future: bracketed paste
EventMouse // Future: mouse support
EventError // Read error
EventClosed // Input closed
)
// Event represents a terminal input event
type Event struct {
Type EventType
Key Key
Rune rune
Modifiers Modifier
Width int // For EventResize
Height int // For EventResize
Err error // For EventError
// Mouse event fields
MouseX int
MouseY int
MouseBtn MouseButton
MouseAction MouseAction
}
// inputReader handles raw stdin parsing
type inputReader struct {
backend Backend
eventCh chan Event
stopCh chan struct{}
doneCh chan struct{}
mu sync.Mutex
running bool
// Persistent buffer for stream assembly, not fixed size zero-alloc to avoid corrupting partial UTF-8 at boundary
buf []byte
}
// escapeTimeout is the duration to wait after ESC to distinguish
// standalone ESC from escape sequence start
const escapeTimeout = 50 * time.Millisecond
// newInputReader creates a new input reader
func newInputReader(backend Backend) *inputReader {
return &inputReader{
backend: backend,
eventCh: make(chan Event, 256),
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
buf: make([]byte, 0, 256),
}
}
// start begins reading input in a goroutine
func (r *inputReader) start() {
r.mu.Lock()
if r.running {
r.mu.Unlock()
return
}
r.running = true
r.mu.Unlock()
go r.readLoop()
}
// stop signals the reader to stop
func (r *inputReader) stop() {
r.mu.Lock()
if !r.running {
r.mu.Unlock()
return
}
r.running = false
r.mu.Unlock()
close(r.stopCh)
// Wait with timeout - don't block forever if read is stuck
select {
case <-r.doneCh:
case <-time.After(100 * time.Millisecond):
// Reader stuck on blocking read, proceed anyway
}
}
// events returns the event channel
func (r *inputReader) events() <-chan Event {
return r.eventCh
}
// readLoop is the main input reading goroutine
func (r *inputReader) readLoop() {
defer close(r.doneCh)
// Panic recovery for raw input reader
defer func() {
if r := recover(); r != nil {
EmergencyReset(os.Stdout)
// Use \r\n for clean output
fmt.Fprintf(os.Stderr, "\r\n\x1b[31mINPUT READER CRASHED: %v\x1b[0m\r\n", r)
fmt.Fprintf(os.Stderr, "Stack Trace:\r\n%s\r\n", debug.Stack())
os.Exit(1)
}
}()
for {
// Blocking read from backend
data, err := r.backend.Read(r.stopCh)
if err != nil {
r.sendEvent(Event{Type: EventError, Err: err})
return
}
if len(data) == 0 {
// Timeout (Unix poll) or empty read
// Emit pending standalone ESC if present
if len(r.buf) == 1 && r.buf[0] == 0x1b {
r.sendEvent(Event{Type: EventKey, Key: KeyEscape})
r.buf = r.buf[:0]
}
select {
case <-r.stopCh:
r.sendEvent(Event{Type: EventClosed})
return
default:
continue
}
}
// Append to persistent buffer
r.buf = append(r.buf, data...)
// Parse as much as possible, get consumed count
consumed := r.parseInput(r.buf)
// Compact buffer
if consumed > 0 {
if consumed >= len(r.buf) {
r.buf = r.buf[:0]
} else {
copy(r.buf, r.buf[consumed:])
r.buf = r.buf[:len(r.buf)-consumed]
}
}
}
}
// parseInput parses raw bytes into events and returns bytes consumed (stop on incomplete sequence)
func (r *inputReader) parseInput(data []byte) int {
i := 0
n := len(data)
for i < n {
select {
case <-r.stopCh:
return i
default:
}
b := data[i]
// Fast path: printable ASCII
if b >= 0x20 && b < 0x7f {
r.sendEvent(Event{Type: EventKey, Key: KeyRune, Rune: rune(b)})
i++
continue
}
// Escape sequence
if b == 0x1b {
// Need at least 2 bytes to determine sequence type
if i+1 >= n {
return i // Wait for more data
}
consumed, ev := r.parseEscape(data[i:])
if consumed == 0 {
// Incomplete sequence, wait for more data
return i
}
// Only emit if not a swallowed unknown sequence
if ev.Key != KeyNone || ev.Type != EventKey {
r.sendEvent(ev)
}
i += consumed
continue
}
// Control characters
if b < 0x20 {
r.sendEvent(r.parseControl(b))
i++
continue
}
// DEL
if b == 0x7f {
r.sendEvent(Event{Type: EventKey, Key: KeyBackspace})
i++
continue
}
// UTF-8 multibyte
if b >= 0x80 {
// Check if full sequence available
seqLen := utf8SeqLen(b)
if seqLen == 0 {
// Invalid start byte, skip
i++
continue
}
if i+seqLen > n {
// Incomplete UTF-8, wait for more data
return i
}
rn, size := decodeRune(data[i:])
r.sendEvent(Event{Type: EventKey, Key: KeyRune, Rune: rn})
i += size
continue
}
i++
}
return i
}
// utf8SeqLen returns expected UTF-8 sequence length from start byte, 0 if invalid
func utf8SeqLen(b byte) int {
if b < 0x80 {
return 1
}
if b&0xe0 == 0xc0 {
return 2
}
if b&0xf0 == 0xe0 {
return 3
}
if b&0xf8 == 0xf0 {
return 4
}
return 0 // Invalid
}
// parseEscape attempts to parse an escape sequence, returns 0 on incomplete
func (r *inputReader) parseEscape(data []byte) (int, Event) {
if len(data) < 2 {
return 0, Event{} // Incomplete, wait for more
}
// ESC ESC -> Alt+Escape
if data[1] == 0x1b {
return 2, Event{Type: EventKey, Key: KeyEscape, Modifiers: ModAlt}
}
if data[1] == '[' {
return r.parseCSI(data)
}
if data[1] == 'O' {
return r.parseSS3(data)
}
// Alt+Control character (ESC + 0x00-0x1F)
if data[1] < 0x20 {
ev := r.parseControl(data[1])
ev.Modifiers |= ModAlt
return 2, ev
}
// Alt+printable
if data[1] >= 0x20 && data[1] < 0x7f {
return 2, Event{Type: EventKey, Key: KeyRune, Rune: rune(data[1]), Modifiers: ModAlt}
}
return 0, Event{}
}
// parseCSI parses CSI sequence without allocation
func (r *inputReader) parseCSI(data []byte) (int, Event) {
if len(data) < 3 {
return 0, Event{}
}
// SGR mouse: ESC [ < Btn ; X ; Y M/m
if data[2] == '<' {
return r.parseSGRMouse(data)
}
end := 2
maxScan := len(data)
if maxScan > 16 {
maxScan = 16
}
for end < maxScan {
b := data[end]
if (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || b == '~' {
end++
break
}
if b < 0x20 || b > 0x7e {
return 0, Event{}
}
end++
}
// Check if we found a terminator or ran out of data
if end <= 2 || end > maxScan {
return 0, Event{} // Incomplete
}
// Check last byte is valid terminator
lastByte := data[end-1]
if !((lastByte >= 'A' && lastByte <= 'Z') || (lastByte >= 'a' && lastByte <= 'z') || lastByte == '~') {
return 0, Event{} // Incomplete, no terminator found
}
if key, mod, ok := lookupCSI(data[2:end]); ok {
return end, Event{Type: EventKey, Key: key, Modifiers: mod}
}
// Unknown but valid CSI syntax - consume and return KeyNone
return end, Event{Type: EventKey, Key: KeyNone}
}
// parseSS3 parses SS3 sequence without allocation, returns length even for unknown sequences
func (r *inputReader) parseSS3(data []byte) (int, Event) {
if len(data) < 3 {
return 0, Event{}
}
if key, mod, ok := lookupSS3(data[2:3]); ok {
return 3, Event{Type: EventKey, Key: key, Modifiers: mod}
}
// Unknown SS3 - consume to prevent garbage
return 3, Event{Type: EventKey, Key: KeyNone}
}
// parseControl maps control characters to keys
func (r *inputReader) parseControl(b byte) Event {
switch b {
case 0x00: // Ctrl+Space or Ctrl+@
return Event{Type: EventKey, Key: KeyCtrlSpace}
case 0x01:
return Event{Type: EventKey, Key: KeyCtrlA}
case 0x02:
return Event{Type: EventKey, Key: KeyCtrlB}
case 0x03:
return Event{Type: EventKey, Key: KeyCtrlC}
case 0x04:
return Event{Type: EventKey, Key: KeyCtrlD}
case 0x05:
return Event{Type: EventKey, Key: KeyCtrlE}
case 0x06:
return Event{Type: EventKey, Key: KeyCtrlF}
case 0x07:
return Event{Type: EventKey, Key: KeyCtrlG}
case 0x08: // Ctrl+H or Backspace
return Event{Type: EventKey, Key: KeyBackspace}
case 0x09: // Tab
return Event{Type: EventKey, Key: KeyTab}
case 0x0a, 0x0d: // LF, CR (Enter)
return Event{Type: EventKey, Key: KeyEnter}
case 0x0b:
return Event{Type: EventKey, Key: KeyCtrlK}
case 0x0c:
return Event{Type: EventKey, Key: KeyCtrlL}
case 0x0e:
return Event{Type: EventKey, Key: KeyCtrlN}
case 0x0f:
return Event{Type: EventKey, Key: KeyCtrlO}
case 0x10:
return Event{Type: EventKey, Key: KeyCtrlP}
case 0x11:
return Event{Type: EventKey, Key: KeyCtrlQ}
case 0x12:
return Event{Type: EventKey, Key: KeyCtrlR}
case 0x13:
return Event{Type: EventKey, Key: KeyCtrlS}
case 0x14:
return Event{Type: EventKey, Key: KeyCtrlT}
case 0x15:
return Event{Type: EventKey, Key: KeyCtrlU}
case 0x16:
return Event{Type: EventKey, Key: KeyCtrlV}
case 0x17:
return Event{Type: EventKey, Key: KeyCtrlW}
case 0x18:
return Event{Type: EventKey, Key: KeyCtrlX}
case 0x19:
return Event{Type: EventKey, Key: KeyCtrlY}
case 0x1a:
return Event{Type: EventKey, Key: KeyCtrlZ}
case 0x1b: // ESC (shouldn't reach here normally)
return Event{Type: EventKey, Key: KeyEscape}
case 0x1c:
return Event{Type: EventKey, Key: KeyCtrlBackslash}
case 0x1d:
return Event{Type: EventKey, Key: KeyCtrlBracketRight}
case 0x1e:
return Event{Type: EventKey, Key: KeyCtrlCaret}
case 0x1f:
return Event{Type: EventKey, Key: KeyCtrlUnderscore}
}
return Event{Type: EventKey, Key: KeyNone}
}
// parseSGRMouse parses mouse SGR sequences
func (r *inputReader) parseSGRMouse(data []byte) (int, Event) {
// Format: ESC [ < Btn ; X ; Y M/m
// Minimum: ESC [ < 0 ; 1 ; 1 M = 10 bytes
if len(data) < 10 {
return 0, Event{}
}
// Find terminator M or m
end := 3
for end < len(data) && end < 32 {
if data[end] == 'M' || data[end] == 'm' {
break
}
end++
}
if end >= len(data) || (data[end] != 'M' && data[end] != 'm') {
return 0, Event{}
}
// Parse: Btn;X;Y
params := data[3:end]
btn, x, y, ok := parseSGRParams(params)
if !ok {
return 0, Event{}
}
ev := Event{Type: EventMouse, MouseX: x - 1, MouseY: y - 1} // Convert to 0-indexed
// Decode button and action
// Bits 0-1: button (0=left, 1=middle, 2=right, 3=release)
// Bit 5 (32): motion
// Bit 6 (64): scroll
buttonID := btn & 0x03
isMotion := btn&32 != 0
isScroll := btn&64 != 0
if isScroll {
// Scroll: buttonID 0=up, 1=down
if buttonID == 0 {
ev.MouseBtn = MouseBtnWheelUp
} else {
ev.MouseBtn = MouseBtnWheelDown
}
ev.MouseAction = MouseActionPress // Scroll is instantaneous
} else {
// Regular button
switch buttonID {
case 0:
ev.MouseBtn = MouseBtnLeft
case 1:
ev.MouseBtn = MouseBtnMiddle
case 2:
ev.MouseBtn = MouseBtnRight
case 3:
ev.MouseBtn = MouseBtnNone // Release with no specific button
}
if data[end] == 'M' {
if isMotion {
if ev.MouseBtn != MouseBtnNone {
ev.MouseAction = MouseActionDrag
} else {
ev.MouseAction = MouseActionMove
}
} else {
ev.MouseAction = MouseActionPress
}
} else {
ev.MouseAction = MouseActionRelease
}
}
// Extract modifiers from button byte
if btn&4 != 0 {
ev.Modifiers |= ModShift
}
if btn&8 != 0 {
ev.Modifiers |= ModAlt
}
if btn&16 != 0 {
ev.Modifiers |= ModCtrl
}
return end + 1, ev
}
// parseSGRParams extracts btn, x, y from "Btn;X;Y" format
func parseSGRParams(data []byte) (btn, x, y int, ok bool) {
state := 0 // 0=btn, 1=x, 2=y
val := 0
for _, b := range data {
if b == ';' {
switch state {
case 0:
btn = val
case 1:
x = val
}
state++
val = 0
if state > 2 {
return 0, 0, 0, false
}
} else if b >= '0' && b <= '9' {
val = val*10 + int(b-'0')
if val > 9999 { // Sanity limit
return 0, 0, 0, false
}
} else {
return 0, 0, 0, false
}
}
if state != 2 {
return 0, 0, 0, false
}
y = val
return btn, x, y, true
}
// sendEvent sends an event to the channel, non-blocking
func (r *inputReader) sendEvent(ev Event) {
select {
case r.eventCh <- ev:
default:
// Channel full, drop event (shouldn't happen with 64 buffer)
}
}
// decodeRune decodes the first UTF-8 rune from data
func decodeRune(data []byte) (rune, int) {
if len(data) == 0 {
return 0, 0
}
b := data[0]
if b < 0x80 {
return rune(b), 1
}
var size int
var min rune
var r rune
switch {
case b&0xe0 == 0xc0:
size = 2
min = 0x80
r = rune(b & 0x1f)
case b&0xf0 == 0xe0:
size = 3
min = 0x800
r = rune(b & 0x0f)
case b&0xf8 == 0xf0:
size = 4
min = 0x10000
r = rune(b & 0x07)
default:
return 0xFFFD, 1 // Invalid, return replacement char
}
if len(data) < size {
return 0xFFFD, 1
}
for i := 1; i < size; i++ {
if data[i]&0xc0 != 0x80 {
return 0xFFFD, 1
}
r = r<<6 | rune(data[i]&0x3f)
}
if r < min {
return 0xFFFD, 1 // Overlong encoding
}
return r, size
}