Refactor
This commit is contained in:
16
Makefile
Normal file
16
Makefile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o levi ./cmd/levi
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f levi
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run ./cmd/levi
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
22
cmd/levi/main.go
Normal file
22
cmd/levi/main.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tea.kareha.org/lab/levi/internal/console"
|
||||||
|
"tea.kareha.org/lab/levi/internal/editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// init
|
||||||
|
console.Raw()
|
||||||
|
defer func() {
|
||||||
|
console.Cooked()
|
||||||
|
console.ShowCursor()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// main
|
||||||
|
editor.New().Main()
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
console.Clear()
|
||||||
|
console.HomeCursor()
|
||||||
|
}
|
||||||
107
internal/console/console.go
Normal file
107
internal/console/console.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var state *term.State
|
||||||
|
|
||||||
|
func Raw() {
|
||||||
|
if state != nil {
|
||||||
|
term.Restore(int(os.Stdin.Fd()), state)
|
||||||
|
state = nil
|
||||||
|
}
|
||||||
|
s, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
state = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cooked() {
|
||||||
|
if state == nil {
|
||||||
|
panic("state is nil")
|
||||||
|
}
|
||||||
|
term.Restore(int(os.Stdin.Fd()), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clear() {
|
||||||
|
fmt.Print("\x1b[2J")
|
||||||
|
}
|
||||||
|
|
||||||
|
func HomeCursor() {
|
||||||
|
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 Size() (int, int) {
|
||||||
|
w, h, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
expected := runeSize(buf[0])
|
||||||
|
if expected == -1 {
|
||||||
|
panic("Invalid UTF-8 head")
|
||||||
|
}
|
||||||
|
full := make([]byte, expected)
|
||||||
|
full[0] = buf[0]
|
||||||
|
if expected > 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 body")
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func Print(s string) {
|
||||||
|
fmt.Print(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Printf(format string, a ...any) (n int, err error) {
|
||||||
|
return fmt.Printf(format, a...)
|
||||||
|
}
|
||||||
55
internal/editor/editor.go
Normal file
55
internal/editor/editor.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tea.kareha.org/lab/levi/internal/console"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Editor struct {
|
||||||
|
scr *Screen
|
||||||
|
kb *Keyboard
|
||||||
|
x, y int
|
||||||
|
line *strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Editor {
|
||||||
|
scr := NewScreen()
|
||||||
|
kb := NewKeyboard()
|
||||||
|
|
||||||
|
return &Editor{
|
||||||
|
scr: &scr,
|
||||||
|
kb: &kb,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
line: new(strings.Builder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ed *Editor) addRune(r rune) {
|
||||||
|
ed.line.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ed *Editor) draw() {
|
||||||
|
console.Clear()
|
||||||
|
console.HomeCursor()
|
||||||
|
|
||||||
|
console.Print("Hit Esc to Exit")
|
||||||
|
|
||||||
|
console.MoveCursor(ed.x, ed.y)
|
||||||
|
console.Print(ed.line.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ed *Editor) Main() {
|
||||||
|
for {
|
||||||
|
console.HideCursor()
|
||||||
|
ed.draw()
|
||||||
|
console.ShowCursor()
|
||||||
|
|
||||||
|
r := ed.kb.ReadRune()
|
||||||
|
if r == Esc {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ed.addRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
internal/editor/keyboard.go
Normal file
17
internal/editor/keyboard.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tea.kareha.org/lab/levi/internal/console"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Esc rune = 0x1b
|
||||||
|
|
||||||
|
type Keyboard struct {}
|
||||||
|
|
||||||
|
func NewKeyboard() Keyboard {
|
||||||
|
return Keyboard{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kb *Keyboard) ReadRune() rune {
|
||||||
|
return console.ReadRune()
|
||||||
|
}
|
||||||
21
internal/editor/screen.go
Normal file
21
internal/editor/screen.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tea.kareha.org/lab/levi/internal/console"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Screen struct {
|
||||||
|
w, h int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScreen() Screen {
|
||||||
|
w, h := console.Size()
|
||||||
|
return Screen{
|
||||||
|
w: w,
|
||||||
|
h: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scr *Screen) Size() (int, int) {
|
||||||
|
return scr.w, scr.h
|
||||||
|
}
|
||||||
49
internal/util/util.go
Normal file
49
internal/util/util.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isWide(r rune) bool {
|
||||||
|
return r >= 0x1100 && (
|
||||||
|
r <= 0x115f || // Hangul Jamo
|
||||||
|
r == 0x2329 || r == 0x232a ||
|
||||||
|
(r >= 0x2e80 && r <= 0xa4cf) ||
|
||||||
|
(r >= 0xac00 && r <= 0xd7a3) ||
|
||||||
|
(r >= 0xf900 && r <= 0xfaff) ||
|
||||||
|
(r >= 0xfe10 && r <= 0xfe19) ||
|
||||||
|
(r >= 0xfe30 && r <= 0xfe6f) ||
|
||||||
|
(r >= 0xff00 && r <= 0xff60) ||
|
||||||
|
(r >= 0xffe0 && r <= 0xffe6))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmoji(r rune) bool {
|
||||||
|
return r >= 0x1f300 && r <= 0x1faff
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuneWidth(r rune) int {
|
||||||
|
// control code
|
||||||
|
if r == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if r < 32 || (r >= 0x7f && r < 0xa0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// combining mark
|
||||||
|
if unicode.Is(unicode.Mn, r) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// wide (loose CJK)
|
||||||
|
if isWide(r) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// emoji
|
||||||
|
if isEmoji(r) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
182
main.go
182
main.go
@@ -1,182 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user