mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
40709dadda
tea.WithInputTTY forces bubbletea to use raw ANSI/VT input on Windows instead of the conInputReader, which correctly parses Ctrl+Alt+Key combos as VT sequences (e.g. \x1b[102;6u → ctrl+alt+f). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
346 lines
7.5 KiB
Go
346 lines
7.5 KiB
Go
// Package console provides the non-TUI console mode.
|
|
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"golang.org/x/term"
|
|
|
|
apppkg "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/app"
|
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config"
|
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/flag"
|
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/session"
|
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/tui"
|
|
)
|
|
|
|
// Run parses flags, sets up the session and app, then runs TUI or console mode.
|
|
func Run() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
fmt.Fprintf(os.Stderr, "fatal: %v\n", r)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
cfg := &config.Config{}
|
|
flag.Init(cfg)
|
|
flag.Normalize()
|
|
flag.Parse()
|
|
flag.Ext(cfg)
|
|
if cfg.PortName == "" {
|
|
flag.GetCliFlag(cfg)
|
|
}
|
|
|
|
ports, err := session.CheckPortAvailability(cfg.PortName)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
flag.PrintUsage(ports)
|
|
os.Exit(0)
|
|
}
|
|
|
|
sess, err := session.Open(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "open session failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
appInst, err := apppkg.New(cfg, sess, os.Stdout)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "create app failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer appInst.Close()
|
|
|
|
appInst.LoadConfiguredForwards()
|
|
appInst.StartOutputLoop()
|
|
|
|
go forwardInterruptToRemote(appInst)
|
|
appInst.SetUIEnabled(cfg.EnableGUI)
|
|
|
|
if cfg.EnableGUI {
|
|
model := tui.New(appInst)
|
|
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithInputTTY(), tea.WithoutSignalHandler())
|
|
if _, err = p.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "tui failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err = RunConsole(appInst); err != nil {
|
|
fmt.Fprintf(os.Stderr, "console failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func forwardInterruptToRemote(appInst *apppkg.App) {
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, os.Interrupt)
|
|
defer signal.Stop(sigCh)
|
|
|
|
for {
|
|
select {
|
|
case <-appInst.WaitDone():
|
|
return
|
|
case <-sigCh:
|
|
if err := appInst.SendCtrl('c'); err != nil {
|
|
appInst.Notifyf("[signal] interrupt pass-through failed: %v", err)
|
|
continue
|
|
}
|
|
appInst.Notifyf("[signal] Ctrl+C forwarded to remote")
|
|
}
|
|
}
|
|
}
|
|
|
|
// RunConsole runs the non-TUI console mode.
|
|
func RunConsole(appInst *apppkg.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) }()
|
|
}
|
|
|
|
appInst.Notifyf("[console] non-gui mode, commands start with '.' at line start\n")
|
|
appInst.Notifyf("[console] Ctrl+<Key> passes through to remote; .exit to exit")
|
|
|
|
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]
|
|
}
|
|
}
|
|
}()
|
|
|
|
out := appInst.Out()
|
|
cfg := appInst.Cfg()
|
|
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 <-appInst.WaitDone():
|
|
return 0, io.EOF
|
|
case rdErr := <-errCh:
|
|
return 0, rdErr
|
|
case b := <-ch:
|
|
return b, nil
|
|
}
|
|
}
|
|
|
|
flushESC := func(seq []byte) bool {
|
|
if isExitHotkeySeq(seq, cfg) {
|
|
appInst.Close()
|
|
return true
|
|
}
|
|
if err = appInst.WriteToSession(seq); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
return false
|
|
}
|
|
|
|
for {
|
|
b, rdErr := readByte()
|
|
if rdErr != nil {
|
|
if rdErr == io.EOF {
|
|
return nil
|
|
}
|
|
return rdErr
|
|
}
|
|
|
|
if b == 0x1b {
|
|
escBuf := []byte{0x1b}
|
|
for {
|
|
nb, ok := tryRead()
|
|
if !ok {
|
|
if err = appInst.WriteToSession([]byte{0x1b}); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
break
|
|
}
|
|
escBuf = append(escBuf, nb)
|
|
if nb >= 0x40 && nb <= 0x7e {
|
|
if flushESC(escBuf) {
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
if len(escBuf) == 2 && escBuf[1] != '[' {
|
|
if flushESC(escBuf) {
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
if len(escBuf) > 16 {
|
|
if err = appInst.WriteToSession(escBuf); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if b == 0x00 {
|
|
if b2, ok := tryRead(); ok {
|
|
if isAltKeyExit(b2, cfg) {
|
|
appInst.Close()
|
|
return nil
|
|
}
|
|
if err = appInst.WriteToSession([]byte{0x00, b2}); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
} else {
|
|
if err = appInst.WriteToSession([]byte{0x00}); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
}
|
|
if commandMode {
|
|
lineStart = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
if commandMode {
|
|
switch b {
|
|
case '\r', '\n':
|
|
echoConsoleNewline(out)
|
|
line := string(cmdBuf)
|
|
if strings.TrimSpace(line) != "" {
|
|
appInst.HandleLine(line)
|
|
}
|
|
commandMode = false
|
|
cmdBuf = cmdBuf[:0]
|
|
lineStart = true
|
|
case 0x7f, 0x08:
|
|
if len(cmdBuf) > 0 {
|
|
cmdBuf = cmdBuf[:len(cmdBuf)-1]
|
|
echoConsoleBackspace(out)
|
|
}
|
|
case 0x09:
|
|
line, cands := appInst.Dispatcher().Complete(string(cmdBuf))
|
|
if len(cands) == 1 {
|
|
cmdBuf = append(cmdBuf[:0], line...)
|
|
echoRedrawCommand(out, line)
|
|
} else if len(cands) > 1 {
|
|
echoConsoleNewline(out)
|
|
appInst.Notifyf("%s", strings.Join(cands, " "))
|
|
echoConsoleByte(out, '.')
|
|
echoConsoleString(out, string(cmdBuf[1:]))
|
|
}
|
|
default:
|
|
cmdBuf = append(cmdBuf, b)
|
|
echoConsoleByte(out, b)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if lineStart && b == '.' {
|
|
commandMode = true
|
|
cmdBuf = append(cmdBuf[:0], b)
|
|
echoConsoleByte(out, b)
|
|
continue
|
|
}
|
|
|
|
if b == '\r' || b == '\n' {
|
|
if err = appInst.WriteToSession([]byte(cfg.EndStr)); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
lineStart = true
|
|
} else {
|
|
if err = appInst.WriteToSession([]byte{b}); err != nil {
|
|
appInst.Statusf("[send] %v", err)
|
|
}
|
|
lineStart = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseCSIu(seq []byte) (cp int, mod int, ok bool) {
|
|
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, cfg *config.Config) bool {
|
|
if normalizeHotkey(cfg.HotkeyMod) != "ctrl+alt" {
|
|
return false
|
|
}
|
|
return b == 0x2e || b == 0x03 || b == 0x63 || b == 0x43
|
|
}
|
|
|
|
func isExitHotkeySeq(seq []byte, cfg *config.Config) bool {
|
|
mod := normalizeHotkey(cfg.HotkeyMod)
|
|
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 normalizeHotkey(mod string) string { return config.NormalizeHotkey(mod) }
|
|
|
|
func echoConsoleByte(out io.Writer, b byte) { _, _ = out.Write([]byte{b}) }
|
|
func echoConsoleNewline(out io.Writer) { _, _ = io.WriteString(out, "\r\n") }
|
|
func echoConsoleBackspace(out io.Writer) { _, _ = io.WriteString(out, "\b \b") }
|
|
func echoConsoleString(out io.Writer, s string) { _, _ = io.WriteString(out, s) }
|
|
func echoRedrawCommand(out io.Writer, s string) { _, _ = io.WriteString(out, "\r\033[K> "+s) }
|