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 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-22 02:25:23 +08:00
parent f6eff2da22
commit 2ffb86cc17
6 changed files with 622 additions and 134 deletions
+3
View File
@@ -3,3 +3,6 @@
dist/
/go.sum
/view/*
.claude/
COM.exe
coverage.out
+4 -6
View File
@@ -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
)
+345 -23
View File
@@ -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+<Key> 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 (0x400x7E): AZ, az, ~, 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)
}
+20 -54
View File
@@ -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
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
}
if !ok {
_, err := io.WriteString(stdinPipe, input.Text())
var buf bytes.Buffer
err := charsetconv.ConvertWith(bytes.NewReader(chunk), charsetconv.Charset(srcCode), &buf, charsetconv.Charset(dstCode), false)
if err != nil {
log.Fatal(err)
}
_, err = io.WriteString(stdinPipe, config.endStr)
if err != nil {
log.Fatal(err)
}
}
err = serialPort.Drain()
ErrorF(err)
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 formatHexFrame(frame []byte, withTimestamp bool, tsFmt string) string {
if withTimestamp {
return fmt.Sprintf("%v % X %q \n", time.Now().Format(tsFmt), frame, frame)
}
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)
}
}
ErrorP(err)
return fmt.Sprintf("% X %q \n", frame, frame)
}
+214
View File
@@ -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
}
+25 -40
View File
@@ -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)
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() {
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)
}
}