From 31dd9da49081996828e6cf195d6ddd1725e33449 Mon Sep 17 00:00:00 2001 From: JiXieShi Date: Sat, 23 May 2026 21:45:08 +0800 Subject: [PATCH] refactor: extract internal/config and eliminate global config var Move Config struct to internal/config with exported fields. Replace global var config with package-level cfg pointer. Add OpenLogFile to config package. Add type alias Config = appconfig.Config in main package for backward compatibility. Co-Authored-By: Claude Opus 4.7 --- app.go | 27 ++++++------- app_test.go | 19 ++++----- command.go | 26 ++++++------- command_test.go | 24 ++++++------ config.go | 40 ++----------------- config_test.go | 61 +++++++++++++++-------------- escape_test.go | 10 ++--- flag.go | 82 +++++++++++++++++++-------------------- internal/config/config.go | 42 ++++++++++++++++++++ main.go | 16 ++++---- tui_hotkeys.go | 4 +- tui_model.go | 8 ++-- tui_panels.go | 4 +- tui_test.go | 14 +++---- utils.go | 10 ++--- 15 files changed, 198 insertions(+), 189 deletions(-) create mode 100644 internal/config/config.go diff --git a/app.go b/app.go index 1bfc6d2..9394f50 100644 --- a/app.go +++ b/app.go @@ -11,6 +11,7 @@ import ( "sync/atomic" "time" + appconfig "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" "github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/charset" "github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward" @@ -35,7 +36,7 @@ type App struct { } func NewApp(cfg *Config) (*App, error) { - f, err := openLogFile() + f, err := appconfig.OpenLogFile(cfg) if err != nil { return nil, err } @@ -168,16 +169,16 @@ func (a *App) waitDone() <-chan struct{} { } func (a *App) loadConfiguredForwards() { - for i, mode := range config.forWard { + for i, mode := range a.cfg.ForWard { m := forward.Mode(mode) if m == forward.None { continue } - if i >= len(config.address) { + if i >= len(a.cfg.Address) { a.Notifyf("[forward] skip #%d: missing address", i) continue } - addr := strings.TrimSpace(config.address[i]) + addr := strings.TrimSpace(a.cfg.Address[i]) if addr == "" { continue } @@ -192,12 +193,12 @@ func (a *App) reportForwardIngress(id int, chunk []byte) { return } - if strings.EqualFold(a.cfg.inputCode, "hex") { + if strings.EqualFold(a.cfg.InputCode, "hex") { a.Notifyf("[forward#%d -> serial] % X\n", id, chunk) return } - converted, err := charset.ConvertChunk(chunk, a.cfg.inputCode, a.cfg.outputCode) + converted, err := charset.ConvertChunk(chunk, a.cfg.InputCode, a.cfg.OutputCode) if err != nil { converted = bytes.Clone(chunk) } @@ -236,7 +237,7 @@ func (a *App) sendLine(line string) error { return nil } - payload := append([]byte(line), []byte(a.cfg.endStr)...) + payload := append([]byte(line), []byte(a.cfg.EndStr)...) return a.writeToSession(payload) } @@ -283,7 +284,7 @@ func (a *App) handleLine(line string) { } func (a *App) startOutputLoop() { - if strings.EqualFold(a.cfg.inputCode, "hex") { + if strings.EqualFold(a.cfg.InputCode, "hex") { go a.readHexOutput() return } @@ -292,7 +293,7 @@ func (a *App) startOutputLoop() { } func (a *App) readHexOutput() { - frameSize := a.cfg.frameSize + frameSize := a.cfg.FrameSize if frameSize <= 0 { frameSize = 16 } @@ -312,7 +313,7 @@ func (a *App) readHexOutput() { if len(outChunk) == 0 { continue } - a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: charset.FormatHexFrame(outChunk, a.cfg.timesTamp, a.cfg.timesFmt)}) + a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: charset.FormatHexFrame(outChunk, a.cfg.TimesTamp, a.cfg.TimesFmt)}) } if err != nil { if err != io.EOF { @@ -347,15 +348,15 @@ func (a *App) readTextOutput() { continue } - converted, convErr := charset.ConvertChunk(outChunk, a.cfg.inputCode, a.cfg.outputCode) + converted, convErr := charset.ConvertChunk(outChunk, a.cfg.InputCode, a.cfg.OutputCode) if convErr != nil { a.Notifyf("[output] convert failed: %v", convErr) converted = bytes.Clone(outChunk) } text := string(converted) - if a.cfg.timesTamp { - text = prefixLines(text, time.Now().Format(a.cfg.timesFmt)+" ") + if a.cfg.TimesTamp { + text = prefixLines(text, time.Now().Format(a.cfg.TimesFmt)+" ") } a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: text}) } diff --git a/app_test.go b/app_test.go index 2313717..6af906c 100644 --- a/app_test.go +++ b/app_test.go @@ -61,7 +61,7 @@ func TestAppUIEvents(t *testing.T) { func TestSendLine(t *testing.T) { setupTestPipes() a := &App{ - cfg: &Config{endStr: "\r\n"}, + cfg: &Config{EndStr: "\r\n"}, plugins: luaplugin.NewManager(), uiEvents: make(chan event.UIEvent, 8), done: make(chan struct{}), @@ -83,7 +83,7 @@ func TestSendLine(t *testing.T) { func TestHandleLine(t *testing.T) { setupTestPipes() a := &App{ - cfg: &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"}, + cfg: &Config{EndStr: "\n", InputCode: "UTF-8", OutputCode: "UTF-8"}, plugins: luaplugin.NewManager(), uiEvents: make(chan event.UIEvent, 8), done: make(chan struct{}), @@ -159,22 +159,19 @@ func TestAppClose(t *testing.T) { } func TestLoadConfiguredForwards(t *testing.T) { - oldCfg := config - defer func() { config = oldCfg }() - listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen failed: %v", err) } defer listener.Close() - config = Config{ - forWard: []int{int(forward.TCP), int(forward.None), int(forward.UDP)}, - address: []string{listener.Addr().String(), "", ""}, + testCfg := &Config{ + ForWard: []int{int(forward.TCP), int(forward.None), int(forward.UDP)}, + Address: []string{listener.Addr().String(), "", ""}, } a := &App{ - cfg: &config, + cfg: testCfg, forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}), uiEvents: make(chan event.UIEvent, 8), done: make(chan struct{}), @@ -191,7 +188,7 @@ func TestLoadConfiguredForwards(t *testing.T) { func TestReportForwardIngress(t *testing.T) { a := &App{ - cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8"}, + cfg: &Config{InputCode: "UTF-8", OutputCode: "UTF-8"}, uiEvents: make(chan event.UIEvent, 4), } a.SetUIEnabled(true) @@ -199,7 +196,7 @@ func TestReportForwardIngress(t *testing.T) { a.reportForwardIngress(1, []byte("test")) // Hex mode - a.cfg.inputCode = "hex" + a.cfg.InputCode = "hex" a.reportForwardIngress(2, []byte{0x41, 0x42}) // Empty chunk diff --git a/command.go b/command.go index 65d0d73..d8c92aa 100644 --- a/command.go +++ b/command.go @@ -414,13 +414,13 @@ func (d *CommandDispatcher) handleModeCommand(args []string) error { } d.app.Notifyf("[mode] input=%s output=%s end=%q hex=%v frame=%d timestamp=%v timefmt=%q forwardTargets=%d plugins=%d", - d.app.cfg.inputCode, - d.app.cfg.outputCode, - d.app.cfg.endStr, - strings.EqualFold(d.app.cfg.inputCode, "hex"), - d.app.cfg.frameSize, - d.app.cfg.timesTamp, - d.app.cfg.timesFmt, + d.app.cfg.InputCode, + d.app.cfg.OutputCode, + d.app.cfg.EndStr, + strings.EqualFold(d.app.cfg.InputCode, "hex"), + d.app.cfg.FrameSize, + d.app.cfg.TimesTamp, + d.app.cfg.TimesFmt, len(d.app.forward.List()), len(d.app.plugins.List()), ) @@ -439,25 +439,25 @@ func (d *CommandDispatcher) handleModeCommand(args []string) error { switch field { case "in": - d.app.cfg.inputCode = value + d.app.cfg.InputCode = value case "out": - d.app.cfg.outputCode = value + d.app.cfg.OutputCode = value case "end": - d.app.cfg.endStr = value + d.app.cfg.EndStr = value case "frame": n, err := strconv.Atoi(value) if err != nil || n <= 0 { return fmt.Errorf("frame must be a positive integer") } - d.app.cfg.frameSize = n + d.app.cfg.FrameSize = n case "timestamp": enabled, ok := parseOnOff(value) if !ok { return fmt.Errorf("timestamp value must be on/off") } - d.app.cfg.timesTamp = enabled + d.app.cfg.TimesTamp = enabled case "timefmt": - d.app.cfg.timesFmt = value + d.app.cfg.TimesFmt = value default: return fmt.Errorf("unknown mode field: %s", field) } diff --git a/command_test.go b/command_test.go index f65cfef..ab8b285 100644 --- a/command_test.go +++ b/command_test.go @@ -26,7 +26,7 @@ func setupTestPipes() { func newTestAppForCommand() *App { a := &App{ - cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"}, + cfg: &Config{InputCode: "UTF-8", OutputCode: "UTF-8", EndStr: "\n"}, plugins: luaplugin.NewManager(), uiEvents: make(chan event.UIEvent, 32), done: make(chan struct{}), @@ -149,15 +149,15 @@ func TestCommandExecuteModeSet(t *testing.T) { if err != nil || !handled { t.Fatalf(".mode set end failed handled=%v err=%v", handled, err) } - if a.cfg.endStr != "\\r\\n" { - t.Fatalf("mode set end not applied, got=%q", a.cfg.endStr) + if a.cfg.EndStr != "\\r\\n" { + t.Fatalf("mode set end not applied, got=%q", a.cfg.EndStr) } handled, err = a.dispatcher.Execute(".mode set timestamp on") if err != nil || !handled { t.Fatalf(".mode set timestamp failed handled=%v err=%v", handled, err) } - if !a.cfg.timesTamp { + if !a.cfg.TimesTamp { t.Fatalf("mode set timestamp should enable timesTamp") } } @@ -298,32 +298,32 @@ func TestCommandExecuteModeSetAll(t *testing.T) { if err != nil || !handled { t.Fatalf(".mode set frame failed: handled=%v err=%v", handled, err) } - if a.cfg.frameSize != 32 { - t.Fatalf("frameSize not set, got=%d", a.cfg.frameSize) + if a.cfg.FrameSize != 32 { + t.Fatalf("frameSize not set, got=%d", a.cfg.FrameSize) } handled, err = a.dispatcher.Execute(".mode set timefmt 2006") if err != nil || !handled { t.Fatalf(".mode set timefmt failed: handled=%v err=%v", handled, err) } - if a.cfg.timesFmt != "2006" { - t.Fatalf("timesFmt not set, got=%q", a.cfg.timesFmt) + if a.cfg.TimesFmt != "2006" { + t.Fatalf("timesFmt not set, got=%q", a.cfg.TimesFmt) } handled, err = a.dispatcher.Execute(".mode set out GBK") if err != nil || !handled { t.Fatalf(".mode set out failed: handled=%v err=%v", handled, err) } - if a.cfg.outputCode != "GBK" { - t.Fatalf("outputCode not set, got=%q", a.cfg.outputCode) + if a.cfg.OutputCode != "GBK" { + t.Fatalf("outputCode not set, got=%q", a.cfg.OutputCode) } handled, err = a.dispatcher.Execute(".mode set in GBK") if err != nil || !handled { t.Fatalf(".mode set in failed: handled=%v err=%v", handled, err) } - if a.cfg.inputCode != "GBK" { - t.Fatalf("inputCode not set, got=%q", a.cfg.inputCode) + if a.cfg.InputCode != "GBK" { + t.Fatalf("inputCode not set, got=%q", a.cfg.InputCode) } } diff --git a/config.go b/config.go index 4b27500..d671981 100644 --- a/config.go +++ b/config.go @@ -1,42 +1,10 @@ package main import ( - "fmt" - "os" - "time" + appconfig "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config" ) -type Config struct { - portName string - baudRate int - dataBits int - stopBits int - parityBit int - outputCode string - inputCode string - endStr string - enableLog bool - logFilePath string - forWard []int - frameSize int - timesTamp bool - timesFmt string - address []string - enableGUI bool - hotkeyMod string -} +// Config is an alias for appconfig.Config to keep main-package code concise. +type Config = appconfig.Config -var config Config - -func openLogFile() (*os.File, error) { - if config.enableLog { - path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405")) - f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) - if err != nil { - return nil, err - } - return f, nil - } - - return nil, nil -} +var cfg = &Config{} diff --git a/config_test.go b/config_test.go index 89e5dc9..c88bcc9 100644 --- a/config_test.go +++ b/config_test.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/pflag" + appconfig "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config" "github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward" ) @@ -55,16 +56,16 @@ func TestParseForwardMode(t *testing.T) { } func TestOpenLogFile(t *testing.T) { - old := config - defer func() { config = old }() + old := *cfg + defer func() { *cfg = old }() - config = Config{ - enableLog: true, - portName: "COM1", - logFilePath: filepath.Join(t.TempDir(), "%s-%s.log"), + *cfg = Config{ + EnableLog: true, + PortName: "COM1", + LogFilePath: filepath.Join(t.TempDir(), "%s-%s.log"), } - f, err := openLogFile() + f, err := appconfig.OpenLogFile(cfg) if err != nil { t.Fatalf("openLogFile() unexpected error: %v", err) } @@ -73,8 +74,8 @@ func TestOpenLogFile(t *testing.T) { } _ = f.Close() - config.enableLog = false - f, err = openLogFile() + cfg.EnableLog = false + f, err = appconfig.OpenLogFile(cfg) if err != nil { t.Fatalf("openLogFile() unexpected error with enableLog=false: %v", err) } @@ -114,55 +115,55 @@ func TestFlagFindValue(t *testing.T) { } func TestFlagExt(t *testing.T) { - old := config - defer func() { config = old }() + old := *cfg + defer func() { *cfg = old }() - config = Config{} + *cfg = Config{} flagExt() - if config.enableLog { + if cfg.EnableLog { t.Fatalf("expected enableLog=false when logFilePath empty") } - if config.timesTamp { + if cfg.TimesTamp { t.Fatalf("expected timesTamp=false when timesFmt empty") } - if config.hotkeyMod != "ctrl+alt" { - t.Fatalf("expected default hotkeyMod=ctrl+alt, got=%q", config.hotkeyMod) + if cfg.HotkeyMod != "ctrl+alt" { + t.Fatalf("expected default hotkeyMod=ctrl+alt, got=%q", cfg.HotkeyMod) } - config = Config{logFilePath: "/tmp/log.txt"} + *cfg = Config{LogFilePath: "/tmp/log.txt"} flagExt() - if !config.enableLog { + if !cfg.EnableLog { t.Fatalf("expected enableLog=true when logFilePath set") } - config = Config{timesFmt: "2006-01-02"} + *cfg = Config{TimesFmt: "2006-01-02"} flagExt() - if !config.timesTamp { + if !cfg.TimesTamp { t.Fatalf("expected timesTamp=true when timesFmt set") } - config = Config{hotkeyMod: ""} + *cfg = Config{HotkeyMod: ""} flagExt() - if config.hotkeyMod != "ctrl+alt" { + if cfg.HotkeyMod != "ctrl+alt" { t.Fatalf("empty hotkeyMod should default to ctrl+alt") } - config = Config{hotkeyMod: "ctrl+shift"} + *cfg = Config{HotkeyMod: "ctrl+shift"} flagExt() - if config.hotkeyMod != "ctrl+shift" { + if cfg.HotkeyMod != "ctrl+shift" { t.Fatalf("expected ctrl+shift preserved") } - config = Config{hotkeyMod: " CTRL+SHIFT "} + *cfg = Config{HotkeyMod: " CTRL+SHIFT "} flagExt() - if config.hotkeyMod != "ctrl+shift" { - t.Fatalf("expected whitespace+case normalization, got=%q", config.hotkeyMod) + if cfg.HotkeyMod != "ctrl+shift" { + t.Fatalf("expected whitespace+case normalization, got=%q", cfg.HotkeyMod) } - config = Config{hotkeyMod: "invalid"} + *cfg = Config{HotkeyMod: "invalid"} flagExt() - if config.hotkeyMod != "ctrl+alt" { - t.Fatalf("invalid hotkeyMod should default to ctrl+alt, got=%q", config.hotkeyMod) + if cfg.HotkeyMod != "ctrl+alt" { + t.Fatalf("invalid hotkeyMod should default to ctrl+alt, got=%q", cfg.HotkeyMod) } } diff --git a/escape_test.go b/escape_test.go index fe13197..f15f09f 100644 --- a/escape_test.go +++ b/escape_test.go @@ -60,10 +60,10 @@ func TestParseCSIu(t *testing.T) { } func TestIsExitHotkeySeq(t *testing.T) { - oldCfg := config - defer func() { config = oldCfg }() + oldCfg := *cfg + defer func() { *cfg = oldCfg }() - config = Config{hotkeyMod: "ctrl+alt"} + *cfg = Config{HotkeyMod: "ctrl+alt"} // CSI u Ctrl+Alt+C (mod=6) if !isExitHotkeySeq([]byte{0x1b, '[', '9', '9', ';', '6', 'u'}) { @@ -88,7 +88,7 @@ func TestIsExitHotkeySeq(t *testing.T) { } // Switch to ctrl+shift - config = Config{hotkeyMod: "ctrl+shift"} + *cfg = Config{HotkeyMod: "ctrl+shift"} if !isExitHotkeySeq([]byte{0x1b, '[', '9', '9', ';', '5', 'u'}) { t.Fatalf("Ctrl+Shift+C should exit with ctrl+shift config") @@ -111,7 +111,7 @@ func TestIsExitHotkeySeq(t *testing.T) { t.Fatalf("plain bytes should not exit") } - config = Config{hotkeyMod: "ctrl+alt"} + *cfg = Config{HotkeyMod: "ctrl+alt"} // Ctrl only (mod=4) should not exit (requires Alt too) if isExitHotkeySeq([]byte{0x1b, '[', '9', '9', ';', '4', 'u'}) { t.Fatalf("Ctrl+C (without Alt) should not exit") diff --git a/flag.go b/flag.go index 6ddf4e5..9253db8 100644 --- a/flag.go +++ b/flag.go @@ -47,21 +47,21 @@ type Flag struct { } var ( - portName = Flag{ptrVal{string: &config.portName}, "p", "port", Val{string: ""}, "要连接的串口\t(/dev/ttyUSB0、COMx)"} - baudRate = Flag{ptrVal{int: &config.baudRate}, "b", "baud", Val{int: 115200}, "波特率"} - dataBits = Flag{ptrVal{int: &config.dataBits}, "d", "data", Val{int: 8}, "数据位"} - stopBits = Flag{ptrVal{int: &config.stopBits}, "s", "stop", Val{int: 0}, "停止位停止位(0: 1停止 1:1.5停止 2:2停止)"} - outputCode = Flag{ptrVal{string: &config.outputCode}, "o", "out", Val{string: "UTF-8"}, "输出编码"} - inputCode = Flag{ptrVal{string: &config.inputCode}, "i", "in", Val{string: "UTF-8"}, "输入编码"} - endStr = Flag{ptrVal{string: &config.endStr}, "e", "end", Val{string: "\n"}, "终端换行符"} - logExt = Flag{v: ptrVal{ext: &config.logFilePath}, sStr: "l", lStr: "log", dv: Val{extdef: "./%s-$s.txt", string: ""}, help: "日志保存路径"} - timeExt = Flag{v: ptrVal{ext: &config.timesFmt}, sStr: "t", lStr: "time", dv: Val{extdef: "[06-01-02 15:04:05.000]", string: ""}, help: "时间戳格式化字段"} - forWard = Flag{ptrVal{il: &config.forWard}, "f", "forward", Val{int: 0}, "转发模式(0: 无 1:TCP-C 2:UDP-C 支持多次传入)"} - address = Flag{ptrVal{sl: &config.address}, "a", "address", Val{string: "127.0.0.1:12345"}, "转发服务地址(支持多次传入)"} - frameSize = Flag{ptrVal{int: &config.frameSize}, "F", "Frame", Val{int: 16}, "帧大小"} - parityBit = Flag{ptrVal{int: &config.parityBit}, "v", "verify", Val{int: 0}, "奇偶校验(0:无校验、1:奇校验、2:偶校验、3:1校验、4:0校验)"} - guiMode = Flag{ptrVal{bool: &config.enableGUI}, "g", "gui", Val{bool: false}, "启用TUI交互界面"} - hotkeyMod = Flag{ptrVal{string: &config.hotkeyMod}, "k", "hotkey-mod", Val{string: "ctrl+alt"}, "本地快捷键修饰(ctrl+alt|ctrl+shift)"} + portName = Flag{ptrVal{string: &cfg.PortName}, "p", "port", Val{string: ""}, "要连接的串口\t(/dev/ttyUSB0、COMx)"} + baudRate = Flag{ptrVal{int: &cfg.BaudRate}, "b", "baud", Val{int: 115200}, "波特率"} + dataBits = Flag{ptrVal{int: &cfg.DataBits}, "d", "data", Val{int: 8}, "数据位"} + stopBits = Flag{ptrVal{int: &cfg.StopBits}, "s", "stop", Val{int: 0}, "停止位停止位(0: 1停止 1:1.5停止 2:2停止)"} + outputCode = Flag{ptrVal{string: &cfg.OutputCode}, "o", "out", Val{string: "UTF-8"}, "输出编码"} + inputCode = Flag{ptrVal{string: &cfg.InputCode}, "i", "in", Val{string: "UTF-8"}, "输入编码"} + endStr = Flag{ptrVal{string: &cfg.EndStr}, "e", "end", Val{string: "\n"}, "终端换行符"} + logExt = Flag{v: ptrVal{ext: &cfg.LogFilePath}, sStr: "l", lStr: "log", dv: Val{extdef: "./%s-$s.txt", string: ""}, help: "日志保存路径"} + timeExt = Flag{v: ptrVal{ext: &cfg.TimesFmt}, sStr: "t", lStr: "time", dv: Val{extdef: "[06-01-02 15:04:05.000]", string: ""}, help: "时间戳格式化字段"} + forWard = Flag{ptrVal{il: &cfg.ForWard}, "f", "forward", Val{int: 0}, "转发模式(0: 无 1:TCP-C 2:UDP-C 支持多次传入)"} + address = Flag{ptrVal{sl: &cfg.Address}, "a", "address", Val{string: "127.0.0.1:12345"}, "转发服务地址(支持多次传入)"} + frameSize = Flag{ptrVal{int: &cfg.FrameSize}, "F", "Frame", Val{int: 16}, "帧大小"} + parityBit = Flag{ptrVal{int: &cfg.ParityBit}, "v", "verify", Val{int: 0}, "奇偶校验(0:无校验、1:奇校验、2:偶校验、3:1校验、4:0校验)"} + guiMode = Flag{ptrVal{bool: &cfg.EnableGUI}, "g", "gui", Val{bool: false}, "启用TUI交互界面"} + hotkeyMod = Flag{ptrVal{string: &cfg.HotkeyMod}, "k", "hotkey-mod", Val{string: "ctrl+alt"}, "本地快捷键修饰(ctrl+alt|ctrl+shift)"} flags = []Flag{portName, baudRate, dataBits, stopBits, outputCode, inputCode, endStr, forWard, address, frameSize, parityBit, logExt, timeExt, guiMode, hotkeyMod} ) @@ -182,18 +182,18 @@ func flagInit(f *Flag) { } } func flagExt() { - if config.logFilePath != "" { - config.enableLog = true + if cfg.LogFilePath != "" { + cfg.EnableLog = true } - if config.timesFmt != "" { - config.timesTamp = true + if cfg.TimesFmt != "" { + cfg.TimesTamp = true } - if config.hotkeyMod == "" { - config.hotkeyMod = "ctrl+alt" + if cfg.HotkeyMod == "" { + cfg.HotkeyMod = "ctrl+alt" } - config.hotkeyMod = strings.ToLower(strings.TrimSpace(config.hotkeyMod)) - if config.hotkeyMod != "ctrl+alt" && config.hotkeyMod != "ctrl+shift" { - config.hotkeyMod = "ctrl+alt" + cfg.HotkeyMod = strings.ToLower(strings.TrimSpace(cfg.HotkeyMod)) + if cfg.HotkeyMod != "ctrl+alt" && cfg.HotkeyMod != "ctrl+shift" { + cfg.HotkeyMod = "ctrl+alt" } } func getCliFlag() { @@ -230,7 +230,7 @@ func getCliFlag() { singleselect.WithPageSize(4), singleselect.WithFilterInput(inputs), ).Display("选择串口") - config.portName = ports[s] + cfg.PortName = ports[s] s, _ = inf.NewSingleSelect( bauds, @@ -238,38 +238,38 @@ func getCliFlag() { singleselect.WithPageSize(4), ).Display("选择波特率") if s != 0 { - config.baudRate, _ = strconv.Atoi(bauds[s]) + cfg.BaudRate, _ = strconv.Atoi(bauds[s]) } else { b, _ := inf.NewText( text.WithPrompt("BaudRate:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue("115200"), ).Display() - config.baudRate, _ = strconv.Atoi(b) + cfg.BaudRate, _ = strconv.Atoi(b) } v, _ := inf.NewConfirmWithSelection( confirm.WithPrompt("启用Hex"), ).Display() if v { - config.inputCode = "hex" + cfg.InputCode = "hex" b, _ := inf.NewText( text.WithPrompt("Frames:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue("16"), ).Display() - config.frameSize, _ = strconv.Atoi(b) + cfg.FrameSize, _ = strconv.Atoi(b) } v, _ = inf.NewConfirmWithSelection( confirm.WithPrompt("启用时间戳"), ).Display() - config.timesTamp = v + cfg.TimesTamp = v if v { b, _ := inf.NewText( text.WithPrompt("格式化字段:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(timeExt.dv.extdef), ).Display() - config.timesFmt = b + cfg.TimesFmt = b } v, _ = inf.NewConfirmWithSelection( confirm.WithPrompt("启用高级配置"), @@ -281,7 +281,7 @@ func getCliFlag() { singleselect.WithPageSize(4), singleselect.WithFilterInput(inputs), ).Display("选择数据位") - config.dataBits, _ = strconv.Atoi(datas[s]) + cfg.DataBits, _ = strconv.Atoi(datas[s]) s, _ = inf.NewSingleSelect( stops, @@ -289,7 +289,7 @@ func getCliFlag() { singleselect.WithPageSize(4), singleselect.WithFilterInput(inputs), ).Display("选择停止位") - config.stopBits = s + cfg.StopBits = s s, _ = inf.NewSingleSelect( paritys, @@ -297,14 +297,14 @@ func getCliFlag() { singleselect.WithPageSize(4), singleselect.WithFilterInput(inputs), ).Display("选择校验位") - config.parityBit = s + cfg.ParityBit = s t, _ := inf.NewText( text.WithPrompt("换行符:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(endStr.dv.string), ).Display() - config.endStr = t + cfg.EndStr = t v, _ = inf.NewConfirmWithSelection( confirm.WithDefaultYes(), @@ -317,14 +317,14 @@ func getCliFlag() { text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(inputCode.dv.string), ).Display() - config.inputCode = t + cfg.InputCode = t t, _ = inf.NewText( text.WithPrompt("输出编码:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(outputCode.dv.string), ).Display() - config.outputCode = t + cfg.OutputCode = t } G_F_mode: s, _ = inf.NewSingleSelect( @@ -334,13 +334,13 @@ func getCliFlag() { singleselect.WithFilterInput(inputs), ).Display("选择转发模式") if s != 0 { - config.forWard = append(config.forWard, s) + cfg.ForWard = append(cfg.ForWard, s) t, _ = inf.NewText( text.WithPrompt("地址:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(address.dv.string), ).Display() - config.address = append(config.address, t) + cfg.Address = append(cfg.Address, t) goto G_F_mode } @@ -348,14 +348,14 @@ func getCliFlag() { confirm.WithDefaultYes(), confirm.WithPrompt("启用日志"), ).Display() - config.enableLog = e + cfg.EnableLog = e if e { t, _ = inf.NewText( text.WithPrompt("Path:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue("./%s-$s.txt"), ).Display() - config.logFilePath = t + cfg.LogFilePath = t } } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..13f27af --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,42 @@ +// Package config holds the application configuration. +package config + +import ( + "fmt" + "os" + "time" +) + +// Config holds all application settings. +type Config struct { + PortName string + BaudRate int + DataBits int + StopBits int + ParityBit int + OutputCode string + InputCode string + EndStr string + EnableLog bool + LogFilePath string + ForWard []int + FrameSize int + TimesTamp bool + TimesFmt string + Address []string + EnableGUI bool + HotkeyMod string +} + +// OpenLogFile opens the configured log file for writing, or returns nil if logging is disabled. +func OpenLogFile(cfg *Config) (*os.File, error) { + if cfg.EnableLog { + path := fmt.Sprintf(cfg.LogFilePath, cfg.PortName, time.Now().Format("2006_01_02T150405")) + f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + return f, nil + } + return nil, nil +} diff --git a/main.go b/main.go index fa40669..ec8b0d7 100644 --- a/main.go +++ b/main.go @@ -32,10 +32,10 @@ func main() { normalizeFlags() pflag.Parse() flagExt() - if config.portName == "" { + if cfg.PortName == "" { getCliFlag() } - ports, err := checkPortAvailability(config.portName) + ports, err := checkPortAvailability(cfg.PortName) if err != nil { fmt.Println(err) printUsage(ports) @@ -52,7 +52,7 @@ func main() { os.Exit(1) } - app, err := NewApp(&config) + app, err := NewApp(cfg) if err != nil { fmt.Fprintf(os.Stderr, "create app failed: %v\n", err) os.Exit(1) @@ -63,9 +63,9 @@ func main() { app.startOutputLoop() go forwardInterruptToRemote(app) - app.SetUIEnabled(config.enableGUI) + app.SetUIEnabled(cfg.EnableGUI) - if config.enableGUI { + if cfg.EnableGUI { model := newUIModel(app) p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithoutSignalHandler()) if _, err = p.Run(); err != nil { @@ -287,7 +287,7 @@ func runConsole(app *App) error { } if b == '\r' || b == '\n' { - if err = app.writeToSession([]byte(config.endStr)); err != nil { + if err = app.writeToSession([]byte(cfg.EndStr)); err != nil { app.Statusf("[send] %v", err) } lineStart = true @@ -328,7 +328,7 @@ func parseCSIu(seq []byte) (cp int, mod int, ok bool) { } func isAltKeyExit(b byte) bool { - if normalizeHotkeyPrefix(config.hotkeyMod) != "ctrl+alt" { + if normalizeHotkeyPrefix(cfg.HotkeyMod) != "ctrl+alt" { return false } // 0x2E = scan code for 'C', 0x03 = Ctrl+C, 0x63 = 'c', 0x43 = 'C' @@ -336,7 +336,7 @@ func isAltKeyExit(b byte) bool { } func isExitHotkeySeq(seq []byte) bool { - mod := normalizeHotkeyPrefix(config.hotkeyMod) + mod := normalizeHotkeyPrefix(cfg.HotkeyMod) // CSI u format: ESC [ codepoint ; modifier u // Only matches when the Ctrl modifier bit (4) is present, diff --git a/tui_hotkeys.go b/tui_hotkeys.go index 14af8ca..1a41f46 100644 --- a/tui_hotkeys.go +++ b/tui_hotkeys.go @@ -9,7 +9,7 @@ import ( func handleLocalHotkey(m *uiModel, key string) bool { if m.isLocalHotkey(key, "h") { - modifier := strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.hotkeyMod)) + modifier := strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.HotkeyMod)) m.app.ShowModal("Shortcuts", modifier+"+C => local exit\nCtrl+C => remote interrupt\n"+modifier+"+F => forward panel\n"+modifier+"+P => plugin panel\n"+modifier+"+M => mode panel\nF1 => shortcut help") return true } @@ -48,7 +48,7 @@ func (m *uiModel) isLocalHotkey(key, action string) bool { } } - mod := normalizeHotkeyPrefix(m.app.cfg.hotkeyMod) + mod := normalizeHotkeyPrefix(m.app.cfg.HotkeyMod) if mod == "ctrl+shift" { return hasCtrl && hasShift } diff --git a/tui_model.go b/tui_model.go index 284c4a7..73d7562 100644 --- a/tui_model.go +++ b/tui_model.go @@ -160,7 +160,7 @@ func (m *uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.isLocalHotkey(keyStr, "c") { - m.app.Statusf("[local] exiting by %s+C", strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.hotkeyMod))) + m.app.Statusf("[local] exiting by %s+C", strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.HotkeyMod))) m.app.Close() return m, tea.Quit } @@ -171,7 +171,7 @@ func (m *uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Some terminals can't encode Ctrl+Alt/Shift+H distinctly and report Ctrl+H. if keyStr == "ctrl+h" { - handleLocalHotkey(m, hotkeyWith(m.app.cfg.hotkeyMod, "h")) + handleLocalHotkey(m, hotkeyWith(m.app.cfg.HotkeyMod, "h")) return m, nil } @@ -184,7 +184,7 @@ func (m *uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch keyStr { case "f1": - handleLocalHotkey(m, hotkeyWith(m.app.cfg.hotkeyMod, "h")) + handleLocalHotkey(m, hotkeyWith(m.app.cfg.HotkeyMod, "h")) return m, nil case "tab", "shift+tab": @@ -245,7 +245,7 @@ func (m *uiModel) View() string { } else if len(m.suggestions) == 1 { suggest = "Tab: " + m.suggestions[0] } - modifier := strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.hotkeyMod)) + modifier := strings.ToUpper(normalizeHotkeyPrefix(m.app.cfg.HotkeyMod)) hotkeys := "Hotkeys: Ctrl+C remote | " + modifier + "+C local | " + modifier + "+F forward | " + modifier + "+P plugins | " + modifier + "+M mode | F1 help" hotkeys = lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("245")).Render(hotkeys) status := m.statusLine diff --git a/tui_panels.go b/tui_panels.go index f42aad7..13760cc 100644 --- a/tui_panels.go +++ b/tui_panels.go @@ -71,7 +71,7 @@ func (m *uiModel) refreshPanel() { } func (m *uiModel) buildModeItems() []modeItem { - return []modeItem{{"in", "Input Charset", m.app.cfg.inputCode}, {"out", "Output Charset", m.app.cfg.outputCode}, {"end", "Line End", fmt.Sprintf("%q", m.app.cfg.endStr)}, {"frame", "Hex Frame Size", fmt.Sprintf("%d", m.app.cfg.frameSize)}, {"timestamp", "Timestamp", fmt.Sprintf("%v", m.app.cfg.timesTamp)}, {"timefmt", "Timestamp Format", m.app.cfg.timesFmt}} + return []modeItem{{"in", "Input Charset", m.app.cfg.InputCode}, {"out", "Output Charset", m.app.cfg.OutputCode}, {"end", "Line End", fmt.Sprintf("%q", m.app.cfg.EndStr)}, {"frame", "Hex Frame Size", fmt.Sprintf("%d", m.app.cfg.FrameSize)}, {"timestamp", "Timestamp", fmt.Sprintf("%v", m.app.cfg.TimesTamp)}, {"timefmt", "Timestamp Format", m.app.cfg.TimesFmt}} } func (m *uiModel) handleForwardPanelKey(key string) bool { @@ -213,7 +213,7 @@ func (m *uiModel) handleModePanelKey(key string) bool { switch key { case " ": if sel.key == "timestamp" { - if m.app.cfg.timesTamp { + if m.app.cfg.TimesTamp { m.app.handleLine(".mode set timestamp off") } else { m.app.handleLine(".mode set timestamp on") diff --git a/tui_test.go b/tui_test.go index 2f0a3ee..47582a8 100644 --- a/tui_test.go +++ b/tui_test.go @@ -42,7 +42,7 @@ func TestRenderModal(t *testing.T) { } func TestHandleCtrlShiftLocalHelp(t *testing.T) { - a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}} + a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{HotkeyMod: "ctrl+alt"}} a.SetUIEnabled(true) m := uiModel{app: a} @@ -103,7 +103,7 @@ func TestIsLocalHotkeyAll(t *testing.T) { } for _, tt := range tests { - a := &App{cfg: &Config{hotkeyMod: tt.mod}} + a := &App{cfg: &Config{HotkeyMod: tt.mod}} m := uiModel{app: a} got := m.isLocalHotkey(tt.key, tt.action) if got != tt.want { @@ -216,7 +216,7 @@ func TestMaxIntFunc(t *testing.T) { } func TestHandleLocalHotkeyForward(t *testing.T) { - a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}} + a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{HotkeyMod: "ctrl+alt"}} a.SetUIEnabled(true) m := uiModel{app: a} @@ -230,7 +230,7 @@ func TestHandleLocalHotkeyForward(t *testing.T) { } func TestHandleLocalHotkeyPlugin(t *testing.T) { - a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}} + a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{HotkeyMod: "ctrl+alt"}} a.SetUIEnabled(true) m := uiModel{app: a} @@ -244,7 +244,7 @@ func TestHandleLocalHotkeyPlugin(t *testing.T) { } func TestHandleLocalHotkeyMode(t *testing.T) { - a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}} + a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{HotkeyMod: "ctrl+alt"}} a.SetUIEnabled(true) m := uiModel{app: a} @@ -258,7 +258,7 @@ func TestHandleLocalHotkeyMode(t *testing.T) { } func TestHandleLocalHotkeyUnknown(t *testing.T) { - a := &App{cfg: &Config{hotkeyMod: "ctrl+alt"}} + a := &App{cfg: &Config{HotkeyMod: "ctrl+alt"}} m := uiModel{app: a} if handleLocalHotkey(&m, "ctrl+alt+x") { @@ -267,7 +267,7 @@ func TestHandleLocalHotkeyUnknown(t *testing.T) { } func TestHandleLocalHotkeyCtrlShift(t *testing.T) { - a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+shift"}} + a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{HotkeyMod: "ctrl+shift"}} a.SetUIEnabled(true) m := uiModel{app: a} diff --git a/utils.go b/utils.go index 86deb99..6f6550c 100644 --- a/utils.go +++ b/utils.go @@ -34,13 +34,13 @@ func checkPortAvailability(name string) ([]string, error) { func OpenSerial() error { mode := &serial.Mode{ - BaudRate: config.baudRate, - StopBits: serial.StopBits(config.stopBits), - DataBits: config.dataBits, - Parity: serial.Parity(config.parityBit), + BaudRate: cfg.BaudRate, + StopBits: serial.StopBits(cfg.StopBits), + DataBits: cfg.DataBits, + Parity: serial.Parity(cfg.ParityBit), } var err error - serialPort, err = serial.Open(config.portName, mode) + serialPort, err = serial.Open(cfg.PortName, mode) return err }