Extract termi

This commit is contained in:
2026-03-26 23:11:48 +09:00
parent 6bf5887c75
commit 3fb8660720
10 changed files with 58 additions and 302 deletions

View File

@@ -1,5 +1,8 @@
all: build
env:
go env -w GOPRIVATE=tea.kareha.org
build:
go build -o levi ./cmd/levi

5
go.mod
View File

@@ -2,6 +2,9 @@ module tea.kareha.org/lab/levi
go 1.25.0
require golang.org/x/term v0.41.0
require (
golang.org/x/term v0.41.0 // indirect
tea.kareha.org/lab/termi v0.0.0-20260326135653-28299eeba224
)
require golang.org/x/sys v0.42.0 // indirect

2
go.sum
View File

@@ -2,3 +2,5 @@ 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=
tea.kareha.org/lab/termi v0.0.0-20260326135653-28299eeba224 h1:nf3D+GjzIP9ab7fXIZmaFloRFD478SV4+bPhB9Wa1U0=
tea.kareha.org/lab/termi v0.0.0-20260326135653-28299eeba224/go.mod h1:+ticjUt1pyuink8Qip4QHN3GGz1QaNPRuJrM+jWRLgU=

View File

@@ -1,65 +0,0 @@
package console
import (
"fmt"
"os"
"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("invalid state")
}
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+1, x+1)
}
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 {
return 80, 24
}
return w, h
}
func EnableInvert() {
fmt.Print("\x1b[7m")
}
func DisableInvert() {
fmt.Print("\x1b[0m")
}

View File

@@ -1,95 +0,0 @@
package console
import (
"io"
"os"
"unicode/utf8"
)
type Key int
const (
KeyNormal Key = iota
KeyUp
KeyDown
KeyRight
KeyLeft
)
const RuneEscape rune = 0x1b
const RuneEnter rune = '\r'
const RuneBackspace rune = '\b'
const RuneDelete rune = 0x7f
var buf []rune = make([]rune, 0)
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 ReadKey() (Key, rune) {
if len(buf) > 0 {
r := buf[0]
buf = buf[1:]
return KeyNormal, r
}
r := readRune()
if r != RuneEscape {
return KeyNormal, r
}
r2 := readRune()
if r2 != '[' {
buf = append(buf, r2)
return KeyNormal, r
}
r3 := readRune()
switch r3 {
case 'A':
return KeyUp, 0
case 'B':
return KeyDown, 0
case 'C':
return KeyRight, 0
case 'D':
return KeyLeft, 0
}
buf = append(buf, r2)
buf = append(buf, r3)
return KeyNormal, r
}

View File

@@ -1,92 +0,0 @@
package console
import (
"fmt"
"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
}
const tabWidth = 4
func runeWidth(r rune, x int) int {
// tab
if r == '\t' {
return tabWidth - (x % tabWidth)
}
// 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
}
func StringWidth(s string, col int) int {
sum := 0
i := 0
for _, r := range s {
if i >= col {
break
}
w := runeWidth(r, sum)
sum += w
i++
}
return sum
}
func Print(s string) {
x := 0
for _, r := range s {
if r == '\t' {
spaces := tabWidth - (x % tabWidth)
for i := 0; i < spaces; i++ {
fmt.Print(" ")
}
x += spaces
} else {
fmt.Printf("%c", r)
x += runeWidth(r, x)
}
}
}
func Printf(format string, a ...any) (n int, err error) {
s := fmt.Sprintf(format, a...)
Print(s)
return len(s), nil
}

View File

@@ -52,6 +52,6 @@ func (ed *Editor) deleteRune(n int) {
}
rc := ed.runeCount()
if ed.col >= rc {
ed.col = max(rc - 1, 0)
ed.col = max(rc-1, 0)
}
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
"unicode/utf8"
"tea.kareha.org/lab/levi/internal/console"
"tea.kareha.org/lab/termi"
)
type mode int
@@ -72,7 +72,7 @@ func Init(args []string) *Editor {
ed.load()
console.Raw()
termi.Raw()
return ed
}
@@ -88,10 +88,10 @@ func (ed *Editor) save() {
}
func (ed *Editor) Finish() {
console.Clear()
console.HomeCursor()
console.Cooked()
console.ShowCursor()
termi.Clear()
termi.HomeCursor()
termi.Cooked()
termi.ShowCursor()
ed.save()
}

View File

