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 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-23 21:45:08 +08:00
parent 2ce672cdde
commit 31dd9da490
15 changed files with 198 additions and 189 deletions
+14 -13
View File
@@ -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})
}
+8 -11
View File
@@ -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
+13 -13
View File
@@ -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)
}
+12 -12
View File
@@ -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)
}
}
+4 -36
View File
@@ -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{}
+31 -30
View File
@@ -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)
}
}
+5 -5
View File
@@ -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")
+41 -41
View File
@@ -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
}
}
+42
View File
@@ -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
}
+8 -8
View File
@@ -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,
+2 -2
View File
@@ -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
}
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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")
+7 -7
View File
@@ -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}
+5 -5
View File
@@ -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
}