Files
YunShu/pkg/termui/input.go
titor d2b9b2c4bb refactor: 项目结构重组,src/ 扁平化为根目录,提取 pkg/ 子包
- 模块名重命名 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*
2026-05-09 03:55:56 +08:00

190 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}