Documentation
¶
Overview ¶
Package bubblessh embeds a real, interactive SSH session inside a Bubble Tea (charm.land/bubbletea/v2) model.
It opens an SSH connection with golang.org/x/crypto/ssh, requests a PTY, starts a remote shell, and feeds the remote output through a virtual terminal emulator (github.com/charmbracelet/x/vt) that understands ANSI/ VT220 escape sequences. The emulator's rendered screen becomes the Model's View(), and keystrokes typed into the Bubble Tea program are encoded and forwarded to the remote shell — the same way a normal `ssh` client would behave, but as a component you can drop into any TUI.
Architecture ¶
┌──────────────┐ bytes ┌──────────────┐ screen ┌────────────┐
│ SSH session │ ───────► │ vt.Emulator │ ────────► │ View() │
│ (remote pty) │ ◄─────── │ (ANSI state) │ │ (string) │
└──────────────┘ bytes └──────────────┘ └────────────┘
▲ ▲
│ │ SendKey()
│ ┌──────┴───────────┐
└── stdin.Write ── │ tea.KeyPressMsg │
└──────────────────┘
All calls into the vt.Emulator (Write, Resize, Render) happen on Bubble Tea's single Update/View goroutine, so there is no manual locking. Reading SSH stdout happens on a background goroutine that only ever pushes bytes onto a channel — the actual terminal-state mutation happens inside Update(), which is what Bubble Tea guarantees is single-threaded.
Basic usage ¶
m := bubblessh.New("example.com:22",
bubblessh.WithUser("alice"),
bubblessh.WithAgent(),
bubblessh.WithSize(80, 24),
)
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
See the examples/ directory for a standalone full-screen client and for embedding the pane as one half of a split-screen layout.
Index ¶
- type CursorShape
- type Model
- func (m Model) Close() error
- func (m Model) Connected() bool
- func (m Model) Content() string
- func (m Model) Cursor() *tea.Cursor
- func (m Model) Err() error
- func (m Model) Init() tea.Cmd
- func (m Model) ScrollDown(lines int) Model
- func (m Model) ScrollToBottom() Model
- func (m Model) ScrollUp(lines int) Model
- func (m Model) Scrolled() bool
- func (m Model) SetSize(cols, rows int) (Model, tea.Cmd)
- func (m Model) State() State
- func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
- func (m Model) View() tea.View
- type Option
- func WithAcceptNewHostKeys(path string) Option
- func WithAgent() Option
- func WithConnectTimeout(d time.Duration) Option
- func WithCursorShape(shape CursorShape) Option
- func WithEnv(key, value string) Option
- func WithInsecureIgnoreHostKey() Option
- func WithKnownHostsFile(paths ...string) Option
- func WithMouseForwarding() Option
- func WithPassword(password string) Option
- func WithPort(port int) Option
- func WithPrivateKey(pemBytes []byte, passphrase string) Option
- func WithPrivateKeyFile(path, passphrase string) Option
- func WithSize(cols, rows int) Option
- func WithTerm(term string) Option
- func WithUser(user string) Option
- type State
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CursorShape ¶ added in v0.2.0
type CursorShape int
const ( CursorBlock CursorShape = iota CursorUnderline CursorBar )
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is a Bubble Tea component that renders a live, interactive SSH session. Build one with New, run its Init() Cmd (directly, or batched into a parent's Init), and route Update/View like any other Bubble Tea model or embeddable sub-model.
The zero value is not usable — always construct via New.
func New ¶
New creates a Model for the given address ("host" or "host:port"). Call options like WithUser and WithAgent/WithPassword/WithPrivateKey to configure authentication before use — nothing is connected yet, that happens when Init()'s command runs.
func (Model) Close ¶
Close tears down the SSH session and the underlying TCP connection. Call it when you're done with this pane — Bubble Tea has no "unmount" hook, so nothing does this for you automatically (e.g. call it before switching away from this pane, or on program exit).
func (Model) Content ¶
Content returns the current screen content as a plain styled string (ANSI colors/links included, no cursor-positioning wrapper). Use this instead of View() when embedding the pane inside a larger layout, e.g. with lipgloss.JoinHorizontal.
The result is always exactly height rows (and at most width cells per row), matching the size last set via WithSize/SetSize. Overflow is clipped from the top so the bottom — the most recent output — stays visible; ScrollUp/ScrollDown already choose which window of history to show, and this only clamps that window to the pane.
func (Model) Cursor ¶ added in v0.2.0
Cursor returns the cursor to draw for the current state, in content-local coordinates (0,0 is the top-left of Content()) — or nil if no cursor should be shown right now (not connected, or the remote hid it). View() uses this directly; if you're composing Content() into a bigger layout instead, use this too and offset the position by wherever you place that content on screen.
func (Model) Init ¶
Init satisfies tea.Model. It kicks off the SSH connection asynchronously; nothing blocks here.
func (Model) ScrollDown ¶ added in v0.2.0
ScrollDown scrolls the view down by the given number of lines, back toward the live screen.
func (Model) ScrollToBottom ¶ added in v0.2.0
ScrollToBottom returns to the live view. A normal terminal does this the moment you type anything while scrolled back — Update() already calls this on every key press and paste, so you're never typing blind into a view that's showing history instead of the live screen.
func (Model) ScrollUp ¶ added in v0.2.0
ScrollUp scrolls the view up by the given number of lines, into the scrollback history. Clamped at the oldest available line.
func (Model) Scrolled ¶ added in v0.2.0
Scrolled reports whether the view is currently showing scrollback history instead of the live screen.
func (Model) SetSize ¶
SetSize resizes the PTY, both locally (the virtual terminal emulator) and on the remote end (an SSH "window-change" request). Wire this to tea.WindowSizeMsg yourself if this pane should track the full window — it's not done automatically since an embedded pane is often smaller than the whole screen.
type Option ¶
type Option func(*Model)
Option configures a Model. Pass any number of them to New.
func WithAcceptNewHostKeys ¶
WithAcceptNewHostKeys behaves like `ssh -o StrictHostKeyChecking=accept-new`: a host you've never connected to before is trusted automatically, and its key is appended to the known_hosts file at path (created if it doesn't exist yet, along with any missing parent directories). Every later connection to that host is then checked strictly against what was learned — if the server's key ever changes unexpectedly, the connection is refused. That refusal on change is the actual security property; the "trust" part only ever applies once, to a genuinely new host. This is the closest equivalent to the interactive "are you sure you want to continue connecting?" prompt a normal ssh client shows — bubble-ssh can't show that prompt itself (Connect runs on a background goroutine while Bubble Tea already owns the terminal), so this trades the prompt for automatic, remembered trust instead.
func WithAgent ¶
func WithAgent() Option
SSH_AUTH_SOCK environment variable. It's resolved lazily at connection time, so it's safe to call even if no agent is running yet (it'll just fail at Connect time with a clear error).
Windows: this only reaches agents exposed as a Unix domain socket (macOS, Linux, Git Bash/MSYS2, WSL). The native Windows OpenSSH agent service uses a named pipe instead, which net.Dial("unix", ...) can't reach — see docs/ssh-agent-windows.md for the full explanation and possible fixes. Not fixed yet; documented as a known gap.
func WithConnectTimeout ¶
WithConnectTimeout bounds how long dialing and authentication may take. Default is 10 seconds.
func WithCursorShape ¶ added in v0.2.0
func WithCursorShape(shape CursorShape) Option
WithCursorShape sets how the connected terminal's cursor is drawn. Default is CursorBar.
This is a fixed choice — the cursor always renders in this shape, regardless of what the remote program is doing. It does not track cursor-shape requests from the remote side. bubblessh always shows the one shape you pick here.
func WithEnv ¶
WithEnv requests an extra environment variable on the remote session. Note most sshd configs only forward variables listed in their AcceptEnv directive — this is a server-side restriction bubblessh can't work around.
func WithInsecureIgnoreHostKey ¶
func WithInsecureIgnoreHostKey() Option
WithInsecureIgnoreHostKey disables host key verification entirely — every connection, forever, no memory of anything. This makes the connection vulnerable to man-in-the-middle attacks — only use it for throwaway boxes (e.g. a container on localhost) or local testing. For a real host you'll reconnect to, prefer WithAcceptNewHostKeys: it only trusts blindly once, then verifies strictly from then on.
func WithKnownHostsFile ¶
WithKnownHostsFile verifies the server's host key against one or more OpenSSH-format known_hosts files (e.g. "~/.ssh/known_hosts", expanded by you). Unknown hosts are rejected — this never prompts or writes anything, it only checks. If you want a new host to be trusted automatically on first connection and remembered from then on, use WithAcceptNewHostKeys instead. If you never call this, WithAcceptNewHostKeys, or WithInsecureIgnoreHostKey, Connect will try the default "~/.ssh/known_hosts" and fail loudly if it can't find it, rather than silently skipping verification.
func WithMouseForwarding ¶
func WithMouseForwarding() Option
WithMouseForwarding forwards mouse events (clicks, wheel, motion) to the remote program, useful for full-screen remote apps like vim or tmux with mouse mode on. Your top-level tea.Program / parent View still needs to request mouse tracking (set tea.View.MouseMode) for Bubble Tea to emit mouse messages in the first place.
func WithPassword ¶
WithPassword adds password authentication. You can combine this with other auth options (e.g. WithAgent) — the SSH client tries each in turn.
func WithPort ¶
WithPort overrides the port. If you already included a port in the addr passed to New (e.g. "host:2222"), this is not needed.
func WithPrivateKey ¶
WithPrivateKey adds public-key authentication from raw PEM bytes. Pass an empty passphrase if the key isn't encrypted.
func WithPrivateKeyFile ¶
WithPrivateKeyFile adds public-key authentication, reading the key from disk (e.g. "~/.ssh/id_ed25519" — expand "~" yourself, Go doesn't). Pass an empty passphrase if the key isn't encrypted.
func WithSize ¶
WithSize sets the initial PTY size, in columns and rows. Default is 80x24. Call SetSize later to resize an already-connected session (e.g. in response to tea.WindowSizeMsg).