mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
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:
+8
-5
@@ -1,5 +1,8 @@
|
|||||||
/build/
|
/build/
|
||||||
.idea
|
.idea
|
||||||
dist/
|
dist/
|
||||||
/go.sum
|
/go.sum
|
||||||
/view/*
|
/view/*
|
||||||
|
.claude/
|
||||||
|
COM.exe
|
||||||
|
coverage.out
|
||||||
@@ -4,12 +4,15 @@ go 1.22
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbles v0.18.0
|
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/fzdwx/infinite v0.12.1
|
||||||
github.com/gobwas/ws v1.4.0
|
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/trzsz/trzsz-go v1.1.7
|
github.com/trzsz/trzsz-go v1.1.7
|
||||||
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
github.com/zimolab/charsetconv v0.1.2
|
github.com/zimolab/charsetconv v0.1.2
|
||||||
go.bug.st/serial v1.6.2
|
go.bug.st/serial v1.6.2
|
||||||
|
golang.org/x/sys v0.19.0
|
||||||
golang.org/x/term 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/alexflint/go-scalar v1.2.0 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/chzyer/readline v1.5.1 // indirect
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // 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/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
|
||||||
github.com/duke-git/lancet/v2 v2.2.1 // indirect
|
github.com/duke-git/lancet/v2 v2.2.1 // indirect
|
||||||
github.com/fzdwx/iter v0.0.0-20230511075109-0afee9319312 // 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/josephspurrier/goversioninfo v1.4.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // 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/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/sync v0.2.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
|
golang.org/x/text v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -13,10 +19,17 @@ func init() {
|
|||||||
for _, f := range flags {
|
for _, f := range flags {
|
||||||
flagInit(&f)
|
flagInit(&f)
|
||||||
}
|
}
|
||||||
cmdinit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "fatal: %v\n", r)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
normalizeFlags()
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
flagExt()
|
flagExt()
|
||||||
if config.portName == "" {
|
if config.portName == "" {
|
||||||
@@ -29,28 +42,337 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志文件输出检测
|
if err = OpenSerial(); err != nil {
|
||||||
checkLogOpen()
|
fmt.Fprintf(os.Stderr, "open serial failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
//串口设备开启
|
|
||||||
OpenSerial()
|
|
||||||
|
|
||||||
defer CloseSerial()
|
|
||||||
// 打开文件服务
|
|
||||||
OpenTrzsz()
|
|
||||||
|
|
||||||
defer CloseTrzsz()
|
|
||||||
|
|
||||||
//开启转发
|
|
||||||
OpenForwarding()
|
|
||||||
|
|
||||||
// 获取终端输入
|
|
||||||
go input(in)
|
|
||||||
|
|
||||||
if len(outs) != 1 {
|
|
||||||
out = io.MultiWriter(outs...)
|
|
||||||
}
|
}
|
||||||
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 (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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/trzsz/trzsz-go/trzsz"
|
"github.com/trzsz/trzsz-go/trzsz"
|
||||||
"github.com/zimolab/charsetconv"
|
"github.com/zimolab/charsetconv"
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -15,9 +14,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
serialPort serial.Port
|
serialPort serial.Port
|
||||||
in io.Reader = os.Stdin
|
|
||||||
out io.Writer = os.Stdout
|
out io.Writer = os.Stdout
|
||||||
outs = []io.Writer{os.Stdout}
|
|
||||||
trzszFilter *trzsz.TrzszFilter
|
trzszFilter *trzsz.TrzszFilter
|
||||||
clientIn *io.PipeReader
|
clientIn *io.PipeReader
|
||||||
stdoutPipe *io.PipeReader
|
stdoutPipe *io.PipeReader
|
||||||
@@ -25,61 +22,30 @@ var (
|
|||||||
clientOut *io.PipeWriter
|
clientOut *io.PipeWriter
|
||||||
)
|
)
|
||||||
|
|
||||||
func input(in io.Reader) {
|
func convertChunk(chunk []byte, srcCode, dstCode string) ([]byte, error) {
|
||||||
var err error
|
if len(chunk) == 0 {
|
||||||
input := bufio.NewScanner(in)
|
return nil, nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func formatHexFrame(frame []byte, withTimestamp bool, tsFmt string) string {
|
||||||
err := charsetconv.EncodeWith(strings.NewReader(str), out, charsetconv.Charset(cs), false)
|
if withTimestamp {
|
||||||
ErrorF(err)
|
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
@@ -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
|
||||||
|
}
|
||||||
@@ -6,18 +6,17 @@ import (
|
|||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkPortAvailability(name string) ([]string, error) {
|
func checkPortAvailability(name string) ([]string, error) {
|
||||||
ports, err := serial.GetPortsList()
|
ports, err := serial.GetPortsList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
return nil, fmt.Errorf("无串口")
|
return nil, fmt.Errorf("无串口")
|
||||||
@@ -33,26 +32,31 @@ func checkPortAvailability(name string) ([]string, error) {
|
|||||||
return ports, fmt.Errorf("串口 " + name + " 未在线")
|
return ports, fmt.Errorf("串口 " + name + " 未在线")
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenSerial() {
|
func OpenSerial() error {
|
||||||
var err error
|
|
||||||
mode := &serial.Mode{
|
mode := &serial.Mode{
|
||||||
BaudRate: config.baudRate,
|
BaudRate: config.baudRate,
|
||||||
StopBits: serial.StopBits(config.stopBits),
|
StopBits: serial.StopBits(config.stopBits),
|
||||||
DataBits: config.dataBits,
|
DataBits: config.dataBits,
|
||||||
Parity: serial.Parity(config.parityBit),
|
Parity: serial.Parity(config.parityBit),
|
||||||
}
|
}
|
||||||
|
var err error
|
||||||
serialPort, err = serial.Open(config.portName, mode)
|
serialPort, err = serial.Open(config.portName, mode)
|
||||||
ErrorF(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloseSerial() {
|
func CloseSerial() {
|
||||||
err := serialPort.Close()
|
if serialPort == nil {
|
||||||
ErrorF(err)
|
return
|
||||||
return
|
}
|
||||||
|
|
||||||
|
if err := serialPort.Close(); err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, err)
|
||||||
|
fmt.Fprint(os.Stderr, "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var termch chan os.Signal
|
var termch chan os.Signal
|
||||||
|
var termchOnce sync.Once
|
||||||
|
|
||||||
// OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ).
|
// OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ).
|
||||||
//
|
//
|
||||||
@@ -61,13 +65,12 @@ var termch chan os.Signal
|
|||||||
// │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │
|
// │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │
|
||||||
// │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │
|
// │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │
|
||||||
// └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘
|
// └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘
|
||||||
func OpenTrzsz() {
|
func OpenTrzsz() error {
|
||||||
fd := int(os.Stdin.Fd())
|
fd := int(os.Stdin.Fd())
|
||||||
width, _, err := term.GetSize(fd)
|
width, _, err := term.GetSize(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
fmt.Printf("term get size failed: %s\n", err)
|
return fmt.Errorf("term get size failed: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
width = 80
|
width = 80
|
||||||
}
|
}
|
||||||
@@ -78,6 +81,8 @@ func OpenTrzsz() {
|
|||||||
trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true})
|
trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true})
|
||||||
trzsz.SetAffectedByWindows(false)
|
trzsz.SetAffectedByWindows(false)
|
||||||
termch = make(chan os.Signal, 1)
|
termch = make(chan os.Signal, 1)
|
||||||
|
termchOnce = sync.Once{}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range termch {
|
for range termch {
|
||||||
width, _, err := term.GetSize(fd)
|
width, _, err := term.GetSize(fd)
|
||||||
@@ -88,38 +93,18 @@ func OpenTrzsz() {
|
|||||||
trzszFilter.SetTerminalColumns(int32(width))
|
trzszFilter.SetTerminalColumns(int32(width))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloseTrzsz() {
|
func CloseTrzsz() {
|
||||||
signal.Stop(termch)
|
if termch == nil {
|
||||||
close(termch)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user