Files
SerialTerminalForWindowsTer…/internal/flag/flag.go
T
JiXieShi 65c1a48f10 refactor: simplify flag system and extract to internal/flag
Replace complex ptrVal/Val/Flag type machinery with direct pflag
calls. Move flag logic and interactive wizard to internal/flag
package. Eliminate ~200 lines of flag boilerplate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:52:40 +08:00

261 lines
9.0 KiB
Go

// Package flag provides CLI flag parsing and interactive configuration.
package flag
import (
"fmt"
"log"
"os"
"sort"
"strconv"
"strings"
"github.com/charmbracelet/bubbles/key"
inf "github.com/fzdwx/infinite"
"github.com/fzdwx/infinite/color"
"github.com/fzdwx/infinite/components"
"github.com/fzdwx/infinite/components/input/text"
"github.com/fzdwx/infinite/components/selection/confirm"
"github.com/fzdwx/infinite/components/selection/singleselect"
"github.com/fzdwx/infinite/style"
"github.com/fzdwx/infinite/theme"
"github.com/spf13/pflag"
"go.bug.st/serial"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config"
)
// Init registers all CLI flags with pflag, binding them to the given config.
func Init(cfg *config.Config) {
pflag.StringVarP(&cfg.PortName, "port", "p", "", "serial port (/dev/ttyUSB0, COMx)")
pflag.IntVarP(&cfg.BaudRate, "baud", "b", 115200, "baud rate")
pflag.IntVarP(&cfg.DataBits, "data", "d", 8, "data bits")
pflag.IntVarP(&cfg.StopBits, "stop", "s", 0, "stop bits (0:1, 1:1.5, 2:2)")
pflag.StringVarP(&cfg.OutputCode, "out", "o", "UTF-8", "output charset")
pflag.StringVarP(&cfg.InputCode, "in", "i", "UTF-8", "input charset")
pflag.StringVarP(&cfg.EndStr, "end", "e", "\n", "line ending")
pflag.IntVarP(&cfg.FrameSize, "Frame", "F", 16, "hex frame size")
pflag.IntVarP(&cfg.ParityBit, "verify", "v", 0, "parity (0:none,1:odd,2:even,3:mark,4:space)")
pflag.BoolVarP(&cfg.EnableGUI, "gui", "g", false, "enable TUI mode")
pflag.StringVarP(&cfg.HotkeyMod, "hotkey-mod", "k", "ctrl+alt", "hotkey modifier (ctrl+alt|ctrl+shift)")
pflag.IntSliceVarP(&cfg.ForWard, "forward", "f", nil, "forward mode (0:none,1:TCP,2:UDP)")
pflag.StringArrayVarP(&cfg.Address, "address", "a", nil, "forward address")
pflag.StringVarP(&cfg.LogFilePath, "log", "l", "", "log file path")
_ = pflag.Lookup("log") // mark for NoOptDefVal
pflag.StringVarP(&cfg.TimesFmt, "time", "t", "", "timestamp format")
_ = pflag.Lookup("time") // mark for NoOptDefVal
}
// Normalize converts single-dash long flags (e.g. -port) to double-dash (--port).
func Normalize() {
known := map[string]bool{
"port": true, "baud": true, "data": true, "stop": true,
"out": true, "in": true, "end": true, "Frame": true,
"verify": true, "gui": true, "hotkey-mod": true,
"forward": true, "address": true, "log": true, "time": true,
}
for i, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") {
name := strings.TrimPrefix(arg, "-")
if known[name] {
os.Args[i+1] = "--" + name
}
}
}
}
// Ext applies post-parse normalization to config values.
func Ext(cfg *config.Config) {
if cfg.LogFilePath != "" {
cfg.EnableLog = true
}
if cfg.TimesFmt != "" {
cfg.TimesTamp = true
}
if cfg.HotkeyMod == "" {
cfg.HotkeyMod = "ctrl+alt"
}
cfg.HotkeyMod = strings.ToLower(strings.TrimSpace(cfg.HotkeyMod))
if cfg.HotkeyMod != "ctrl+alt" && cfg.HotkeyMod != "ctrl+shift" {
cfg.HotkeyMod = "ctrl+alt"
}
}
// PrintUsage displays flag help and available ports.
func PrintUsage(ports []string) {
type flagInfo struct{ short, long, typ, help, def string }
flags := []flagInfo{
{"-p", "--port", "string", "serial port", ""},
{"-b", "--baud", "int", "baud rate", "115200"},
{"-d", "--data", "int", "data bits", "8"},
{"-s", "--stop", "int", "stop bits", "0"},
{"-o", "--out", "string", "output charset", "UTF-8"},
{"-i", "--in", "string", "input charset", "UTF-8"},
{"-e", "--end", "string", "line ending", "\\n"},
{"-F", "--Frame", "int", "hex frame size", "16"},
{"-v", "--verify", "int", "parity", "0"},
{"-g", "--gui", "bool", "enable TUI", "false"},
{"-k", "--hotkey-mod", "string", "hotkey modifier", "ctrl+alt"},
{"-f", "--forward", "[]int", "forward mode", "0"},
{"-a", "--address", "[]string", "forward address", "127.0.0.1:12345"},
{"-l", "--log", "string", "log path", "./%s-$s.txt"},
{"-t", "--time", "string", "timestamp format", "[06-01-02 15:04:05.000]"},
}
sort.Slice(flags, func(i, j int) bool { return flags[i].long < flags[j].long })
fmt.Printf("\nFlags:\n")
fmt.Printf(" %-6s %-14s %-8s %-44s %s\n", "Short", "Long", "Type", "Help", "Default")
fmt.Printf(" %-6s %-14s %-8s %-44s %s\n", "------", "------", "------", "------", "------")
for _, f := range flags {
fmt.Printf(" %-6s %-14s %-8s %-44s %q\n", f.short, f.long, f.typ, f.help, f.def)
}
fmt.Printf("\nAvailable ports: %v\n", strings.Join(ports, ", "))
}
var (
bauds = []string{"Custom", "300", "600", "1200", "2400", "4800", "9600",
"14400", "19200", "38400", "56000", "57600", "115200", "128000",
"256000", "460800", "512000", "750000", "921600", "1500000"}
datas = []string{"5", "6", "7", "8"}
stops = []string{"1", "1.5", "2"}
paritys = []string{"None", "Odd", "Even", "Mark", "Space"}
forwards = []string{"No", "TCP-C", "UDP-C"}
)
// GetCliFlag runs an interactive configuration wizard when no port is specified.
func GetCliFlag(cfg *config.Config) {
ports, err := serial.GetPortsList()
if err != nil {
log.Fatal(err)
}
inputs := components.NewInput()
inputs.Prompt = "Filtering: "
inputs.PromptStyle = style.New().Bold().Italic().Fg(color.LightBlue)
selectKeymap := singleselect.DefaultSingleKeyMap()
selectKeymap.Confirm = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "finish select"))
selectKeymap.Choice = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "finish select"))
selectKeymap.NextPage = key.NewBinding(key.WithKeys("right"), key.WithHelp("->", "next page"))
selectKeymap.PrevPage = key.NewBinding(key.WithKeys("left"), key.WithHelp("<-", "prev page"))
s, _ := inf.NewSingleSelect(ports,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("Select serial port")
cfg.PortName = ports[s]
s, _ = inf.NewSingleSelect(bauds,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
).Display("Select baud rate")
if s != 0 {
cfg.BaudRate, _ = strconv.Atoi(bauds[s])
} else {
b, _ := inf.NewText(
text.WithPrompt("BaudRate:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("115200"),
).Display()
cfg.BaudRate, _ = strconv.Atoi(b)
}
v, _ := inf.NewConfirmWithSelection(confirm.WithPrompt("Enable Hex")).Display()
if v {
cfg.InputCode = "hex"
b, _ := inf.NewText(
text.WithPrompt("Frames:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("16"),
).Display()
cfg.FrameSize, _ = strconv.Atoi(b)
}
v, _ = inf.NewConfirmWithSelection(confirm.WithPrompt("Enable Timestamp")).Display()
cfg.TimesTamp = v
if v {
b, _ := inf.NewText(
text.WithPrompt("Format:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("[06-01-02 15:04:05.000]"),
).Display()
cfg.TimesFmt = b
}
v, _ = inf.NewConfirmWithSelection(confirm.WithPrompt("Enable advanced config")).Display()
if v {
s, _ = inf.NewSingleSelect(datas,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("Select data bits")
cfg.DataBits, _ = strconv.Atoi(datas[s])
s, _ = inf.NewSingleSelect(stops,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("Select stop bits")
cfg.StopBits = s
s, _ = inf.NewSingleSelect(paritys,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("Select parity")
cfg.ParityBit = s
t, _ := inf.NewText(
text.WithPrompt("Line ending:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("\n"),
).Display()
cfg.EndStr = t
v, _ = inf.NewConfirmWithSelection(confirm.WithDefaultYes(), confirm.WithPrompt("Enable charset conversion")).Display()
if v {
t, _ = inf.NewText(
text.WithPrompt("Input charset:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("UTF-8"),
).Display()
cfg.InputCode = t
t, _ = inf.NewText(
text.WithPrompt("Output charset:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("UTF-8"),
).Display()
cfg.OutputCode = t
}
G_F_mode:
s, _ = inf.NewSingleSelect(forwards,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(3),
singleselect.WithFilterInput(inputs),
).Display("Select forward mode")
if s != 0 {
cfg.ForWard = append(cfg.ForWard, s)
t, _ = inf.NewText(
text.WithPrompt("Address:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("127.0.0.1:12345"),
).Display()
cfg.Address = append(cfg.Address, t)
goto G_F_mode
}
e, _ := inf.NewConfirmWithSelection(confirm.WithDefaultYes(), confirm.WithPrompt("Enable logging")).Display()
cfg.EnableLog = e
if e {
t, _ = inf.NewText(
text.WithPrompt("Path:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("./%s-$s.txt"),
).Display()
cfg.LogFilePath = t
}
}
}