Add terminal pkg
This commit is contained in:
@@ -0,0 +1,276 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Key Key
|
||||||
|
Rune rune
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reader struct {
|
||||||
|
buf *bufio.Reader
|
||||||
|
events chan *Event
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parameters struct {
|
||||||
|
Chan chan *Event
|
||||||
|
In *os.File
|
||||||
|
Log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(p Parameters) *Reader {
|
||||||
|
return &Reader{
|
||||||
|
buf: bufio.NewReader(p.In),
|
||||||
|
events: p.Chan,
|
||||||
|
log: p.Log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send delivers e, or gives up when ctx is done.
|
||||||
|
func (i *Reader) send(ctx context.Context, e *Event) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case i.events <- e:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reading one rune at a time is slow, especially when pasting large
|
||||||
|
// quantities of text into the active buffer. It would be nice to take more
|
||||||
|
// input at once, while still being able to handle escape sequences without
|
||||||
|
// too many edge cases.
|
||||||
|
func (i *Reader) Run(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
r, size, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
}
|
||||||
|
i.log.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"read rune",
|
||||||
|
slog.String("rune", string(r)),
|
||||||
|
slog.Any("bytes", []byte(string(r))),
|
||||||
|
slog.Int("size", size),
|
||||||
|
)
|
||||||
|
switch r {
|
||||||
|
case utf8.RuneError:
|
||||||
|
i.log.ErrorContext(
|
||||||
|
ctx,
|
||||||
|
"rune error",
|
||||||
|
slog.String("rune", string(r)),
|
||||||
|
slog.Int("size", size),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle escape sequences.
|
||||||
|
case Escape:
|
||||||
|
if err := i.parseEscapeSequence(ctx); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"failed to read: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if err := i.send(ctx, &Event{Rune: r}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Reader) parseEscapeSequence(ctx context.Context) error {
|
||||||
|
r1, _, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore invalid escape sequences.
|
||||||
|
if r1 != '[' && r1 != 'O' {
|
||||||
|
if err := i.send(ctx, &Event{Rune: r1}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've received an input of Esc + [ or Esc + O.
|
||||||
|
// Determine the escape sequence.
|
||||||
|
r2, _, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check letter escape sequences.
|
||||||
|
switch r2 {
|
||||||
|
case 'A':
|
||||||
|
if err := i.send(ctx, &Event{Key: Up}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case 'B':
|
||||||
|
if err := i.send(ctx, &Event{Key: Down}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case 'C':
|
||||||
|
if err := i.send(ctx, &Event{Key: Right}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case 'D':
|
||||||
|
if err := i.send(ctx, &Event{Key: Left}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case 'O':
|
||||||
|
r3, _, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
}
|
||||||
|
switch r3 {
|
||||||
|
case 'P': // F1
|
||||||
|
return nil
|
||||||
|
case 'Q': // F2
|
||||||
|
return nil
|
||||||
|
case 'R': // F3
|
||||||
|
return nil
|
||||||
|
case 'S': // F4
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
// No match.
|
||||||
|
if err := i.send(ctx, &Event{Rune: r1}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := i.send(ctx, &Event{Rune: r2}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := i.send(ctx, &Event{Rune: r3}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for single digit numerical escape sequences.
|
||||||
|
r3, _, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r2 == '1' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: Home}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: Insert}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '3' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Rune: Delete}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '4' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: End}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '5' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: PageUp}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '6' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: PageDown}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '7' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: Home}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '8' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: End}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case r2 == '9' && r3 == '~':
|
||||||
|
if err := i.send(ctx, &Event{Key: End}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for double digit numerical escape sequences.
|
||||||
|
r4, _, err := i.buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read: %w", err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r2 == '1' && r3 == '0' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '1' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '2' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '3' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '4' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '4' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '6' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '7' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '8' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '1' && r3 == '9' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '0' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '1' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '2' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '3' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r2 == '2' && r3 == '4' && r4 == '~':
|
||||||
|
return nil
|
||||||
|
case r4 == '~':
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match.
|
||||||
|
if err := i.send(ctx, &Event{Rune: r1}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := i.send(ctx, &Event{Rune: r2}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := i.send(ctx, &Event{Rune: r3}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := i.send(ctx, &Event{Rune: r4}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
type Key uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Down Key = iota + 1
|
||||||
|
Left
|
||||||
|
Right
|
||||||
|
Up
|
||||||
|
End
|
||||||
|
Home
|
||||||
|
Insert
|
||||||
|
PageDown
|
||||||
|
PageUp
|
||||||
|
)
|
||||||
|
|
||||||
|
const Control rune = 0x1f
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Control Characters
|
||||||
|
Null rune = 0
|
||||||
|
StartOfHeading rune = 1
|
||||||
|
StartOfText rune = 2
|
||||||
|
EndOfText rune = 3
|
||||||
|
EndOfTransmission rune = 4
|
||||||
|
Enquiry rune = 5
|
||||||
|
Acknowledgment rune = 6
|
||||||
|
Bell rune = 7
|
||||||
|
Backspace rune = 8
|
||||||
|
HorizontalTab rune = 9
|
||||||
|
LineFeed rune = 10
|
||||||
|
VerticalTab rune = 11
|
||||||
|
FormFeed rune = 12
|
||||||
|
CarriageReturn rune = 13
|
||||||
|
ShiftOut rune = 14
|
||||||
|
ShiftIn rune = 15
|
||||||
|
DataLineEscape rune = 16
|
||||||
|
DeviceControl1 rune = 17
|
||||||
|
DeviceControl2 rune = 18
|
||||||
|
DeviceControl3 rune = 19
|
||||||
|
DeviceControl4 rune = 20
|
||||||
|
NegativeAcknowledgment rune = 21
|
||||||
|
SynchronousIdle rune = 22
|
||||||
|
EndOfTransmitBlock rune = 23
|
||||||
|
Cancel rune = 24
|
||||||
|
EndOfMedium rune = 25
|
||||||
|
Substitute rune = 26
|
||||||
|
Escape rune = 27
|
||||||
|
FileSeparator rune = 28
|
||||||
|
GroupSeparator rune = 29
|
||||||
|
RecordSeparator rune = 30
|
||||||
|
UnitSeparator rune = 31
|
||||||
|
|
||||||
|
// Printable Characters
|
||||||
|
Space rune = 32
|
||||||
|
ExclamationMark rune = 33
|
||||||
|
DoubleQuote rune = 34
|
||||||
|
Number rune = 35
|
||||||
|
Dollar rune = 36
|
||||||
|
Percentage rune = 37
|
||||||
|
Ampersand rune = 38
|
||||||
|
SingleQuote rune = 39
|
||||||
|
LeftParenthesis rune = 40
|
||||||
|
RightParenthesis rune = 41
|
||||||
|
Asterisk rune = 42
|
||||||
|
Plus rune = 43
|
||||||
|
Comma rune = 44
|
||||||
|
Hyphen rune = 45
|
||||||
|
Period rune = 46
|
||||||
|
ForwardSlash rune = 47
|
||||||
|
Zero rune = 48
|
||||||
|
One rune = 49
|
||||||
|
Two rune = 50
|
||||||
|
Three rune = 51
|
||||||
|
Four rune = 52
|
||||||
|
Five rune = 53
|
||||||
|
Six rune = 54
|
||||||
|
Seven rune = 55
|
||||||
|
Eight rune = 56
|
||||||
|
Nine rune = 57
|
||||||
|
Colon rune = 58
|
||||||
|
Semicolon rune = 59
|
||||||
|
LessThan rune = 60
|
||||||
|
Equals rune = 61
|
||||||
|
GreaterThan rune = 62
|
||||||
|
QuestionMark rune = 63
|
||||||
|
At rune = 64
|
||||||
|
UpperA rune = 65
|
||||||
|
UpperB rune = 66
|
||||||
|
UpperC rune = 67
|
||||||
|
UpperD rune = 68
|
||||||
|
UpperE rune = 69
|
||||||
|
UpperF rune = 70
|
||||||
|
UpperG rune = 71
|
||||||
|
UpperH rune = 72
|
||||||
|
UpperI rune = 73
|
||||||
|
UpperJ rune = 74
|
||||||
|
UpperK rune = 75
|
||||||
|
UpperL rune = 76
|
||||||
|
UpperM rune = 77
|
||||||
|
UpperN rune = 78
|
||||||
|
UpperO rune = 79
|
||||||
|
UpperP rune = 80
|
||||||
|
UpperQ rune = 81
|
||||||
|
UpperR rune = 82
|
||||||
|
UpperS rune = 83
|
||||||
|
UpperT rune = 84
|
||||||
|
UpperU rune = 85
|
||||||
|
UpperV rune = 86
|
||||||
|
UpperW rune = 87
|
||||||
|
UpperX rune = 88
|
||||||
|
UpperY rune = 89
|
||||||
|
UpperZ rune = 90
|
||||||
|
LeftBracket rune = 91
|
||||||
|
Backslash rune = 92
|
||||||
|
RightBracket rune = 93
|
||||||
|
Caret rune = 94
|
||||||
|
Underscore rune = 95
|
||||||
|
Grave rune = 96
|
||||||
|
LowerA rune = 97
|
||||||
|
LowerB rune = 98
|
||||||
|
LowerC rune = 99
|
||||||
|
LowerD rune = 100
|
||||||
|
LowerE rune = 101
|
||||||
|
LowerF rune = 102
|
||||||
|
LowerG rune = 103
|
||||||
|
LowerH rune = 104
|
||||||
|
LowerI rune = 105
|
||||||
|
LowerJ rune = 106
|
||||||
|
LowerK rune = 107
|
||||||
|
LowerL rune = 108
|
||||||
|
LowerM rune = 109
|
||||||
|
LowerN rune = 110
|
||||||
|
LowerO rune = 111
|
||||||
|
LowerP rune = 112
|
||||||
|
LowerQ rune = 113
|
||||||
|
LowerR rune = 114
|
||||||
|
LowerS rune = 115
|
||||||
|
LowerT rune = 116
|
||||||
|
LowerU rune = 117
|
||||||
|
LowerV rune = 118
|
||||||
|
LowerW rune = 119
|
||||||
|
LowerX rune = 120
|
||||||
|
LowerY rune = 121
|
||||||
|
LowerZ rune = 122
|
||||||
|
LeftBrace rune = 123
|
||||||
|
VerticalBar rune = 124
|
||||||
|
RightBrace rune = 125
|
||||||
|
Tilde rune = 126
|
||||||
|
Delete rune = 127
|
||||||
|
)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClearScreen = "\u001Bc"
|
||||||
|
CursorHide = "\u001B[?25l"
|
||||||
|
CursorShow = "\u001B[?25h"
|
||||||
|
CursorTopLeft = "\u001B[H"
|
||||||
|
EraseLine = "\u001B[K"
|
||||||
|
|
||||||
|
SetCursorFmt = "\u001B[%d;%dH"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Terminal struct {
|
||||||
|
fd uintptr
|
||||||
|
termios *syscall.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
type Size struct {
|
||||||
|
Rows uint16
|
||||||
|
Columns uint16
|
||||||
|
XPixels uint16
|
||||||
|
YPixels uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fd uintptr) (*Terminal, error) {
|
||||||
|
var t = &Terminal{
|
||||||
|
fd: fd,
|
||||||
|
termios: &syscall.Termios{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
fd,
|
||||||
|
syscall.TCGETS,
|
||||||
|
uintptr(unsafe.Pointer(t.termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return nil, fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the terminal into the original termios.
|
||||||
|
func (t *Terminal) Reset() error {
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
uintptr(syscall.TCSETS),
|
||||||
|
uintptr(unsafe.Pointer(t.termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRaw enables raw mode.
|
||||||
|
func (t *Terminal) SetRaw() error {
|
||||||
|
var termios = *t.termios
|
||||||
|
termios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
|
||||||
|
termios.Oflag &^= syscall.OPOST
|
||||||
|
termios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
||||||
|
termios.Cflag &^= syscall.CSIZE | syscall.PARENB
|
||||||
|
termios.Cflag |= syscall.CS8
|
||||||
|
termios.Cc[syscall.VMIN] = 1
|
||||||
|
termios.Cc[syscall.VTIME] = 0
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
uintptr(syscall.TCSETS),
|
||||||
|
uintptr(unsafe.Pointer(&termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Size() (*Size, error) {
|
||||||
|
var size = &Size{}
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
syscall.TIOCGWINSZ,
|
||||||
|
uintptr(unsafe.Pointer(size)),
|
||||||
|
); err != 0 {
|
||||||
|
return nil, fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user