- 模块名重命名 yunshu -> hub.gaomia.site/titor/YunShu - Go 版本升级 1.21 -> 1.25 - src/ 目录删除,所有文件移至根目录 - 新增 pkg/mdprint/: Markdown AST 解析+ANSI 渲染 - 新增 pkg/style/: 终端颜色样式(8色 ANSI + 24位真彩色) - 新增 pkg/termui/: 终端输入组件(交互式输入/密码/确认) - 更新文档:AGENTS.md、architecture.md、changelog.md、taolun.md - gitignore 通配符修复 yunshu.exe -> yunshu.exe*
190 lines
3.5 KiB
Go
190 lines
3.5 KiB
Go
package termui
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"syscall"
|
||
"unsafe"
|
||
|
||
"hub.gaomia.site/titor/YunShu/pkg/style"
|
||
)
|
||
|
||
const (
|
||
stdInputHandle = ^uintptr(9)
|
||
stdOutputHandle = ^uintptr(10)
|
||
enableProcessed = 0x0001
|
||
enableLineInput = 0x0002
|
||
enableEchoInput = 0x0004
|
||
enableVtProcessing = 0x0004
|
||
)
|
||
|
||
var (
|
||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||
procGetStdHandle = kernel32.NewProc("GetStdHandle")
|
||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||
)
|
||
|
||
func ensureLineMode() {
|
||
h, _, _ := procGetStdHandle.Call(stdInputHandle)
|
||
if h == 0 || h == uintptr(syscall.InvalidHandle) {
|
||
return
|
||
}
|
||
var mode uint32
|
||
ret, _, _ := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode)))
|
||
if ret == 0 {
|
||
return
|
||
}
|
||
need := uint32(enableProcessed | enableLineInput | enableEchoInput)
|
||
if mode&need != need {
|
||
procSetConsoleMode.Call(h, uintptr(need))
|
||
}
|
||
}
|
||
|
||
type InputConfig struct {
|
||
Label string
|
||
Help string
|
||
Default string
|
||
Required bool
|
||
Validator Validator
|
||
}
|
||
|
||
type InputOption func(*InputConfig)
|
||
|
||
func WithDefault(v string) InputOption {
|
||
return func(c *InputConfig) { c.Default = v }
|
||
}
|
||
|
||
func WithHelp(v string) InputOption {
|
||
return func(c *InputConfig) { c.Help = v }
|
||
}
|
||
|
||
func WithRequired(v bool) InputOption {
|
||
return func(c *InputConfig) { c.Required = v }
|
||
}
|
||
|
||
func WithValidator(v Validator) InputOption {
|
||
return func(c *InputConfig) { c.Validator = v }
|
||
}
|
||
|
||
func applyOpts(opts []InputOption) InputConfig {
|
||
c := InputConfig{}
|
||
for _, o := range opts {
|
||
o(&c)
|
||
}
|
||
return c
|
||
}
|
||
|
||
func printLabel(label string, required bool) {
|
||
s := style.New().Bold().Render(label)
|
||
if required {
|
||
s += "(必填)"
|
||
}
|
||
fmt.Println(s)
|
||
}
|
||
|
||
func ReadLine() string {
|
||
ensureLineMode()
|
||
r := bufio.NewReader(os.Stdin)
|
||
s, err := r.ReadString('\n')
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return strings.TrimRight(s, "\r\n")
|
||
}
|
||
|
||
func TextInput(label string, opts ...InputOption) string {
|
||
cfg := applyOpts(opts)
|
||
printLabel(label, cfg.Required)
|
||
|
||
for {
|
||
fmt.Print(style.Cyan.Render("? "))
|
||
|
||
line := ReadLine()
|
||
|
||
if line == "" {
|
||
line = cfg.Default
|
||
}
|
||
|
||
var err error
|
||
if cfg.Required && line == "" {
|
||
err = fmt.Errorf("不能为空")
|
||
} else if cfg.Validator != nil {
|
||
err = cfg.Validator(line)
|
||
}
|
||
|
||
if err != nil {
|
||
fmt.Println(style.Red.Render("⚠ " + err.Error()))
|
||
continue
|
||
}
|
||
|
||
fmt.Println(style.Green.Render("✔ " + line))
|
||
return line
|
||
}
|
||
}
|
||
|
||
func PasswordInput(label string, opts ...InputOption) string {
|
||
cfg := applyOpts(opts)
|
||
printLabel(label, cfg.Required)
|
||
|
||
for {
|
||
fmt.Print(style.Cyan.Render("? "))
|
||
|
||
line := ReadLine()
|
||
|
||
fmt.Print("\033[A\r\033[K")
|
||
|
||
if line == "" {
|
||
line = cfg.Default
|
||
}
|
||
|
||
var err error
|
||
if cfg.Required && line == "" {
|
||
err = fmt.Errorf("不能为空")
|
||
} else if cfg.Validator != nil {
|
||
err = cfg.Validator(line)
|
||
}
|
||
|
||
if err != nil {
|
||
fmt.Println(style.Red.Render("⚠ " + err.Error()))
|
||
continue
|
||
}
|
||
|
||
masked := strings.Repeat("*", len(line))
|
||
fmt.Println(style.Green.Render("✔ " + masked))
|
||
return line
|
||
}
|
||
}
|
||
|
||
func Confirm(label string, defaultValue bool) bool {
|
||
hint := "Y/n"
|
||
if !defaultValue {
|
||
hint = "y/N"
|
||
}
|
||
|
||
fmt.Print(style.Cyan.Render("?"), label, "(", hint, ")")
|
||
|
||
line := ReadLine()
|
||
line = strings.ToLower(line)
|
||
|
||
fmt.Print("\033[A\r\033[K")
|
||
|
||
ok := defaultValue
|
||
if line == "y" || line == "yes" {
|
||
ok = true
|
||
} else if line == "n" || line == "no" {
|
||
ok = false
|
||
} else if line != "" {
|
||
ok = false
|
||
}
|
||
|
||
if ok {
|
||
fmt.Println(style.Green.Render("✔ " + label))
|
||
} else {
|
||
fmt.Println(style.Red.Render("✘ " + label))
|
||
}
|
||
return ok
|
||
}
|