@@ -3,7 +3,7 @@ package editor
import (
"unicode/utf8"
"tea.kareha.org/lab/levi/internal/console"
"tea.kareha.org/lab/termi"
)
func (ed *Editor) exitInsert() {
@@ -53,12 +53,12 @@ func (ed *Editor) Main() {
for {
ed.repaint()
k, r := console.ReadKey()
key := termi.ReadKey()
switch ed.mode {
case modeCommand:
switch k {
case console.KeyNormal:
switch r {
switch key.Kind {
case termi.KeyRune:
switch key.Rune {
case 'q':
return
case 'i':
@@ -78,42 +78,42 @@ func (ed *Editor) Main() {
default:
ed.ring()
}
case console.KeyUp:
case termi.KeyUp:
ed.moveUp(1)
case console.KeyDown:
case termi.KeyDown:
ed.moveDown(1)
case console.KeyRight:
case termi.KeyRight:
ed.moveRight(1)
case console.KeyLeft:
case termi.KeyLeft:
ed.moveLeft(1)
default:
ed.ring()
}
case modeInsert:
switch k {
case console.KeyNormal:
switch r {
case console.RuneEscape:
switch key.Kind {
case termi.KeyRune:
switch key.Rune {
case termi.RuneEscape:
ed.exitInsert()
case console.RuneEnter:
case termi.RuneEnter:
ed.insertNewline()
case console.RuneBackspace:
case termi.RuneBackspace:
ed.deleteBefore()
case console.RuneDelete:
case termi.RuneDelete:
ed.deleteBefore()
default:
ed.insertRune(r)
ed.insertRune(key.Rune)
}
case console.KeyUp:
case termi.KeyUp:
ed.exitInsert()
ed.moveUp(1)
case console.KeyDown:
case termi.KeyDown:
ed.exitInsert()
ed.moveDown(1)
case console.KeyRight:
case termi.KeyRight:
ed.exitInsert()
ed.moveRight(1)
case console.KeyLeft:
case termi.KeyLeft:
ed.exitInsert()
ed.moveLeft(1)
default:

View File

@@ -3,18 +3,18 @@ package editor
import (
"unicode/utf8"
"tea.kareha.org/lab/levi/internal/console"
"tea.kareha.org/lab/termi"
)
func (ed *Editor) lineHeight(line string) int {
w, _ := console.Size()
w, _ := termi.Size()
rc := utf8.RuneCountInString(line)
width := console.StringWidth(line, rc)
width := termi.StringWidth(line, rc)
return 1 + max(width-1, 0)/w
}
func (ed *Editor) drawBuffer() {
_, h := console.Size()
_, h := termi.Size()
y := 0
for i := ed.vrow; i < len(ed.lines); i++ {
@@ -25,8 +25,8 @@ func (ed *Editor) drawBuffer() {
line = ed.lines[i]
}
console.MoveCursor(0, y)
console.Print(line)
termi.MoveCursor(0, y)
termi.Draw(line)
y += ed.lineHeight(line)
if y >= h-1 {
@@ -35,8 +35,8 @@ func (ed *Editor) drawBuffer() {
}
for ; y < h-1; y++ {
console.MoveCursor(0, y)
console.Print("~")
termi.MoveCursor(0, y)
termi.Draw("~")
}
}
@@ -49,20 +49,20 @@ func (ed *Editor) drawStatus() {
m = "i"
}
_, h := console.Size()
console.MoveCursor(0, h-1)
_, h := termi.Size()
termi.MoveCursor(0, h-1)
if ed.bell {
console.EnableInvert()
termi.EnableInvert()
}
console.Printf("%s %d,%d %s", m, ed.row, ed.col, ed.path)
termi.Printf("%s %d,%d %s", m, ed.row, ed.col, ed.path)
if ed.bell {
console.DisableInvert()
termi.DisableInvert()
}
ed.bell = false
}
func (ed *Editor) updateCursor() {
w, h := console.Size()
w, h := termi.Size()
var dy int
switch ed.mode {
@@ -72,12 +72,12 @@ func (ed *Editor) updateCursor() {
ed.col = min(ed.col, max(len-1, 0))
// XXX approximation
width := console.StringWidth(ed.lines[ed.row], ed.col)
width := termi.StringWidth(ed.lines[ed.row], ed.col)
ed.x = width % w
dy = width / w
case modeInsert:
// XXX approximation
width := console.StringWidth(ed.head+ed.insert.String(), ed.col)
width := termi.StringWidth(ed.head+ed.insert.String(), ed.col)
ed.x = width % w
dy = width / w
}
@@ -104,17 +104,17 @@ func (ed *Editor) updateCursor() {
}
func (ed *Editor) repaint() {
console.HideCursor()
termi.HideCursor()
console.Clear()
console.HomeCursor()
termi.Clear()
termi.HomeCursor()
ed.updateCursor()
ed.drawBuffer()
ed.drawStatus()
console.MoveCursor(ed.x, ed.y)
termi.MoveCursor(ed.x, ed.y)
console.ShowCursor()
termi.ShowCursor()
}