The terminal's DarkScrollBar derives from Control, which is selectable by default. Once enough output accumulated to create scrollback, the scrollbar became enabled and was the only selectable child of the TerminalView (a ContainerControl). On window re-activation (minimize/restore, click away and back) WinForms restored focus down the container chain onto the scrollbar, which has no keyboard handling, so typing/Enter went nowhere until the session was reopened. This affected both Serial and PowerShell tabs (shared TerminalView), and only after output appeared — matching the reported symptom. Make the scrollbar mouse-only: SetStyle(Selectable, false) + TabStop=false, so it never takes keyboard focus and the TerminalView keeps focus itself, exactly as it already did when idle (scrollbar disabled). Bump version to 0.3.3 and add the About changelog entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ETTerms
A native Windows terminal workspace (C# .NET 8 WinForms) — SSH, Serial Port, and local Shell (ConPTY) in one window, with a TTL scripting engine ported from MyTeraTerm for automation, plus an optional Serial MCP server that lets AI agents (Kiro CLI / Claude CLI) drive the serial port directly. Standalone, no cloud, no login.
📖 Table of Contents
- ✨ Features
- 🖼️ Layout
- 💻 System Requirements
- 📥 Installation
- 🚀 Quick Start
- 📚 Usage Guide
- 🤖 TTL Scripting
- 👥 Group Sync Execution
- ⚡ PDU Power Control
- 🤖 AI / MCP Integration
- 🔐 Data & Security
- 🔧 Troubleshooting
- 🔨 Building from Source
- 📁 Project Structure
- 🤝 Contributing
- 📜 Version History
- 📄 License
- 🙏 Acknowledgments
✨ Features
Core Functionality
-
🖥️ Multiple protocols in one workspace
- SSH (
SSH.NET): password / private key / keyboard-interactive auth, with built-in SFTP - Serial Port (
System.IO.Ports): configurable COM port, baud, data bits, parity, stop bits, handshake - Local Shell (Windows ConPTY): launch PowerShell / Cmd / Bash directly
- SSH (
-
🪟 Tiling tabbed workspace
- One-click grid layouts:
1×1 / 1×2 / 2×1 / 2×2 / 2×3 / 3×3 - Per-pane manual split:
↔horizontal /↕vertical, drag splitters to resize proportionally - Each pane hosts its own mini tab strip for multiple connections
- One-click grid layouts:
-
🗂️ Editable connection sidebar (Saved Connections)
- Nested folders with connection-count badges, expand / collapse all
- Live filter by name / host, matching branches auto-expand
- Context menu & toolbar: add / rename / delete, drag to re-categorize
- Double-click to open in the active pane; ad-hoc quick connect (not saved)
-
🤖 TTL script automation
- TTL (Tera Term Language) interpreter ported and extended from MyTeraTerm
- Runs
.ttlagainst the active session, showing file / line / current command live - Supports
send/sendln/wait/if/while/ variable math / file logging - Long-running
wait/whilecan be aborted anytime with ■ Stop
-
👥 Group sync execution
- Assign tabs to Group 1/2/3 and run one script across the whole group at once
- Sync commands:
waitall(barrier),sendlnall(send after all arrive),sendlngroup(target a member)
-
⚡ PDU power control (optional)
- Control PDU outlets over SNMP (
SnmpSharpNet) to power-cycle devices during tests - Status → PDU tab: connect to a PDU, auto-poll outlet state every 3 s, and toggle any port on/off with per-row Control buttons
- Script commands:
pduconnect/pductrl
- Control PDU outlets over SNMP (
-
🤖 AI / MCP integration (optional)
- The GUI owns the COM port; a standalone stdio Serial MCP server bridges to it over a local named pipe for Kiro CLI / Claude CLI
- Tools:
serial_list/serial_attach/serial_write/serial_read/serial_detach - AI's serial TX/RX shows live in the GUI tagged
[AI]— open the port in the GUI first, then let the AI attach - One-click setup in Settings → AI MCP: register the server into Claude Code / Kiro with a single button
-
📊 Session RX logging
logopen/logwrite/logclosewrite session output to file
Terminal Rendering
- 🎨 Owner-drawn VT100 / ANSI control, double-buffered cell grid
- 🌑 KKTerm-style dark theme (incl. DWM dark title bar + dark terminal scrollbar)
- 🔤 Configurable font / size / palette / scrollback
- 🖱️ Scroll back through long output with the mouse wheel or the dark scrollbar
- 📋 Select / copy / paste — right-click copy clears the highlight; multi-line paste uses bracketed paste so it isn't submitted line-by-line
🖼️ Layout
┌──────┬─────────────────────┬───────────────────────────────────────┐
│ ▣ T │ Saved Connections │ Workspace (tiling) │
│ ▣ S │ 🔍 search │ ┌─────────────────┬─────────────────┐ │
│ ▣ ⚙ │ ▾ 📁 Servers (2) │ │ [tab1][tab2] + │ [tab1] + │ │
│ │ ▸ SSH srv-01 │ │ │ │ │
│ icon │ ▸ SSH nas │ │ TerminalView │ TerminalView │ │
│ rail │ ▾ 📁 Boards (2) │ │ (owner-drawn) │ (owner-drawn) │ │
│ │ ▸ COM3 @115200 │ │ │ │ │
│ │ ▸ COM7 @9600 │ ├─────────────────┴─────────────────┤ │
│ │ │ │ [Group1-A] ▶ Script ■ Stop │ │
└──────┴─────────────────────┴───────────────────────────────────────┘
Activity Rail Sidebar Tabbed / Tiling Workspace
The Activity Rail switches the three main views — Terminal / Scripts / Settings. The sidebar manages saved connections; the workspace holds multiple sessions across tabs and tiled panes.
💻 System Requirements
| Item | Requirement |
|---|---|
| OS | Windows 10 (1809+) / Windows 11 |
| Runtime | .NET 8 Desktop Runtime (Microsoft.WindowsDesktop.App 8.0.x) |
| Display | 1600×900 or higher recommended |
| Optional hardware | USB-to-Serial adapter (Serial), SNMP-capable PDU (power control) |
Check the runtime:
dotnet --list-runtimes | findstr WindowsDesktop
📥 Installation
Source build for now (no binary release yet).
git clone <repo-url> ETTerms
cd ETTerms
dotnet build
dotnet run --project src\ETTerms\ETTerms.csproj
Full steps in Building from Source.
🚀 Quick Start
Create an SSH connection
- Launch ETTerms; on the left Activity Rail switch to Terminal
- In the sidebar click + New Connection and pick type SSH
- Enter
Host/Port(default 22) /Username, choose auth (password / private key) - Double-click the connection → opens a tab in the active pane
Create a Serial connection
- Sidebar + New Connection, type Serial
- Pick
COM portandBaudRate(e.g.COM3/115200) - Double-click to open
Run a TTL script
- With a tab connected, click ▶ Script on the script bar and pick a
.ttl - The left status bar shows the running line and command live
- Press ■ Stop to abort if needed
📚 Usage Guide
Activity Rail
Switches the three main views: Terminal (workspace), Scripts (edit & run), Settings (preferences).
Connection Sidebar (Saved Connections)
| Action | How |
|---|---|
| Add folder / connection | Toolbar button or context menu |
| Rename / delete | Context menu |
| Categorize | Drag a connection or folder into a target folder (cannot drop into its own descendant) |
| Search | Top search box, live filter by name / host |
| Open connection | Double-click → opens in the active pane |
Workspace Tiling
- One-click grid layouts:
1×1 / 1×2 / 2×1 / 2×2 / 2×3 / 3×3 - Per-pane actions (top-right):
↔split horizontally,↕split vertically,+new tab,✕close - Closing a pane collapses the split so siblings fill the space; at least one pane remains
- The active pane is outlined with an accent border
Terminal
- Incoming channel bytes → ANSI parser → screen buffer → painted
- Keyboard input is encoded to byte sequences sent to the remote
- Supports scrollback, select / copy / paste; font and palette under Settings
🤖 TTL Scripting
On any connection tab's script bar, click ▶ Script to load a .ttl and run it against that connection. Full syntax and examples in
docs/ttl-script-reference.md; sample scripts in tools/scripts/.
Supported Commands
| Command | Description |
|---|---|
send 'text' |
Send text (no newline) |
sendln 'text' |
Send text plus \r\n |
wait 'string' |
Wait until the string appears in the RX buffer (infinite by default, cancellable via Stop); sets result=1 on match |
flushrecv |
Clear the RX buffer |
pause seconds |
Pause N seconds (cancellable via Stop) |
timeout = seconds |
wait timeout; 0 = infinite, N>0 aborts the script with an error on timeout |
if … then / elseif … then / else / endif |
Conditional branching (nestable) |
while … / endwhile |
Loop (nestable, cancellable via Stop) |
name = value |
Variable assignment; integer + - * / and strings; built-in result |
logopen 'file' |
Open a log file (overwrite) |
logwrite 'text' |
Write one line to the log |
logclose |
Close the log (auto-closed at script end) |
messagebox 'msg' |
Show a topmost dialog |
; comment |
Inline comment (from ; to end of line) |
:label |
Label line (skipped) |
Comparison operators (if / elseif / while): >= <= > < == != =, or none (non-zero = true).
Example: auto login
; tools/scripts/login.ttl
timeout = 10
wait 'login: '
sendln 'root'
wait 'Password: '
sendln 'toor'
wait '# '
sendln 'uname -a'
👥 Group Sync Execution
Assign multiple tabs to one Group and run a single script across all of them — ideal for syncing multiple DUTs.
- Right-click a tab → set as Group 1 / 2 / 3 (or clear); the cell footer shows a
[Group1-A]label - Click ▶ Group1 / ▶ Group2 / ▶ Group3 on the toolbar to run across the whole group
| Command | Description |
|---|---|
waitall 'string' |
All members wait for the keyword, then continue together (System.Threading.Barrier) |
sendlnall 'text' |
Each member sends once all have arrived |
sendlngroup N 'text' |
Only the specified member sends |
⚠️ Group sync commands work only in Run Group mode;
▶ Scriptand▶ Run Allreject scripts containing them and show a warning.
⚡ PDU Power Control
Control PDU outlets over SNMP to power-cycle a DUT from within a script.
; connect PDU (device 2 = iPoMan II 1202, IP 192.168.1.21)
pduconnect 2 192.168.1.21
pductrl 2 1 0 ; turn port 1 off
pause 5
pductrl 2 1 1 ; turn port 1 on
wait 'login: '
| Command | Syntax | Description |
|---|---|---|
pduconnect |
pduconnect <device> <ip> |
Connect to a PDU |
pductrl |
pductrl <device> <port> <0|1> |
Set a port off(0) / on(1) |
🤖 AI / MCP Integration
Let AI agents (Kiro CLI / Claude CLI) send/receive on a serial port while you watch it live in the ETTerms GUI.
Key design: the GUI owns the COM port; the MCP server never opens it. A COM port can be opened by only one process at a time, so rather than the MCP server grabbing it, the GUI is the sole owner and runs a local named pipe server (SerialBridgeServer). The standalone ETTerms.SerialMcp (launched by Kiro/Claude CLI) is a thin client: every write / read is forwarded over the pipe to the GUI, which does the real port I/O. AI's TX is echoed into the terminal tagged [AI], so RX/TX flow through the GUI channel and what you see is exactly what the AI sees.
Full closed loop — all inside the ETTerms GUI:
- Tab 1: a Serial session holding e.g. COM3 (shows traffic live; AI's writes tagged
[AI]) - Tab 2: a PowerShell (ConPTY) tab running
kiro-cli, which spawnsETTerms.SerialMcp, which connects back to the pipe
| Tool | Params | Description |
|---|---|---|
serial_list |
— | List serial sessions currently open in the GUI (name + baud) |
serial_attach |
portName | Bind to a GUI serial session by COM name (required before write/read) |
serial_write |
text, appendNewline? | Send text via the GUI (shown live tagged [AI]) |
serial_read |
waitFor?, timeoutMs? | Drain accumulated RX; optionally wait for a substring / timeout |
serial_detach |
— | Unbind (does not close the GUI's port) |
One-click setup (recommended)
In Settings → AI MCP, click Setup on the Claude Code or Kiro card to register the Serial MCP server into that CLI's user-level config (~/.claude.json / ~/.kiro/settings/mcp.json) with a single button. Your other MCP servers are preserved, and each card shows the CLI command to verify the connection. Remove unregisters it.
Manual setup
kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj"
Or add it to agent.json under mcpServers (Claude CLI uses an equivalent mcpServers config). Open the target serial connection in the ETTerms GUI first, then tell the AI: "list serial sessions, attach to COM3, send AT and read the reply."
⚠️ The COM port is owned solely by the GUI. The MCP server attaches over the pipe — it never opens the port itself, so there's no GUI-vs-server conflict.
📖 Step-by-step setup: docs/serial-mcp-guide.md.
🔐 Data & Security
- Standalone, no cloud: connection metadata is stored in a local SQLite file (
%LocalAppData%\ETTerms\ettermsdb.sqlite); no telemetry is sent. - Passwords never stored in plaintext: SQLite holds only a
CredentialKeypointing to Windows Credential Manager; actual passwords / key passphrases are accessed viaCredentialVault. - SSH host keys: the fingerprint is shown for confirmation on first connect (trust-on-first-use), then compared; mismatches warn.
- Logs contain no secrets:
AppLoggerandlogopenrecord only host / port, never credentials. - No
secret/directory: a single-user desktop app with no server-side secrets, DB passwords, or compile-time secrets.
🔧 Troubleshooting
| Issue | Likely cause | Fix |
|---|---|---|
| Startup reports missing runtime | .NET 8 Desktop Runtime not installed | Install Microsoft.WindowsDesktop.App 8.0.x; verify with dotnet --list-runtimes |
| Serial connect fails / port busy | COM port held by another app | Close other terminal apps; a COM port can be opened by only one session at a time |
| SSH auth fails | Wrong credentials / key or host-key mismatch | Check auth settings; verify the host-key fingerprint |
Script stuck on wait |
Expected string never received | Check connection & baud, verify the wait string; set timeout or press ■ Stop |
| Group script rejected | Ran a script with Group commands via ▶ Script |
Use ▶ GroupN instead |
More runbooks in docs/runbooks/.
🔨 Building from Source
Prerequisites
- .NET 8 SDK (or SDK 9/10 + .NET 8 Desktop Runtime)
- Visual Studio 2022 or VS Code + C# extension
- Git
Build & Run
dotnet --list-sdks
dotnet build
dotnet run --project src\ETTerms\ETTerms.csproj
Key NuGet Packages
| Package | Purpose |
|---|---|
SSH.NET |
SSH shell + SFTP |
System.IO.Ports |
Serial connections |
Microsoft.Data.Sqlite |
Connection metadata storage |
SnmpSharpNet |
PDU control (optional) |
Publish (self-contained=false)
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false
📁 Project Structure
ETTerms/
├── README.md # English (default)
├── README.zh-TW.md # Traditional Chinese
├── ARCHITECTURE.md # Full architecture doc
├── CLAUDE.md # Project memory & dev commands
├── ETTerms.slnx
├── docs/
│ ├── ttl-script-reference.md # TTL command reference
│ └── runbooks/ # Runbooks / troubleshooting
├── tools/scripts/ # Sample .ttl scripts
├── src/ETTerms/ # Main application (WinForms)
│ ├── App/ # Window shell: MainForm / ActivityRail / ConnectionSidebar / Workspace
│ ├── Terminal/ # Owner-drawn VT100: TerminalView / AnsiParser / ScreenBuffer / TerminalInput
│ ├── Sessions/ # Connection abstraction: ISessionChannel / SshChannel / SerialChannel / ShellChannel
│ ├── Connections/ # Connection data: Connection / ConnectionStore(SQLite) / CredentialVault
│ ├── Scripting/ # TTL engine: TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu
│ └── Infrastructure/ # AppLogger / AppSettings / McpRegistrar / NativeTheme
└── src/ETTerms.SerialMcp/ # Serial MCP server (stdio) — lets AI agents drive the serial port
Core design: every connection implements ISessionChannel (Write(byte[]) + event DataReceived). TerminalView and TTLInterpreter only know this abstraction, so SSH / Serial / Shell look identical to upper layers — the key to "one script engine driving multiple connection types." See ARCHITECTURE.md.
🤝 Contributing
- Fork and create a feature branch:
git checkout -b feature/your-feature - Follow Conventional Commits:
feat(serial): add auto-reconnect - Push and open a Pull Request
Conventions: PascalCase types / methods, _camelCase private fields, filename = class name; the UI layer depends only on the ISessionChannel abstraction; channel I/O runs in the background and all UI updates go back to the UI thread via Control.Invoke.
📜 Version History
v0.3.2
- Status → PDU tab — a dedicated
Statusrail view hosts the PDU panel: connect by IP, auto-poll all 12 outlets every 3 s (background thread, no manual Refresh), with live current / power readouts - Manual outlet control buttons — each row has a Control button that toggles that port on/off over SNMP; the label tracks state (ON → "Turn OFF", OFF → "Turn ON"), the SNMP set runs off the UI thread, and the grid refreshes after the command
v0.3.1
- Terminal usability fixes — added a dark vertical scrollbar (drag or click-to-page through long output instead of long mouse-wheel scrolling)
- Multi-line paste into Kiro CLI / PSReadLine now pastes as a single block (bracketed paste, DEC mode 2004) instead of submitting each line immediately
- Right-click copy now clears the highlight so you know it worked
v0.3.0
- AI-driven PDU power control (MCP) — new
ETTerms.PduMcpserver lets AI agents (Kiro CLI / Claude CLI) control an SNMP PDU directly: outlets on/off, status, and power-cycle a DUT during automated tests - PDU runs over SNMP (UDP, non-exclusive), so the AI talks to it directly — the ETTerms GUI does not need to be running
- Tools:
pdu_connect/pdu_list/pdu_set_port/pdu_get_port/pdu_status/pdu_power_cycle/pdu_disconnect - Settings → AI MCP now registers both
etterms-serialandetterms-pduwith one click; publishing the GUI auto-bundles both MCP servers
v0.2.2
- Terminal stability — the terminal no longer freezes after minimizing or switching tabs (most noticeable with full-screen TUIs like Kiro CLI in PowerShell); a degenerate 1×1 size is no longer sent to the pseudo-console
- Shift+Enter inserts a newline in the shell, so you can type multi-line commands (plain Enter still submits)
- High-DPI fixes — text and buttons no longer clipped at 125% / 150% scaling (PerMonitorV2); toolbar / settings / sidebar / About now scale adaptively
v0.2.1
- AI MCP one-click setup — new Settings → AI MCP tab registers the Serial MCP server into Claude Code (
~/.claude.json) or Kiro (~/.kiro/settings/mcp.json) with a single button; existing MCP servers are preserved, and each card shows the verify command - Publish now auto-bundles
ETTerms.SerialMcpinto the app folder, so the registered path always resolves
v0.2.0
- Phase 9: Serial MCP server (
ETTerms.SerialMcp) — AI agents (Kiro CLI / Claude CLI) send/receive on the serial port while you watch it live in the GUI - GUI is the sole COM-port owner; the MCP server bridges over a local named pipe (
SerialBridgeServer) and never opens the port itself - Tools:
serial_list/serial_attach/serial_write/serial_read/serial_detach; AI's TX echoed into the terminal tagged[AI] - New app icon (window / taskbar / executable); version shown in the title bar
v0.1.0
- Phases 1–8 complete: window shell, connection sidebar, tiling workspace, Serial / SSH / local Shell (ConPTY) / SFTP
- Owner-drawn VT100 terminal rendering
- TTL scripting engine (ported from MyTeraTerm) + Group sync execution
- PDU power control (SNMP)
- Settings / About
📄 License
Licensed under the MIT License — see LICENSE.
🙏 Acknowledgments
- KKTerm — by ryantsai (MIT) — UI design reference (Activity Rail + tabbed workspace + Saved Connections)
- MyTeraTerm — source of the TTL scripting engine and
AppLogger - SSH.NET, SnmpSharpNet — open-source connectivity / SNMP libraries
- Microsoft — .NET 8, WinForms, ConPTY, Credential Manager