|
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m6s
Three fixes:
1. Streaming TextDecoder: a single TextDecoder instance with {stream: true}
persists across events. Split multi-byte UTF-8 sequences at Go read()
boundaries are now buffered and decoded correctly across chunks.
2. requestAnimationFrame batching: incoming SSH data is accumulated and
flushed to xterm.js once per frame instead of on every Wails event.
Eliminates the laggy typewriter effect when output arrives in small
chunks (which is normal for SSH PTY output).
3. PTY baud rate: bumped TTY_OP_ISPEED/OSPEED from 14400 (modem speed)
to 115200. Some remote PTYs throttle output to match the declared rate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .gitea/workflows | ||
| docs | ||
| frontend | ||
| images | ||
| internal | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
Wraith
Native desktop SSH + RDP + SFTP client — a MobaXTerm replacement built with Go and Vue.
Features
- Multi-tabbed SSH terminal with xterm.js + WebGL rendering
- SFTP sidebar on every SSH session (MobaXTerm's killer feature) -- same SSH connection, separate channel
- RDP via FreeRDP3 dynamic linking (purego, no CGO)
- Encrypted vault -- master password derived with Argon2id, secrets sealed with AES-256-GCM
- Connection manager with hierarchical groups, tags, color labels, and full-text search
- 7 built-in terminal themes -- Dracula, Nord, Monokai, One Dark, Solarized Dark, Gruvbox Dark, MobaXTerm Classic
- Tab detach / reattach -- sessions live in the Go backend; tabs can be torn off into separate windows and reattached without dropping the connection
- MobaXTerm import -- plugin interface for
.mobaconfand other formats - Command palette (Ctrl+K) for quick connection search and actions
- Single binary -- ships as
wraith.exe+freerdp3.dll, no Docker, no database server
Tech Stack
Backend (Go)
| Component | Technology | Purpose |
|---|---|---|
| Framework | Wails v3 | Desktop shell, multi-window, type-safe Go-to-JS bindings |
| SSH | golang.org/x/crypto/ssh |
SSH client, PTY, key/password auth |
| SFTP | github.com/pkg/sftp |
Remote filesystem over SSH channel |
| RDP | FreeRDP3 via purego |
RDP protocol, bitmap rendering |
| Database | SQLite via modernc.org/sqlite (pure Go) |
Connections, credentials, settings, themes |
| Encryption | AES-256-GCM + Argon2id | Vault encryption at rest |
Frontend (Vue 3 in WebView2)
| Component | Technology | Purpose |
|---|---|---|
| Framework | Vue 3 (Composition API) | UI framework |
| Terminal | xterm.js 5.x + WebGL addon | SSH terminal emulator |
| CSS | Tailwind CSS 4 | Utility-first styling |
| Components | Naive UI | Tree, tabs, modals, dialogs |
| State | Pinia | Reactive stores for sessions, connections, app state |
| Build | Vite 6 | Frontend build tooling |
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Go | 1.22+ | go.dev/dl |
| Node.js | 20+ | nodejs.org |
| Wails CLI | v3 | go install github.com/wailsapp/wails/v3/cmd/wails3@latest |
Quick Start
# Clone
git clone https://github.com/vstockwell/wraith.git
cd wraith
# Install frontend dependencies
cd frontend && npm install && cd ..
# Run in dev mode (hot-reload frontend + Go backend)
wails3 dev
The app opens a 1400x900 window. On first launch you will be prompted to create a master password for the vault.
Building
# Production build for Windows
wails3 build
# Output: build/bin/wraith.exe
The build embeds the compiled frontend (frontend/dist) into the Go binary via //go:embed. Ship wraith.exe alongside freerdp3.dll for RDP support.
Project Structure
wraith/
main.go # Entry point -- Wails app setup, service registration
go.mod # Go module (github.com/vstockwell/wraith)
internal/
app/
app.go # WraithApp -- wires all services, vault create/unlock
db/
sqlite.go # SQLite open with WAL mode, busy timeout, FK enforcement
migrations.go # Embedded SQL migration runner
migrations/
001_initial.sql # Schema: groups, connections, credentials, ssh_keys,
# themes, host_keys, connection_history, settings
vault/
service.go # Argon2id key derivation + AES-256-GCM encrypt/decrypt
connections/
service.go # Connection and Group CRUD (hierarchical tree)
search.go # Full-text search + tag filtering via json_each()
settings/
service.go # Key-value settings store (vault salt, preferences)
theme/
builtins.go # 7 built-in theme definitions
service.go # Theme CRUD + idempotent seeding
session/
session.go # SessionInfo struct + state machine (connecting/connected/detached)
manager.go # Concurrent session manager -- create, detach, reattach, 32-session cap
plugin/
interfaces.go # ProtocolHandler + Importer + Session interfaces
registry.go # Plugin registry -- register/lookup protocol handlers and importers
frontend/
package.json # Vue 3, Pinia, Naive UI, Tailwind CSS, Vite
vite.config.ts # Vite + Vue + Tailwind plugin config
src/
main.ts # App bootstrap -- createApp, Pinia, mount
App.vue # Root component
layouts/
MainLayout.vue # Sidebar + tab bar + session area + status bar
UnlockLayout.vue # Master password entry screen
components/
sidebar/
ConnectionTree.vue # Hierarchical connection/group tree
SidebarToggle.vue # Collapse/expand sidebar
session/
TabBar.vue # Draggable session tabs
SessionContainer.vue # Active session viewport
common/
StatusBar.vue # Bottom status bar
stores/
app.store.ts # Global app state (sidebar, vault status)
connection.store.ts # Connection + group state
session.store.ts # Active sessions state
images/
wraith-logo.png # Application logo
Architecture
Go Backend Wails v3 Bindings Vue 3 Frontend
(services + business logic) (type-safe Go <-> JS) (WebView2)
|
WraithApp ─────────────────────────┼──────────────> Pinia Stores
|-- VaultService | |-- app.store
|-- ConnectionService | |-- connection.store
|-- ThemeService | |-- session.store
|-- SettingsService | |
|-- SessionManager | Vue Components
|-- PluginRegistry | |-- MainLayout
| |-- ConnectionTree
SQLite (WAL mode) | |-- TabBar
wraith.db | |-- SessionContainer
%APPDATA%\Wraith\ | |-- StatusBar
How it fits together:
main.gocreates aWraithAppand registers Go services as Wails bindings.- Wails generates type-safe JavaScript bindings so the Vue frontend can call Go methods directly.
- The Vue frontend uses Pinia stores to manage reactive state, calling into Go services for all data operations.
- All secrets (passwords, SSH keys) are encrypted with AES-256-GCM before being written to SQLite. The encryption key is derived from the master password using Argon2id and is never persisted.
- Sessions are managed by the Go
SessionManager-- they are decoupled from windows, enabling tab detach/reattach without dropping connections.
Data storage: SQLite with WAL mode at %APPDATA%\Wraith\wraith.db (Windows) or ~/.local/share/wraith/wraith.db (Linux/macOS dev). Foreign keys enforced, 5-second busy timeout.
Plugin Development
Wraith uses a plugin registry with two interfaces: ProtocolHandler for new connection protocols and Importer for loading connections from external tools.
Implementing a ProtocolHandler
package myplugin
import "github.com/vstockwell/wraith/internal/plugin"
type MyProtocol struct{}
func (p *MyProtocol) Name() string { return "myproto" }
func (p *MyProtocol) Connect(config map[string]interface{}) (plugin.Session, error) {
// Establish connection, return a Session
}
func (p *MyProtocol) Disconnect(sessionID string) error {
// Clean up resources
}
Implementing an Importer
package myplugin
import "github.com/vstockwell/wraith/internal/plugin"
type MyImporter struct{}
func (i *MyImporter) Name() string { return "myformat" }
func (i *MyImporter) FileExtensions() []string { return []string{".myconf"} }
func (i *MyImporter) Parse(data []byte) (*plugin.ImportResult, error) {
// Parse file bytes into ImportResult (groups, connections, host keys, theme)
}
Registering Plugins
Register handlers and importers with the plugin registry during app initialization:
app.Plugins.RegisterProtocol(&myplugin.MyProtocol{})
app.Plugins.RegisterImporter(&myplugin.MyImporter{})
The ImportResult struct supports groups, connections, host keys, and an optional theme -- everything needed to migrate from another tool in a single import.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes
- Run tests:
go test ./... - Run frontend checks:
cd frontend && npm run build - Commit and push your branch
- Open a Pull Request
License
MIT -- Copyright (c) 2026 Vantz Stockwell
