This commit is contained in:
2026-03-26 11:50:27 +09:00
parent d9d75b9305
commit 24cdd59e12
6 changed files with 124 additions and 155 deletions

View File

@@ -2,9 +2,7 @@ package console
import (
"fmt"
"io"
"os"
"unicode/utf8"
"golang.org/x/term"
)
@@ -25,7 +23,7 @@ func Raw() {
func Cooked() {
if state == nil {
panic("state is nil")
panic("invalid state")
}
term.Restore(int(os.Stdin.Fd()), state)
}
@@ -53,47 +51,7 @@ func ShowCursor() {
func Size() (int, int) {
w, h, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
panic(err)
return 80, 24
}
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
}

95
internal/console/input.go Normal file
View File

@@ -0,0 +1,95 @@
package console
import (
"io"
"os"
"unicode/utf8"
)
type Key int
const (
KeyNormal = 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

@@ -0,0 +1,92 @@
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
}