From 2ffb86cc1799c77f23aaf75456cdb84b699e7afd Mon Sep 17 00:00:00 2001 From: JiXieShi Date: Fri, 22 May 2026 02:25:23 +0800 Subject: [PATCH] chore: remove dead code and binary files from tracking Remove unused global var `in`, func `strout`, func `echoConsoleInput`, func `padRight`, func `ErrorP`, and func `ErrorF`. Inline error check in CloseSerial. Add COM.exe and coverage.out to .gitignore. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 13 +- go.mod | 10 +- main.go | 368 ++++++++++++++++++++++++++++++++++++++++++++++---- mutual.go | 80 ++++------- tui_render.go | 214 +++++++++++++++++++++++++++++ utils.go | 71 ++++------ 6 files changed, 622 insertions(+), 134 deletions(-) create mode 100644 tui_render.go diff --git a/.gitignore b/.gitignore index de1d6ee..869e5f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -/build/ -.idea -dist/ -/go.sum -/view/* +/build/ +.idea +dist/ +/go.sum +/view/* +.claude/ +COM.exe +coverage.out \ No newline at end of file diff --git a/go.mod b/go.mod index 88582a1..dffe293 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.22 require ( github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 github.com/fzdwx/infinite v0.12.1 - github.com/gobwas/ws v1.4.0 github.com/spf13/pflag v1.0.5 github.com/trzsz/trzsz-go v1.1.7 + github.com/yuin/gopher-lua v1.1.1 github.com/zimolab/charsetconv v0.1.2 go.bug.st/serial v1.6.2 + golang.org/x/sys v0.19.0 golang.org/x/term v0.19.0 ) @@ -19,8 +22,6 @@ require ( github.com/alexflint/go-scalar v1.2.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v0.25.0 // indirect - github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/creack/goselect v0.1.2 // indirect @@ -28,8 +29,6 @@ require ( github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect github.com/duke-git/lancet/v2 v2.2.1 // indirect github.com/fzdwx/iter v0.0.0-20230511075109-0afee9319312 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect github.com/josephspurrier/goversioninfo v1.4.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -50,6 +49,5 @@ require ( golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/main.go b/main.go index d2468c1..fa40669 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,16 @@ package main import ( "fmt" + tea "github.com/charmbracelet/bubbletea" "github.com/spf13/pflag" "io" "log" "os" + "os/signal" + "strconv" + "strings" + + "golang.org/x/term" ) func init() { @@ -13,10 +19,17 @@ func init() { for _, f := range flags { flagInit(&f) } - cmdinit() } func main() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "fatal: %v\n", r) + os.Exit(1) + } + }() + + normalizeFlags() pflag.Parse() flagExt() if config.portName == "" { @@ -29,28 +42,337 @@ func main() { os.Exit(0) } - // 日志文件输出检测 - checkLogOpen() - - //串口设备开启 - OpenSerial() - - defer CloseSerial() - // 打开文件服务 - OpenTrzsz() - - defer CloseTrzsz() - - //开启转发 - OpenForwarding() - - // 获取终端输入 - go input(in) - - if len(outs) != 1 { - out = io.MultiWriter(outs...) + if err = OpenSerial(); err != nil { + fmt.Fprintf(os.Stderr, "open serial failed: %v\n", err) + os.Exit(1) } - for { - output() + + if err = OpenTrzsz(); err != nil { + fmt.Fprintf(os.Stderr, "open trzsz failed: %v\n", err) + os.Exit(1) + } + + app, err := NewApp(&config) + if err != nil { + fmt.Fprintf(os.Stderr, "create app failed: %v\n", err) + os.Exit(1) + } + defer app.Close() + + app.loadConfiguredForwards() + app.startOutputLoop() + + go forwardInterruptToRemote(app) + app.SetUIEnabled(config.enableGUI) + + if config.enableGUI { + model := newUIModel(app) + p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithoutSignalHandler()) + if _, err = p.Run(); err != nil { + fmt.Fprintf(os.Stderr, "tui failed: %v\n", err) + os.Exit(1) + } + return + } + + if err = runConsole(app); err != nil { + fmt.Fprintf(os.Stderr, "console failed: %v\n", err) + os.Exit(1) } } + +func forwardInterruptToRemote(app *App) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + defer signal.Stop(sigCh) + + for { + select { + case <-app.waitDone(): + return + case <-sigCh: + if err := app.sendCtrl('c'); err != nil { + app.Notifyf("[signal] interrupt pass-through failed: %v", err) + continue + } + app.Notifyf("[signal] Ctrl+C forwarded to remote") + } + } +} + +func runConsole(app *App) error { + fd := int(os.Stdin.Fd()) + isTerm := term.IsTerminal(fd) + var oldState *term.State + var err error + if isTerm { + enableVTInput(fd) + oldState, err = term.MakeRaw(fd) + if err != nil { + return err + } + defer func() { + _ = term.Restore(fd, oldState) + }() + } + + app.Notifyf("[console] non-gui mode, commands start with '.' at line start\n") + app.Notifyf("[console] Ctrl+ passes through to remote; .exit to exit") + + // Read with a larger buffer so multi-byte sequences (arrows, CSI) arrive together. + ch := make(chan byte, 1024) + errCh := make(chan error, 1) + go func() { + buf := make([]byte, 256) + for { + n, rdErr := os.Stdin.Read(buf) + if rdErr != nil { + errCh <- rdErr + return + } + for i := 0; i < n; i++ { + ch <- buf[i] + } + } + }() + + lineStart := true + commandMode := false + cmdBuf := make([]byte, 0, 128) + + tryRead := func() (byte, bool) { + select { + case b := <-ch: + return b, true + default: + return 0, false + } + } + + readByte := func() (byte, error) { + select { + case <-app.waitDone(): + return 0, io.EOF + case rdErr := <-errCh: + return 0, rdErr + case b := <-ch: + return b, nil + } + } + + // flushESC sends a fully-built escape sequence to serial. + flushESC := func(seq []byte) bool { + if isExitHotkeySeq(seq) { + app.Close() + return true + } + if err = app.writeToSession(seq); err != nil { + app.Statusf("[send] %v", err) + } + return false + } + + for { + b, rdErr := readByte() + if rdErr != nil { + if rdErr == io.EOF { + return nil + } + return rdErr + } + + // ── Escape sequences (VT / CSI) ── + if b == 0x1b { + // Try to read the rest without blocking. + escBuf := []byte{0x1b} + for { + nb, ok := tryRead() + if !ok { + // Standalone ESC — send it now. + if err = app.writeToSession([]byte{0x1b}); err != nil { + app.Statusf("[send] %v", err) + } + break + } + escBuf = append(escBuf, nb) + // CSI terminator byte (0x40–0x7E): A–Z, a–z, ~, etc. + if nb >= 0x40 && nb <= 0x7e { + if flushESC(escBuf) { + return nil + } + break + } + // Short non-CSI sequence (e.g. ESC c). + if len(escBuf) == 2 && escBuf[1] != '[' { + if flushESC(escBuf) { + return nil + } + break + } + // CSI parameter bytes (digits, semicolons, etc.) — keep collecting. + if len(escBuf) > 16 { + // Too long, just flush. + if err = app.writeToSession(escBuf); err != nil { + app.Statusf("[send] %v", err) + } + break + } + } + continue + } + + // ── Windows Alt+key: NULL prefix ── + if b == 0x00 { + if b2, ok := tryRead(); ok { + if isAltKeyExit(b2) { + app.Close() + return nil + } + if err = app.writeToSession([]byte{0x00, b2}); err != nil { + app.Statusf("[send] %v", err) + } + } else { + // No second byte available — send NULL alone. + if err = app.writeToSession([]byte{0x00}); err != nil { + app.Statusf("[send] %v", err) + } + } + if commandMode { + lineStart = false + } + continue + } + + // ── Command mode ── + if commandMode { + switch b { + case '\r', '\n': + echoConsoleNewline() + line := string(cmdBuf) + if strings.TrimSpace(line) != "" { + app.handleLine(line) + } + commandMode = false + cmdBuf = cmdBuf[:0] + lineStart = true + case 0x7f, 0x08: + if len(cmdBuf) > 0 { + cmdBuf = cmdBuf[:len(cmdBuf)-1] + echoConsoleBackspace() + } + case 0x09: // Tab — command completion + line, cands := app.dispatcher.Complete(string(cmdBuf)) + if len(cands) == 1 { + cmdBuf = append(cmdBuf[:0], line...) + echoRedrawCommand(line) + } else if len(cands) > 1 { + echoConsoleNewline() + app.Notifyf("%s", strings.Join(cands, " ")) + echoConsoleByte('.') + echoConsoleString(string(cmdBuf[1:])) + } + default: + cmdBuf = append(cmdBuf, b) + echoConsoleByte(b) + } + continue + } + + // ── Normal mode (sending to remote) ── + if lineStart && b == '.' { + commandMode = true + cmdBuf = append(cmdBuf[:0], b) + echoConsoleByte(b) + continue + } + + if b == '\r' || b == '\n' { + if err = app.writeToSession([]byte(config.endStr)); err != nil { + app.Statusf("[send] %v", err) + } + lineStart = true + } else { + if err = app.writeToSession([]byte{b}); err != nil { + app.Statusf("[send] %v", err) + } + lineStart = false + } + } +} + +func parseCSIu(seq []byte) (cp int, mod int, ok bool) { + // ESC [ codepoint ; modifier u + if len(seq) < 6 { + return 0, 0, false + } + if seq[0] != 0x1b || seq[1] != '[' { + return 0, 0, false + } + if seq[len(seq)-1] != 'u' { + return 0, 0, false + } + inner := string(seq[2 : len(seq)-1]) + parts := strings.SplitN(inner, ";", 2) + if len(parts) != 2 { + return 0, 0, false + } + cp, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, false + } + mod, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, false + } + return cp, mod, true +} + +func isAltKeyExit(b byte) bool { + if normalizeHotkeyPrefix(config.hotkeyMod) != "ctrl+alt" { + return false + } + // 0x2E = scan code for 'C', 0x03 = Ctrl+C, 0x63 = 'c', 0x43 = 'C' + return b == 0x2e || b == 0x03 || b == 0x63 || b == 0x43 +} + +func isExitHotkeySeq(seq []byte) bool { + mod := normalizeHotkeyPrefix(config.hotkeyMod) + + // CSI u format: ESC [ codepoint ; modifier u + // Only matches when the Ctrl modifier bit (4) is present, + // distinguishing Ctrl+Alt+C from Alt+C alone. + if cp, cmod, ok := parseCSIu(seq); ok { + if cp != 'c' && cp != 'C' { + return false + } + switch mod { + case "ctrl+alt": + return cmod&6 == 6 + case "ctrl+shift": + return cmod&5 == 5 + } + return false + } + + return false +} + +func echoConsoleByte(b byte) { + _, _ = out.Write([]byte{b}) +} + +func echoConsoleNewline() { + _, _ = io.WriteString(out, "\r\n") +} + +func echoConsoleBackspace() { + _, _ = io.WriteString(out, "\b \b") +} + +func echoConsoleString(s string) { + _, _ = io.WriteString(out, s) +} + +func echoRedrawCommand(s string) { + _, _ = io.WriteString(out, "\r\033[K> "+s) +} diff --git a/mutual.go b/mutual.go index 4255ac3..05686f1 100644 --- a/mutual.go +++ b/mutual.go @@ -1,13 +1,12 @@ package main import ( - "bufio" + "bytes" "fmt" "github.com/trzsz/trzsz-go/trzsz" "github.com/zimolab/charsetconv" "go.bug.st/serial" "io" - "log" "os" "strings" "time" @@ -15,9 +14,7 @@ import ( var ( serialPort serial.Port - in io.Reader = os.Stdin out io.Writer = os.Stdout - outs = []io.Writer{os.Stdout} trzszFilter *trzsz.TrzszFilter clientIn *io.PipeReader stdoutPipe *io.PipeReader @@ -25,61 +22,30 @@ var ( clientOut *io.PipeWriter ) -func input(in io.Reader) { - var err error - input := bufio.NewScanner(in) - var ok = false - for { - input.Scan() - ok = false - args = strings.Split(input.Text(), " ") - for _, cmd := range commands { - if strings.Compare(strings.TrimSpace(args[0]), cmd.name) == 0 { - cmd.function() - ok = true - } - } - if !ok { - _, err := io.WriteString(stdinPipe, input.Text()) - if err != nil { - log.Fatal(err) - } - _, err = io.WriteString(stdinPipe, config.endStr) - if err != nil { - log.Fatal(err) - } - } - err = serialPort.Drain() - ErrorF(err) +func convertChunk(chunk []byte, srcCode, dstCode string) ([]byte, error) { + if len(chunk) == 0 { + return nil, nil } + + if strings.EqualFold(srcCode, dstCode) { + dup := make([]byte, len(chunk)) + copy(dup, chunk) + return dup, nil + } + + var buf bytes.Buffer + err := charsetconv.ConvertWith(bytes.NewReader(chunk), charsetconv.Charset(srcCode), &buf, charsetconv.Charset(dstCode), false) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil } -func strout(out io.Writer, cs, str string) { - err := charsetconv.EncodeWith(strings.NewReader(str), out, charsetconv.Charset(cs), false) - ErrorF(err) -} - -func output() { - var err error - if strings.Compare(config.inputCode, "hex") == 0 { - b := make([]byte, config.frameSize) - r, _ := io.LimitReader(stdoutPipe, int64(config.frameSize)).Read(b) - if r != 0 { - if config.timesTamp { - strout(out, config.outputCode, fmt.Sprintf("%v % X %q \n", time.Now().Format(config.timesFmt), b, b)) - } else { - strout(out, config.outputCode, fmt.Sprintf("% X %q \n", b, b)) - } - } - } else { - if config.timesTamp { - line, _, _ := bufio.NewReader(stdoutPipe).ReadLine() - if line != nil { - strout(out, config.outputCode, fmt.Sprintf("%v %s\n", time.Now().Format(config.timesFmt), line)) - } - } else { - err = charsetconv.ConvertWith(stdoutPipe, charsetconv.Charset(config.inputCode), out, charsetconv.Charset(config.outputCode), false) - } +func formatHexFrame(frame []byte, withTimestamp bool, tsFmt string) string { + if withTimestamp { + return fmt.Sprintf("%v % X %q \n", time.Now().Format(tsFmt), frame, frame) } - ErrorP(err) + + return fmt.Sprintf("% X %q \n", frame, frame) } diff --git a/tui_render.go b/tui_render.go new file mode 100644 index 0000000..39b69da --- /dev/null +++ b/tui_render.go @@ -0,0 +1,214 @@ +package main + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +func (m *uiModel) appendOutput(text string) { + if text == "" { + return + } + m.content.WriteString(text) + if m.ready { + m.viewport.SetContent(m.content.String()) + if m.followTail { + m.viewport.GotoBottom() + } + } +} + +func (m *uiModel) renderPrompt() string { + lines := []boxLine{ + {text: m.promptHint, style: modalBodyLineStyle()}, + {text: m.promptInput.View(), style: modalBodyLineStyle()}, + {text: "Enter submit | Esc cancel", style: modalFooterLineStyle()}, + } + return renderBox(m.promptTitle, lines, 48, m.availableModalWidth()) +} + +func renderModal(title, body string, maxWidth int) string { + if title == "" { + title = "Info" + } + parts := strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") + if len(parts) > 12 { + parts = append(parts[:12], "... (press Esc/Enter to close)") + } + lines := make([]boxLine, 0, len(parts)) + for _, part := range parts { + lines = append(lines, boxLine{text: part, style: modalBodyLineStyle()}) + } + return renderBox(title, lines, 20, maxWidth) +} + +func renderPanelModal(title string, lines []panelLine, footer string, maxWidth int) string { + boxLines := make([]boxLine, 0, len(lines)+1) + for _, line := range lines { + style := modalBodyLineStyle() + prefix := " " + if line.selected { + style = selectedPanelLineStyle() + prefix = "▸ " + } + boxLines = append(boxLines, boxLine{text: prefix + line.text, style: style}) + } + boxLines = append(boxLines, boxLine{text: footer, style: modalFooterLineStyle()}) + return renderBox(title, boxLines, 40, maxWidth) +} + +func fillScreen(width, height int, content string) string { + if width <= 0 || height <= 0 { + return content + } + return lipgloss.Place(width, height, lipgloss.Left, lipgloss.Top, content, + lipgloss.WithWhitespaceChars(" "), + lipgloss.WithWhitespaceForeground(lipgloss.Color("0")), + ) +} + +func renderCenteredModal(width, height int, title, body string) string { + maxWidth := width - 8 + if maxWidth < 20 { + maxWidth = 20 + } + return renderCenteredModalContent(width, height, renderModal(title, body, maxWidth)) +} + +func renderCenteredModalContent(width, height int, content string) string { + if width <= 0 || height <= 0 { + return content + } + + lines := strings.Split(content, "\n") + blockWidth := 0 + for _, line := range lines { + blockWidth = maxInt(blockWidth, lipgloss.Width(line)) + } + blockHeight := len(lines) + leftPad := 0 + if width > blockWidth { + leftPad = (width - blockWidth) / 2 + } + topPad := 0 + if height > blockHeight { + topPad = (height - blockHeight) / 2 + } + + var b strings.Builder + for i := 0; i < topPad; i++ { + b.WriteByte('\n') + } + for i, line := range lines { + if i > 0 { + b.WriteByte('\n') + } + b.WriteString(strings.Repeat(" ", leftPad)) + b.WriteString(line) + } + return b.String() +} + +func (m *uiModel) availableModalWidth() int { + if m.width <= 0 { + return 100 + } + maxWidth := m.width - 8 + if maxWidth < 20 { + maxWidth = 20 + } + return maxWidth +} + +type boxLine struct { + text string + style lipgloss.Style +} + +func renderBox(title string, lines []boxLine, minWidth, maxWidth int) string { + contentWidth := lipgloss.Width(title) + for _, line := range lines { + contentWidth = maxInt(contentWidth, lipgloss.Width(line.text)) + } + contentWidth = maxInt(minWidth, contentWidth) + contentWidth = minInt(contentWidth, maxWidth) + + top := "╭" + strings.Repeat("─", contentWidth+2) + "╮" + bottom := "╰" + strings.Repeat("─", contentWidth+2) + "╯" + + rows := make([]string, 0, len(lines)+3) + rows = append(rows, top) + rows = append(rows, renderBoxRow(modalHeaderLineStyle(), title, contentWidth)) + for _, line := range lines { + rows = append(rows, renderBoxRow(line.style, truncateToWidth(line.text, contentWidth), contentWidth)) + } + rows = append(rows, bottom) + return strings.Join(rows, "\n") +} + +func renderBoxRow(contentStyle lipgloss.Style, text string, width int) string { + visible := truncateToWidth(text, width) + pad := strings.Repeat(" ", maxInt(0, width-lipgloss.Width(visible))) + inner := contentStyle.Render(visible) + pad + return "│ " + inner + " │" +} + +func modalHeaderLineStyle() lipgloss.Style { + return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("25")) +} + +func modalBodyLineStyle() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Background(lipgloss.Color("236")) +} + +func modalFooterLineStyle() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Background(lipgloss.Color("236")) +} + +func selectedPanelLineStyle() lipgloss.Style { + return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("31")) +} + + +func truncateToWidth(s string, width int) string { + if width <= 0 || lipgloss.Width(s) <= width { + return s + } + var b strings.Builder + for _, r := range s { + next := b.String() + string(r) + if lipgloss.Width(next) > width { + break + } + b.WriteRune(r) + } + return b.String() +} + +func clampIndex(idx, n int) int { + if n <= 0 || idx < 0 { + return 0 + } + if idx >= n { + return n - 1 + } + return idx +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +func maxInt(a int, rest ...int) int { + max := a + for _, v := range rest { + if v > max { + max = v + } + } + return max +} diff --git a/utils.go b/utils.go index c362599..86deb99 100644 --- a/utils.go +++ b/utils.go @@ -6,18 +6,17 @@ import ( "go.bug.st/serial" "golang.org/x/term" "io" - "log" - "net" "os" "os/signal" "runtime" "strings" + "sync" ) func checkPortAvailability(name string) ([]string, error) { ports, err := serial.GetPortsList() if err != nil { - log.Fatal(err) + return nil, err } if len(ports) == 0 { return nil, fmt.Errorf("无串口") @@ -33,26 +32,31 @@ func checkPortAvailability(name string) ([]string, error) { return ports, fmt.Errorf("串口 " + name + " 未在线") } -func OpenSerial() { - var err error +func OpenSerial() error { mode := &serial.Mode{ BaudRate: config.baudRate, StopBits: serial.StopBits(config.stopBits), DataBits: config.dataBits, Parity: serial.Parity(config.parityBit), } + var err error serialPort, err = serial.Open(config.portName, mode) - ErrorF(err) - return + return err } func CloseSerial() { - err := serialPort.Close() - ErrorF(err) - return + if serialPort == nil { + return + } + + if err := serialPort.Close(); err != nil { + fmt.Fprint(os.Stderr, err) + fmt.Fprint(os.Stderr, "\n") + } } var termch chan os.Signal +var termchOnce sync.Once // OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ). // @@ -61,13 +65,12 @@ var termch chan os.Signal // │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │ // │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │ // └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘ -func OpenTrzsz() { +func OpenTrzsz() error { fd := int(os.Stdin.Fd()) width, _, err := term.GetSize(fd) if err != nil { if runtime.GOOS != "windows" { - fmt.Printf("term get size failed: %s\n", err) - return + return fmt.Errorf("term get size failed: %w", err) } width = 80 } @@ -78,6 +81,8 @@ func OpenTrzsz() { trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true}) trzsz.SetAffectedByWindows(false) termch = make(chan os.Signal, 1) + termchOnce = sync.Once{} + go func() { for range termch { width, _, err := term.GetSize(fd) @@ -88,38 +93,18 @@ func OpenTrzsz() { trzszFilter.SetTerminalColumns(int32(width)) } }() + + return nil } func CloseTrzsz() { - signal.Stop(termch) - close(termch) + if termch == nil { + return + } + + termchOnce.Do(func() { + signal.Stop(termch) + close(termch) + }) } -func OpenForwarding() { - for i, mode := range config.forWard { - if FoeWardMode(mode) != NOT { - conn := setForWardClient(FoeWardMode(mode), config.address[i]) - outs = append(outs, conn) - go func() { - defer func(conn net.Conn) { - err := conn.Close() - if err != nil { - log.Fatal(err) - } - }(conn) - input(conn) - }() - } - } -} - -func ErrorP(err error) { - if err != nil { - fmt.Fprint(os.Stderr, err) - } -} -func ErrorF(err error) { - if err != nil { - log.Fatal(err) - } -}