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:
@@ -3,3 +3,6 @@
|
||||
dist/
|
||||
/go.sum
|
||||
/view/*
|
||||
.claude/
|
||||
COM.exe
|
||||
coverage.out
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 (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
|
||||
|
||||
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
@@ -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"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user