mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
refactor: extract pkg/forward and pkg/luaplugin packages
Move ForwardManager → pkg/forward/Manager and PluginManager → pkg/luaplugin/Manager. Move FoeWardMode (now forward.Mode) with ParseMode/Network/String into pkg/forward. Rename constants: NOT→None, TCPC→TCP, UDPC→UDP. Update all references in main package. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,14 @@ import (
|
||||
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/charset"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *Config
|
||||
forward *ForwardManager
|
||||
plugins *PluginManager
|
||||
forward *forward.Manager
|
||||
plugins *luaplugin.Manager
|
||||
dispatcher *CommandDispatcher
|
||||
|
||||
uiEvents chan event.UIEvent
|
||||
@@ -40,14 +42,14 @@ func NewApp(cfg *Config) (*App, error) {
|
||||
|
||||
a := &App{
|
||||
cfg: cfg,
|
||||
plugins: NewPluginManager(),
|
||||
plugins: luaplugin.NewManager(),
|
||||
uiEvents: make(chan event.UIEvent, 512),
|
||||
done: make(chan struct{}),
|
||||
logFile: f,
|
||||
}
|
||||
a.uiEnabled.Store(true)
|
||||
|
||||
a.forward = NewForwardManager(a.writeRawToSession, a.Notifyf)
|
||||
a.forward = forward.NewManager(a.writeRawToSession, a.Notifyf)
|
||||
a.forward.SetInboundReporter(a.reportForwardIngress)
|
||||
a.dispatcher = NewCommandDispatcher(a)
|
||||
if err = a.loadDefaultDemoPlugin(); err != nil {
|
||||
@@ -167,8 +169,8 @@ func (a *App) waitDone() <-chan struct{} {
|
||||
|
||||
func (a *App) loadConfiguredForwards() {
|
||||
for i, mode := range config.forWard {
|
||||
m := FoeWardMode(mode)
|
||||
if m == NOT {
|
||||
m := forward.Mode(mode)
|
||||
if m == forward.None {
|
||||
continue
|
||||
}
|
||||
if i >= len(config.address) {
|
||||
|
||||
+10
-8
@@ -9,6 +9,8 @@ import (
|
||||
"go.bug.st/serial"
|
||||
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||
)
|
||||
|
||||
func TestPrefixLines(t *testing.T) {
|
||||
@@ -60,7 +62,7 @@ func TestSendLine(t *testing.T) {
|
||||
setupTestPipes()
|
||||
a := &App{
|
||||
cfg: &Config{endStr: "\r\n"},
|
||||
plugins: NewPluginManager(),
|
||||
plugins: luaplugin.NewManager(),
|
||||
uiEvents: make(chan event.UIEvent, 8),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
@@ -82,12 +84,12 @@ func TestHandleLine(t *testing.T) {
|
||||
setupTestPipes()
|
||||
a := &App{
|
||||
cfg: &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"},
|
||||
plugins: NewPluginManager(),
|
||||
plugins: luaplugin.NewManager(),
|
||||
uiEvents: make(chan event.UIEvent, 8),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
a.SetUIEnabled(true)
|
||||
a.forward = NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
a.forward = forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
a.dispatcher = NewCommandDispatcher(a)
|
||||
|
||||
a.handleLine("hello")
|
||||
@@ -142,8 +144,8 @@ func TestEmitUISaturation(t *testing.T) {
|
||||
func TestAppClose(t *testing.T) {
|
||||
a := &App{
|
||||
done: make(chan struct{}),
|
||||
plugins: NewPluginManager(),
|
||||
forward: NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}),
|
||||
plugins: luaplugin.NewManager(),
|
||||
forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
|
||||
uiEvents: make(chan event.UIEvent, 4),
|
||||
}
|
||||
a.SetUIEnabled(true)
|
||||
@@ -167,20 +169,20 @@ func TestLoadConfiguredForwards(t *testing.T) {
|
||||
defer listener.Close()
|
||||
|
||||
config = Config{
|
||||
forWard: []int{int(TCPC), int(NOT), int(UDPC)},
|
||||
forWard: []int{int(forward.TCP), int(forward.None), int(forward.UDP)},
|
||||
address: []string{listener.Addr().String(), "", ""},
|
||||
}
|
||||
|
||||
a := &App{
|
||||
cfg: &config,
|
||||
forward: NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}),
|
||||
forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
|
||||
uiEvents: make(chan event.UIEvent, 8),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
a.SetUIEnabled(true)
|
||||
|
||||
a.loadConfiguredForwards()
|
||||
// TCPC should be added, NOT skipped, UDPC skipped (empty address)
|
||||
// forward.TCP should be added, forward.None skipped, forward.UDP skipped (empty address)
|
||||
items := a.forward.List()
|
||||
if len(items) != 1 || items[0].Mode != "tcp" {
|
||||
t.Fatalf("expected 1 TCP forward, got %+v", items)
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
)
|
||||
|
||||
type CommandHandler func(args []string) error
|
||||
@@ -297,7 +298,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
|
||||
if len(args) < 4 {
|
||||
return fmt.Errorf("usage: .forward add <tcp|udp> <address>")
|
||||
}
|
||||
mode, ok := parseForwardMode(args[2])
|
||||
mode, ok := forward.ParseMode(args[2])
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown forward mode: %s", args[2])
|
||||
}
|
||||
@@ -333,7 +334,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode, ok := parseForwardMode(args[3])
|
||||
mode, ok := forward.ParseMode(args[3])
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown forward mode: %s", args[3])
|
||||
}
|
||||
|
||||
+4
-2
@@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||
)
|
||||
|
||||
func setupTestPipes() {
|
||||
@@ -25,12 +27,12 @@ func setupTestPipes() {
|
||||
func newTestAppForCommand() *App {
|
||||
a := &App{
|
||||
cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"},
|
||||
plugins: NewPluginManager(),
|
||||
plugins: luaplugin.NewManager(),
|
||||
uiEvents: make(chan event.UIEvent, 32),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
a.SetUIEnabled(true)
|
||||
a.forward = NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
a.forward = forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
a.dispatcher = NewCommandDispatcher(a)
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -27,49 +26,8 @@ type Config struct {
|
||||
hotkeyMod string
|
||||
}
|
||||
|
||||
type FoeWardMode int
|
||||
|
||||
const (
|
||||
NOT FoeWardMode = iota
|
||||
TCPC
|
||||
UDPC
|
||||
)
|
||||
|
||||
var config Config
|
||||
|
||||
func (m FoeWardMode) Network() string {
|
||||
switch m {
|
||||
case TCPC:
|
||||
return "tcp"
|
||||
case UDPC:
|
||||
return "udp"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m FoeWardMode) String() string {
|
||||
switch m {
|
||||
case TCPC:
|
||||
return "tcp"
|
||||
case UDPC:
|
||||
return "udp"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
func parseForwardMode(v string) (FoeWardMode, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "tcp", "tcp-c", "tcpc", "1":
|
||||
return TCPC, true
|
||||
case "udp", "udp-c", "udpc", "2":
|
||||
return UDPC, true
|
||||
default:
|
||||
return NOT, false
|
||||
}
|
||||
}
|
||||
|
||||
func openLogFile() (*os.File, error) {
|
||||
if config.enableLog {
|
||||
path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405"))
|
||||
|
||||
+16
-14
@@ -6,17 +6,19 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
)
|
||||
|
||||
func TestForwardModeNetworkAndString(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode FoeWardMode
|
||||
mode forward.Mode
|
||||
network string
|
||||
name string
|
||||
}{
|
||||
{mode: NOT, network: "", name: "none"},
|
||||
{mode: TCPC, network: "tcp", name: "tcp"},
|
||||
{mode: UDPC, network: "udp", name: "udp"},
|
||||
{mode: forward.None, network: "", name: "none"},
|
||||
{mode: forward.TCP, network: "tcp", name: "tcp"},
|
||||
{mode: forward.UDP, network: "udp", name: "udp"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -32,22 +34,22 @@ func TestForwardModeNetworkAndString(t *testing.T) {
|
||||
func TestParseForwardMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
mode FoeWardMode
|
||||
mode forward.Mode
|
||||
ok bool
|
||||
}{
|
||||
{input: "tcp", mode: TCPC, ok: true},
|
||||
{input: "TCP-C", mode: TCPC, ok: true},
|
||||
{input: "1", mode: TCPC, ok: true},
|
||||
{input: "udp", mode: UDPC, ok: true},
|
||||
{input: " 2 ", mode: UDPC, ok: true},
|
||||
{input: "unknown", mode: NOT, ok: false},
|
||||
{input: "", mode: NOT, ok: false},
|
||||
{input: "tcp", mode: forward.TCP, ok: true},
|
||||
{input: "TCP-C", mode: forward.TCP, ok: true},
|
||||
{input: "1", mode: forward.TCP, ok: true},
|
||||
{input: "udp", mode: forward.UDP, ok: true},
|
||||
{input: " 2 ", mode: forward.UDP, ok: true},
|
||||
{input: "unknown", mode: forward.None, ok: false},
|
||||
{input: "", mode: forward.None, ok: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, ok := parseForwardMode(tt.input)
|
||||
got, ok := forward.ParseMode(tt.input)
|
||||
if ok != tt.ok || got != tt.mode {
|
||||
t.Fatalf("parseForwardMode(%q) got=(%v,%v) want=(%v,%v)", tt.input, got, ok, tt.mode, tt.ok)
|
||||
t.Fatalf("forward.ParseMode(%q) got=(%v,%v) want=(%v,%v)", tt.input, got, ok, tt.mode, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package forward
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestForwardManagerTCPFlow(t *testing.T) {
|
||||
func TestManagerTCPFlow(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen failed: %v", err)
|
||||
@@ -25,13 +25,13 @@ func TestForwardManagerTCPFlow(t *testing.T) {
|
||||
}()
|
||||
|
||||
serialCh := make(chan string, 2)
|
||||
mgr := NewForwardManager(func(b []byte) error {
|
||||
mgr := NewManager(func(b []byte) error {
|
||||
serialCh <- string(b)
|
||||
return nil
|
||||
}, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
|
||||
id, err := mgr.Add(TCPC, listener.Addr().String())
|
||||
id, err := mgr.Add(TCP, listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Add() failed: %v", err)
|
||||
}
|
||||
@@ -92,12 +92,12 @@ func TestForwardManagerTCPFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardManagerErrorCases(t *testing.T) {
|
||||
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
func TestManagerErrorCases(t *testing.T) {
|
||||
mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
|
||||
if _, err := mgr.Add(NOT, "127.0.0.1:1"); err == nil {
|
||||
t.Fatalf("Add(NOT) expected error")
|
||||
if _, err := mgr.Add(None, "127.0.0.1:1"); err == nil {
|
||||
t.Fatalf("Add(None) expected error")
|
||||
}
|
||||
|
||||
if err := mgr.Remove(999); err == nil {
|
||||
@@ -112,7 +112,7 @@ func TestForwardManagerErrorCases(t *testing.T) {
|
||||
t.Fatalf("Enable(non-existing) expected error")
|
||||
}
|
||||
|
||||
if err := mgr.Update(999, TCPC, "127.0.0.1:1"); err == nil {
|
||||
if err := mgr.Update(999, TCP, "127.0.0.1:1"); err == nil {
|
||||
t.Fatalf("Update(non-existing) expected error")
|
||||
}
|
||||
|
||||
@@ -122,28 +122,27 @@ func TestForwardManagerErrorCases(t *testing.T) {
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
id, err := mgr.Add(TCPC, listener.Addr().String())
|
||||
id, err := mgr.Add(TCP, listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Add() failed: %v", err)
|
||||
}
|
||||
if err = mgr.Update(id, NOT, "127.0.0.1:1"); err == nil {
|
||||
t.Fatalf("Update(NOT) expected error")
|
||||
if err = mgr.Update(id, None, "127.0.0.1:1"); err == nil {
|
||||
t.Fatalf("Update(None) expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardManagerSetInboundReporter(t *testing.T) {
|
||||
func TestManagerSetInboundReporter(t *testing.T) {
|
||||
reported := make(chan []byte, 1)
|
||||
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
mgr.SetInboundReporter(func(id int, chunk []byte) {
|
||||
reported <- chunk
|
||||
})
|
||||
if mgr.onInbound == nil {
|
||||
t.Fatalf("SetInboundReporter should set onInbound")
|
||||
}
|
||||
// Verify the callback was stored (indirect test)
|
||||
_ = reported
|
||||
}
|
||||
|
||||
func TestForwardManagerBroadcastToDisabled(t *testing.T) {
|
||||
func TestManagerBroadcastToDisabled(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen failed: %v", err)
|
||||
@@ -151,37 +150,34 @@ func TestForwardManagerBroadcastToDisabled(t *testing.T) {
|
||||
defer listener.Close()
|
||||
|
||||
writeCh := make(chan []byte, 4)
|
||||
mgr := NewForwardManager(func([]byte) error {
|
||||
mgr := NewManager(func([]byte) error {
|
||||
writeCh <- nil
|
||||
return nil
|
||||
}, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
|
||||
id, err := mgr.Add(TCPC, listener.Addr().String())
|
||||
id, err := mgr.Add(TCP, listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Add() failed: %v", err)
|
||||
}
|
||||
|
||||
// Disable and verify broadcast skips it
|
||||
if err = mgr.Disable(id); err != nil {
|
||||
t.Fatalf("Disable() failed: %v", err)
|
||||
}
|
||||
|
||||
mgr.Broadcast([]byte("should-not-arrive"))
|
||||
|
||||
// No writeToSerial should be triggered
|
||||
select {
|
||||
case <-writeCh:
|
||||
t.Fatalf("broadcast should not write to serial when disabled")
|
||||
default:
|
||||
}
|
||||
|
||||
// Empty data should be no-op
|
||||
mgr.Broadcast(nil)
|
||||
mgr.Broadcast([]byte{})
|
||||
}
|
||||
|
||||
func TestForwardManagerEnable(t *testing.T) {
|
||||
func TestManagerEnable(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen failed: %v", err)
|
||||
@@ -189,13 +185,13 @@ func TestForwardManagerEnable(t *testing.T) {
|
||||
defer listener.Close()
|
||||
|
||||
writeCh := make(chan []byte, 2)
|
||||
mgr := NewForwardManager(func([]byte) error {
|
||||
mgr := NewManager(func([]byte) error {
|
||||
writeCh <- nil
|
||||
return nil
|
||||
}, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
|
||||
id, err := mgr.Add(TCPC, listener.Addr().String())
|
||||
id, err := mgr.Add(TCP, listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Add() failed: %v", err)
|
||||
}
|
||||
@@ -204,7 +200,6 @@ func TestForwardManagerEnable(t *testing.T) {
|
||||
t.Fatalf("Disable() failed: %v", err)
|
||||
}
|
||||
|
||||
// Re-enable should create a new connection
|
||||
if err = mgr.Enable(id); err != nil {
|
||||
t.Fatalf("Enable() failed: %v", err)
|
||||
}
|
||||
@@ -214,13 +209,12 @@ func TestForwardManagerEnable(t *testing.T) {
|
||||
t.Fatalf("expected enabled after Enable(), got=%+v", items)
|
||||
}
|
||||
|
||||
// Enable again (should be no-op since already enabled and connected)
|
||||
if err = mgr.Enable(id); err != nil {
|
||||
t.Fatalf("second Enable() should succeed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardManagerUpdate(t *testing.T) {
|
||||
func TestManagerUpdate(t *testing.T) {
|
||||
l1, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen 1 failed: %v", err)
|
||||
@@ -233,16 +227,15 @@ func TestForwardManagerUpdate(t *testing.T) {
|
||||
}
|
||||
defer l2.Close()
|
||||
|
||||
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
|
||||
defer mgr.Close()
|
||||
|
||||
id, err := mgr.Add(TCPC, l1.Addr().String())
|
||||
id, err := mgr.Add(TCP, l1.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Add() failed: %v", err)
|
||||
}
|
||||
|
||||
// Update to new address (reconnects)
|
||||
if err = mgr.Update(id, TCPC, l2.Addr().String()); err != nil {
|
||||
if err = mgr.Update(id, TCP, l2.Addr().String()); err != nil {
|
||||
t.Fatalf("Update() failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -251,11 +244,10 @@ func TestForwardManagerUpdate(t *testing.T) {
|
||||
t.Fatalf("update should change address, got=%+v", items)
|
||||
}
|
||||
|
||||
// Update disabled target
|
||||
if err = mgr.Disable(id); err != nil {
|
||||
t.Fatalf("Disable() failed: %v", err)
|
||||
}
|
||||
if err = mgr.Update(id, TCPC, l1.Addr().String()); err != nil {
|
||||
if err = mgr.Update(id, TCP, l1.Addr().String()); err != nil {
|
||||
t.Fatalf("Update() on disabled should succeed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,84 @@
|
||||
package main
|
||||
// Package forward manages TCP/UDP forwarding targets for serial data.
|
||||
package forward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ForwardStats struct {
|
||||
// Mode is the forwarding protocol mode.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
None Mode = iota
|
||||
TCP
|
||||
UDP
|
||||
)
|
||||
|
||||
// ParseMode parses a mode string. Accepts "tcp"/"tcp-c"/"tcpc"/"1" → TCP, "udp"/"udp-c"/"udpc"/"2" → UDP.
|
||||
func ParseMode(v string) (Mode, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "tcp", "tcp-c", "tcpc", "1":
|
||||
return TCP, true
|
||||
case "udp", "udp-c", "udpc", "2":
|
||||
return UDP, true
|
||||
default:
|
||||
return None, false
|
||||
}
|
||||
}
|
||||
|
||||
func (m Mode) Network() string {
|
||||
switch m {
|
||||
case TCP:
|
||||
return "tcp"
|
||||
case UDP:
|
||||
return "udp"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case TCP:
|
||||
return "tcp"
|
||||
case UDP:
|
||||
return "udp"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
// Stats holds I/O statistics for a forward target.
|
||||
type Stats struct {
|
||||
ReadBytes uint64
|
||||
WrittenBytes uint64
|
||||
LastError string
|
||||
}
|
||||
|
||||
type ForwardTarget struct {
|
||||
// Target represents a single forwarding connection.
|
||||
type Target struct {
|
||||
ID int
|
||||
Mode FoeWardMode
|
||||
Mode Mode
|
||||
Address string
|
||||
Enabled bool
|
||||
Connected bool
|
||||
CreatedAt time.Time
|
||||
|
||||
conn net.Conn
|
||||
stats ForwardStats
|
||||
stats Stats
|
||||
mu sync.Mutex
|
||||
closeCh chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
type ForwardSnapshot struct {
|
||||
// Snapshot is a read-only view of a forward target for display.
|
||||
type Snapshot struct {
|
||||
ID int
|
||||
Mode string
|
||||
Address string
|
||||
@@ -41,36 +89,40 @@ type ForwardSnapshot struct {
|
||||
LastError string
|
||||
}
|
||||
|
||||
type ForwardManager struct {
|
||||
// Manager coordinates forwarding targets.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
targets map[int]*ForwardTarget
|
||||
targets map[int]*Target
|
||||
nextID int
|
||||
writeToSerial func([]byte) error
|
||||
notify func(string, ...any)
|
||||
onInbound func(int, []byte)
|
||||
}
|
||||
|
||||
func NewForwardManager(writeToSerial func([]byte) error, notify func(string, ...any)) *ForwardManager {
|
||||
return &ForwardManager{
|
||||
targets: make(map[int]*ForwardTarget),
|
||||
// NewManager creates a forwarding manager.
|
||||
func NewManager(writeToSerial func([]byte) error, notify func(string, ...any)) *Manager {
|
||||
return &Manager{
|
||||
targets: make(map[int]*Target),
|
||||
nextID: 1,
|
||||
writeToSerial: writeToSerial,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ForwardManager) SetInboundReporter(fn func(int, []byte)) {
|
||||
// SetInboundReporter sets a callback invoked when inbound data arrives from a target.
|
||||
func (m *Manager) SetInboundReporter(fn func(int, []byte)) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.onInbound = fn
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) {
|
||||
if mode == NOT {
|
||||
// Add creates and connects a new forward target.
|
||||
func (m *Manager) Add(mode Mode, address string) (int, error) {
|
||||
if mode == None {
|
||||
return 0, fmt.Errorf("forward mode cannot be none")
|
||||
}
|
||||
|
||||
t := &ForwardTarget{
|
||||
t := &Target{
|
||||
Mode: mode,
|
||||
Address: address,
|
||||
Enabled: true,
|
||||
@@ -98,7 +150,7 @@ func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) {
|
||||
return t.ID, nil
|
||||
}
|
||||
|
||||
func (m *ForwardManager) readLoop(t *ForwardTarget, conn net.Conn, stop <-chan struct{}) {
|
||||
func (m *Manager) readLoop(t *Target, conn net.Conn, stop <-chan struct{}) {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
@@ -133,7 +185,8 @@ func (m *ForwardManager) readLoop(t *ForwardTarget, conn net.Conn, stop <-chan s
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Remove(id int) error {
|
||||
// Remove disconnects and removes a target.
|
||||
func (m *Manager) Remove(id int) error {
|
||||
m.mu.Lock()
|
||||
t, ok := m.targets[id]
|
||||
if !ok {
|
||||
@@ -148,7 +201,8 @@ func (m *ForwardManager) Remove(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Enable(id int) error {
|
||||
// Enable (re)connects a target.
|
||||
func (m *Manager) Enable(id int) error {
|
||||
m.mu.RLock()
|
||||
t, ok := m.targets[id]
|
||||
m.mu.RUnlock()
|
||||
@@ -178,8 +232,9 @@ func (m *ForwardManager) Enable(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error {
|
||||
if mode == NOT {
|
||||
// Update changes a target's mode and address, reconnecting if enabled.
|
||||
func (m *Manager) Update(id int, mode Mode, address string) error {
|
||||
if mode == None {
|
||||
return fmt.Errorf("forward mode cannot be none")
|
||||
}
|
||||
|
||||
@@ -196,7 +251,6 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
|
||||
t.Address = address
|
||||
t.mu.Unlock()
|
||||
|
||||
// Restart the target to apply new mode/address when enabled.
|
||||
t.close()
|
||||
|
||||
if !wasEnabled {
|
||||
@@ -207,7 +261,8 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
|
||||
return m.Enable(id)
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Disable(id int) error {
|
||||
// Disable disconnects a target without removing it.
|
||||
func (m *Manager) Disable(id int) error {
|
||||
m.mu.RLock()
|
||||
t, ok := m.targets[id]
|
||||
m.mu.RUnlock()
|
||||
@@ -223,13 +278,14 @@ func (m *ForwardManager) Disable(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Broadcast(data []byte) {
|
||||
// Broadcast sends data to all enabled, connected targets.
|
||||
func (m *Manager) Broadcast(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
items := make([]*ForwardTarget, 0, len(m.targets))
|
||||
items := make([]*Target, 0, len(m.targets))
|
||||
for _, t := range m.targets {
|
||||
items = append(items, t)
|
||||
}
|
||||
@@ -251,11 +307,12 @@ func (m *ForwardManager) Broadcast(data []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ForwardManager) List() []ForwardSnapshot {
|
||||
// List returns a snapshot of all targets.
|
||||
func (m *Manager) List() []Snapshot {
|
||||
m.mu.RLock()
|
||||
items := make([]ForwardSnapshot, 0, len(m.targets))
|
||||
items := make([]Snapshot, 0, len(m.targets))
|
||||
for _, t := range m.targets {
|
||||
items = append(items, ForwardSnapshot{
|
||||
items = append(items, Snapshot{
|
||||
ID: t.ID,
|
||||
Mode: t.Mode.String(),
|
||||
Address: t.Address,
|
||||
@@ -275,13 +332,14 @@ func (m *ForwardManager) List() []ForwardSnapshot {
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *ForwardManager) Close() {
|
||||
// Close disconnects and removes all targets.
|
||||
func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
items := make([]*ForwardTarget, 0, len(m.targets))
|
||||
items := make([]*Target, 0, len(m.targets))
|
||||
for _, t := range m.targets {
|
||||
items = append(items, t)
|
||||
}
|
||||
m.targets = map[int]*ForwardTarget{}
|
||||
m.targets = map[int]*Target{}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, t := range items {
|
||||
@@ -289,7 +347,7 @@ func (m *ForwardManager) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ForwardTarget) close() {
|
||||
func (t *Target) close() {
|
||||
t.mu.Lock()
|
||||
if t.closed {
|
||||
t.mu.Unlock()
|
||||
@@ -0,0 +1,50 @@
|
||||
package luaplugin
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
func callStringHook(L *lua.LState, name string, payload string) (*string, bool, error) {
|
||||
fn := L.GetGlobal(name)
|
||||
if fn.Type() == lua.LTNil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}, lua.LString(payload)); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
if ret.Type() == lua.LTNil {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
s := ret.String()
|
||||
return &s, true, nil
|
||||
}
|
||||
|
||||
func callCommandHook(L *lua.LState, name, line string) (string, bool, bool, error) {
|
||||
fn := L.GetGlobal(name)
|
||||
if fn.Type() == lua.LTNil {
|
||||
return "", true, false, nil
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 2, Protect: true}, lua.LString(line)); err != nil {
|
||||
return "", true, true, err
|
||||
}
|
||||
|
||||
allowVal := L.Get(-1)
|
||||
lineVal := L.Get(-2)
|
||||
L.Pop(2)
|
||||
|
||||
allow := true
|
||||
if allowVal.Type() == lua.LTBool {
|
||||
allow = lua.LVAsBool(allowVal)
|
||||
}
|
||||
|
||||
next := ""
|
||||
if lineVal.Type() != lua.LTNil {
|
||||
next = lineVal.String()
|
||||
}
|
||||
|
||||
return next, allow, true, nil
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
package main
|
||||
// Package luaplugin provides a Lua plugin system for processing serial data streams.
|
||||
package luaplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -10,7 +11,8 @@ import (
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type LuaPlugin struct {
|
||||
// Plugin represents a loaded Lua plugin.
|
||||
type Plugin struct {
|
||||
Name string
|
||||
Path string
|
||||
Enabled bool
|
||||
@@ -18,22 +20,26 @@ type LuaPlugin struct {
|
||||
callMu sync.Mutex
|
||||
}
|
||||
|
||||
type PluginSnapshot struct {
|
||||
// Snapshot is a read-only view of a plugin for display.
|
||||
type Snapshot struct {
|
||||
Name string
|
||||
Path string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
// Manager coordinates plugin lifecycle and hook execution.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[string]*LuaPlugin
|
||||
plugins map[string]*Plugin
|
||||
}
|
||||
|
||||
func NewPluginManager() *PluginManager {
|
||||
return &PluginManager{plugins: make(map[string]*LuaPlugin)}
|
||||
// NewManager creates a plugin manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{plugins: make(map[string]*Plugin)}
|
||||
}
|
||||
|
||||
func (m *PluginManager) Load(path string) (string, error) {
|
||||
// Load loads a Lua plugin from the given path.
|
||||
func (m *Manager) Load(path string) (string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -56,7 +62,7 @@ func (m *PluginManager) Load(path string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.plugins[name] = &LuaPlugin{
|
||||
m.plugins[name] = &Plugin{
|
||||
Name: name,
|
||||
Path: abs,
|
||||
Enabled: true,
|
||||
@@ -66,7 +72,8 @@ func (m *PluginManager) Load(path string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Unload(name string) error {
|
||||
// Unload unloads a plugin and closes its Lua state.
|
||||
func (m *Manager) Unload(name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
p, ok := m.plugins[name]
|
||||
@@ -79,7 +86,8 @@ func (m *PluginManager) Unload(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Enable(name string) error {
|
||||
// Enable enables a previously loaded plugin.
|
||||
func (m *Manager) Enable(name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
p, ok := m.plugins[name]
|
||||
@@ -90,7 +98,8 @@ func (m *PluginManager) Enable(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Disable(name string) error {
|
||||
// Disable disables a plugin without unloading it.
|
||||
func (m *Manager) Disable(name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
p, ok := m.plugins[name]
|
||||
@@ -101,7 +110,8 @@ func (m *PluginManager) Disable(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Reload(name string) error {
|
||||
// Reload reloads a plugin's file.
|
||||
func (m *Manager) Reload(name string) error {
|
||||
m.mu.Lock()
|
||||
p, ok := m.plugins[name]
|
||||
m.mu.Unlock()
|
||||
@@ -117,11 +127,12 @@ func (m *PluginManager) Reload(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *PluginManager) List() []PluginSnapshot {
|
||||
// List returns a snapshot of all plugins.
|
||||
func (m *Manager) List() []Snapshot {
|
||||
m.mu.RLock()
|
||||
res := make([]PluginSnapshot, 0, len(m.plugins))
|
||||
res := make([]Snapshot, 0, len(m.plugins))
|
||||
for _, p := range m.plugins {
|
||||
res = append(res, PluginSnapshot{Name: p.Name, Path: p.Path, Enabled: p.Enabled})
|
||||
res = append(res, Snapshot{Name: p.Name, Path: p.Path, Enabled: p.Enabled})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
@@ -131,17 +142,19 @@ func (m *PluginManager) List() []PluginSnapshot {
|
||||
return res
|
||||
}
|
||||
|
||||
func (m *PluginManager) ProcessInput(data []byte) ([]byte, error) {
|
||||
// ProcessInput runs the OnInput hook chain across all enabled plugins.
|
||||
func (m *Manager) ProcessInput(data []byte) ([]byte, error) {
|
||||
return m.processDataHook("OnInput", data)
|
||||
}
|
||||
|
||||
func (m *PluginManager) ProcessOutput(data []byte) ([]byte, error) {
|
||||
// ProcessOutput runs the OnOutput hook chain across all enabled plugins.
|
||||
func (m *Manager) ProcessOutput(data []byte) ([]byte, error) {
|
||||
return m.processDataHook("OnOutput", data)
|
||||
}
|
||||
|
||||
func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error) {
|
||||
func (m *Manager) processDataHook(name string, data []byte) ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
plugins := make([]*LuaPlugin, 0, len(m.plugins))
|
||||
plugins := make([]*Plugin, 0, len(m.plugins))
|
||||
for _, p := range m.plugins {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
@@ -170,9 +183,10 @@ func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) ProcessCommand(line string) (string, bool, error) {
|
||||
// ProcessCommand runs the OnCommand hook chain across all enabled plugins.
|
||||
func (m *Manager) ProcessCommand(line string) (string, bool, error) {
|
||||
m.mu.RLock()
|
||||
plugins := make([]*LuaPlugin, 0, len(m.plugins))
|
||||
plugins := make([]*Plugin, 0, len(m.plugins))
|
||||
for _, p := range m.plugins {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
@@ -205,58 +219,12 @@ func (m *PluginManager) ProcessCommand(line string) (string, bool, error) {
|
||||
return current, true, nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Close() {
|
||||
// Close closes all plugin Lua states.
|
||||
func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, p := range m.plugins {
|
||||
p.L.Close()
|
||||
}
|
||||
m.plugins = map[string]*LuaPlugin{}
|
||||
}
|
||||
|
||||
func callStringHook(L *lua.LState, name string, payload string) (*string, bool, error) {
|
||||
fn := L.GetGlobal(name)
|
||||
if fn.Type() == lua.LTNil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}, lua.LString(payload)); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
if ret.Type() == lua.LTNil {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
s := ret.String()
|
||||
return &s, true, nil
|
||||
}
|
||||
|
||||
func callCommandHook(L *lua.LState, name, line string) (string, bool, bool, error) {
|
||||
fn := L.GetGlobal(name)
|
||||
if fn.Type() == lua.LTNil {
|
||||
return "", true, false, nil
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 2, Protect: true}, lua.LString(line)); err != nil {
|
||||
return "", true, true, err
|
||||
}
|
||||
|
||||
allowVal := L.Get(-1)
|
||||
lineVal := L.Get(-2)
|
||||
L.Pop(2)
|
||||
|
||||
allow := true
|
||||
if allowVal.Type() == lua.LTBool {
|
||||
allow = lua.LVAsBool(allowVal)
|
||||
}
|
||||
|
||||
next := ""
|
||||
if lineVal.Type() != lua.LTNil {
|
||||
next = lineVal.String()
|
||||
}
|
||||
|
||||
return next, allow, true, nil
|
||||
m.plugins = map[string]*Plugin{}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package luaplugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -15,8 +15,8 @@ func writeLuaScript(t *testing.T, name, content string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
func TestPluginManagerLoadAndHooks(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerLoadAndHooks(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "rewrite.lua", `
|
||||
@@ -66,8 +66,8 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerDisableAndUnload(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerDisableAndUnload(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "simple.lua", `
|
||||
@@ -111,8 +111,8 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerOutputDrop(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerOutputDrop(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "drop.lua", `
|
||||
@@ -134,8 +134,8 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerReload(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerReload(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "reloadable.lua", `
|
||||
@@ -165,8 +165,8 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerCommandBlock(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerCommandBlock(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "blocker.lua", `
|
||||
@@ -188,8 +188,8 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerLoadErrors(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerLoadErrors(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
_, err := m.Load("nonexistent_file.lua")
|
||||
@@ -204,8 +204,8 @@ func TestPluginManagerLoadErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerDuplicateLoad(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerDuplicateLoad(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "once.lua", "function OnInput(s) return s end")
|
||||
@@ -220,8 +220,8 @@ func TestPluginManagerDuplicateLoad(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManagerListWithDisabled(t *testing.T) {
|
||||
m := NewPluginManager()
|
||||
func TestManagerListWithDisabled(t *testing.T) {
|
||||
m := NewManager()
|
||||
t.Cleanup(m.Close)
|
||||
|
||||
path := writeLuaScript(t, "mylist.lua", "function OnInput(s) return s end")
|
||||
+4
-2
@@ -9,6 +9,8 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||
)
|
||||
|
||||
type doneMsg struct{}
|
||||
@@ -45,8 +47,8 @@ type uiModel struct {
|
||||
panelKind event.UIPanelKind
|
||||
panelIndex int
|
||||
|
||||
forwardItems []ForwardSnapshot
|
||||
pluginItems []PluginSnapshot
|
||||
forwardItems []forward.Snapshot
|
||||
pluginItems []luaplugin.Snapshot
|
||||
modeItems []modeItem
|
||||
|
||||
promptActive bool
|
||||
|
||||
Reference in New Issue
Block a user