From fb8f4eaee2dbc63082bdbf7715f45b60936dae1c Mon Sep 17 00:00:00 2001 From: Aki Kareha Date: Wed, 25 Mar 2026 03:14:28 +0900 Subject: [PATCH] Add initial version --- .gitignore | 1 + go.mod | 7 +++ go.sum | 4 ++ main.go | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d371af3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/levi diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1147796 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module tea.kareha.org/lab/levi + +go 1.25.0 + +require golang.org/x/term v0.41.0 + +require golang.org/x/sys v0.42.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..91a25f5 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f7fd2bd --- /dev/null +++ b/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + "unicode/utf8" + + "golang.org/x/term" +) + +func enableRawMode() (*term.State, error) { + return term.MakeRaw(int(os.Stdin.Fd())) +} + +func disableRawMode(state *term.State) { + term.Restore(int(os.Stdin.Fd()), state) +} + +func clearScreen() { + fmt.Print("\x1b[2J") +} + +func goHome() { + fmt.Print("\x1b[H") +} + +func moveCursor(x, y int) { + fmt.Printf("\x1b[%d;%dH", y, x) +} + +func hideCursor() { + fmt.Print("\x1b[?25l") +} + +func showCursor() { + fmt.Print("\x1b[?25h") +} + +func getScreenSize() (int, int) { + w, h, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + panic(err) + } + return w, h +} + +type Screen struct { + w, h int +} + +func NewScreen() Screen { + w, h := getScreenSize() + return Screen{ + w: w, + h: h, + } +} + +func (scr *Screen) Size() (int, int) { + return scr.w, scr.h +} + +const Esc rune = 0x1b + +func runeSize(b byte) int { + switch { + case b & 0x80 == 0: + return 1 + case b & 0xe0 == 0xc0: + return 2 + case b & 0xf0 == 0xe0: + return 3 + case b & 0xf8 == 0xf0: + return 4 + default: + return -1 // invalid + } +} + +func readRune() rune { + buf := make([]byte, 1) + _, err := io.ReadFull(os.Stdin, buf) + if err != nil { + panic(err) + } + size := runeSize(buf[0]) + if size == -1 { + panic("Invalid UTF-8") + } + full := make([]byte, size) + full[0] = buf[0] + if size > 1 { + _, err := io.ReadFull(os.Stdin, full[1:]) + if err != nil { + panic(err) + } + } + r, size := utf8.DecodeRune(full) + if r == utf8.RuneError && size == 1 { + panic("Invalid UTF-8") + } + return r +} + +type Keyboard struct {} + +func NewKeyboard() Keyboard { + return Keyboard{} +} + +func (kb *Keyboard) ReadRune() rune { + return readRune() +} + +type Editor struct { + scr *Screen + kb *Keyboard + x, y int + line *strings.Builder +} + +func NewEditor(scr *Screen, kb *Keyboard) Editor { + _, h := scr.Size() + return Editor{ + scr: scr, + x: 0, + y: h / 2, + line: new(strings.Builder), + } +} + +func (ed *Editor) Screen() *Screen { + return ed.scr +} + +func (ed *Editor) AddRune(r rune) { + ed.line.WriteRune(r) +} + +func draw(ed *Editor) { + clearScreen() + goHome() + + fmt.Print("Hit Esc to Exit") + + moveCursor(ed.x, ed.y) + fmt.Print(ed.line.String()) +} + +func main() { + // init + oldState, err := enableRawMode() + if err != nil { + panic(err) + } + defer func() { + disableRawMode(oldState) + showCursor() + }() + + // main + scr := NewScreen() + kb := NewKeyboard() + ed := NewEditor(&scr, &kb) + for { + hideCursor() + draw(&ed) + showCursor() + + r := kb.ReadRune() + if r == Esc { + break + } + ed.AddRune(r) + } + + // cleanup + clearScreen() + goHome() +}