← ssh · API reference · version 0.1.4
3 file(s), 203 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
Session struct | 25 | — | 68 69 74 80 86 91 … (10 total) |
byte_at fn | 59 | SSH-002 | 57 63 66 |
connect fn | 68 | SSH-001 | 10 27 28 |
ok fn | 74 | SSH-001 | 11 28 |
login fn | 80 | — | 13 30 |
open_shell fn | 86 | — | 31 |
send fn | 91 | — | 36 |
recv fn | 100 | SSH-002 | 33 34 |
resize fn | 107 | — | 37 |
is_open fn | 112 | — | 14 32 |
close fn | 117 | — | 15 39 |
src/ssh.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// --- SSH client ---
// A blocking / polling SSH client for driving an interactive remote shell —
// e.g. attaching `tmux` from a terminal app. Password authentication only; the
// password is used for the handshake and never stored. Native-only: SSH needs
// real TCP sockets, so this library does not ship the browser (`--html`) target.
//
// Typical use:
// s = ssh::connect(host, 42022);
// if s.ok() && s.login(user, password) && s.open_shell("xterm-256color", 80, 24) {
// s.send("tmux attach\n");
// while s.is_open() {
// out = s.recv(); // non-blocking; "" when idle
// for i in 0..size(out) { handle_byte(ssh::byte_at(i, out)); }
// }
// }
//
// Note the `size(out)` bound: `recv` answers raw BYTES and `byte_at` indexes bytes,
// while `len` counts CHARACTERS — a `0..len(out)` loop drops the tail of every
// multi-byte sequence, silently.
// A live SSH session. `handle >= 0` once the TCP connect + transport handshake
// succeed; `< 0` means the connect failed. Obtain one from `connect`.
pub struct Session {
handle: integer
}
// --- internal native surface (implemented in native/src/lib.rs) ---
fn ssh_connect(host: text, port: integer) -> integer;
#native
fn ssh_login(handle: integer, user: text, password: text) -> boolean;
#native
fn ssh_open_shell(handle: integer, term: text, cols: integer, rows: integer) -> boolean;
#native
fn ssh_send(handle: integer, data: text);
#native
fn ssh_recv(handle: integer) -> text;
#native
fn ssh_resize(handle: integer, cols: integer, rows: integer);
#native
fn ssh_is_open(handle: integer) -> boolean;
#native
fn ssh_close(handle: integer);
#native
// Read the byte at `idx` (0..size) of a `recv` result. A terminal stream is raw
// bytes, so read it byte-by-byte rather than as UTF-8. Out of range returns -1 —
// not 0, which the stream itself may legitimately carry.
// Example: @SSH-002 — bound the loop with `size`, and test the end with `< 0`.
pub fn byte_at(idx: integer, data: text) -> integer;
#native
// --- public API ---
// Open a TCP connection to `host:port` and run the SSH transport handshake.
// Check `ok()` before authenticating.
// Example: @SSH-001 — a failed session stays callable and silent, so `ok()` is the
// only thing that reports it.
pub fn connect(host: text, port: integer) -> Session {
Session { handle: ssh_connect(host, port) }
}
// True when the session connected and completed the transport handshake.
// Example: @SSH-001 — skip this and the whole session ladder runs, quietly, on nothing.
pub fn ok(self: Session) -> boolean {
self.handle >= 0
}
// Authenticate with a password. Returns true on success. The password is used
// only for this handshake and is never stored by the library.
pub fn login(self: Session, user: text, password: text) -> boolean {
self.handle >= 0 && ssh_login(self.handle, user, password)
}
// Request a pseudo-terminal of `cols` x `rows` (terminal type `term`, e.g.
// "xterm-256color") and start the login shell. Returns true on success.
pub fn open_shell(self: Session, term: text, cols: integer, rows: integer) -> boolean {
self.handle >= 0 && ssh_open_shell(self.handle, term, cols, rows)
}
// Send bytes to the remote shell (keystrokes, escape sequences). Binary-safe.
pub fn send(self: Session, data: text) {
if self.handle >= 0 { ssh_send(self.handle, data); }
}
// Return the bytes the remote shell produced since the last call, or "" when
// nothing is pending (non-blocking). Binary-safe: iterate 0..size(result) and
// read each byte with `byte_at` — `size` is the BYTE count, which is what
// `byte_at` is indexed against; `len` counts characters and would truncate.
// Example: @SSH-002 — the byte a `len`-bounded loop drops, and why -1 is not 0.
pub fn recv(self: Session) -> text {
if self.handle < 0 { return ""; }
ssh_recv(self.handle)
}
// Tell the remote the terminal resized to `cols` x `rows` so it re-flows. Call
// after a zoom / rotate / window resize.
pub fn resize(self: Session, cols: integer, rows: integer) {
if self.handle >= 0 { ssh_resize(self.handle, cols, rows); }
}
// True while the shell channel is open (the remote has not closed it).
pub fn is_open(self: Session) -> boolean {
self.handle >= 0 && ssh_is_open(self.handle)
}
// Close the shell channel and disconnect.
pub fn close(self: Session) {
if self.handle >= 0 { ssh_close(self.handle); }
}
tests/connect_fail.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Gate test: connecting to a closed port must fail (not panic, not hang), and
// the library must load + call through the native FFI on both the interpreter
// and --native. No sshd or credentials required.
use ssh::*;
fn main() {
s = ssh::connect("127.0.0.1", 1);
assert(!s.ok());
// A failed session's operations must be safe no-ops.
assert(!s.login("u", "p"));
assert(!s.is_open());
s.close();
println("ssh connect_fail: PASS");
}
tests/worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `ssh` library (@PLN141). Each `test_*` below is a REAL,
// run-every-CI demonstration of a function whose correct use is not obvious from its
// signature alone; the function documents itself by CITING the tag with
// `// Example: @SSH-00N`, and `check_doc_drift.sh examples` fails if a citation goes
// dangling (no test carries the tag).
//
// Neither needs an sshd. @SSH-001 is about what a FAILED session does, so a refused
// connection is the subject rather than a limitation; @SSH-002 is about how to read the
// bytes `recv` hands back, which is a property of the text, not of the connection.
use ssh::*;
// @SSH-001 — every method self-guards, so nothing tells you the session is dead unless
// you ask. `connect` always answers a `Session`; a refused connection, a wrong host, a
// closed port all give one back with a negative handle, and from there `login` and
// `open_shell` answer false, `recv` answers "", `send` / `resize` / `close` do nothing,
// and none of it raises. That is deliberate — a terminal app must not die because a
// host went away mid-frame — but it means a caller who skips `ok()` gets a program that
// runs its whole session ladder, sends every keystroke, reads an endless empty stream,
// and never once reports a problem. Check `ok()` right after `connect`, before
// authenticating; after that, `is_open()` is the per-iteration question, because the
// remote can close the channel on a session that connected perfectly well.
fn test_a_failed_session_is_silent_not_fatal() {
// Port 1 is privileged and unbound, so this is a refused connection.
s = ssh::connect("127.0.0.1", 1);
assert(!s.ok(), "ok() is the one call that reports the connect failed");
// Everything downstream stays callable and answers its own falsey value.
assert(!s.login("user", "secret"), "login on a dead session answers false");
assert(!s.open_shell("xterm-256color", 80, 24), "open_shell answers false");
assert(!s.is_open(), "is_open answers false");
idle = s.recv();
assert(idle.size() == 0, "recv answers an empty stream, never an error");
// The write side is a silent no-op rather than a failure a caller could notice.
s.send("tmux attach\n");
s.resize(100, 40);
// Closing a session that never opened is safe too.
s.close();
}
// @SSH-002 — `recv` hands back a raw BYTE stream, so bound the loop with `size`, never
// `len`. A terminal emits UTF-8, escape sequences and raw control bytes together, and
// loft `text` measures `len` in CHARACTERS while `byte_at` indexes BYTES. Bound a
// byte-indexed loop by the character count and every multi-byte sequence loses its tail
// — silently, since both are integers and the loop simply ends early. `size` is the
// byte count and is what `byte_at` is indexed against; out of range answers -1, which is
// how a decoder tells the end of the stream from a legitimate 0 byte in it.
fn test_recv_is_bytes_so_bound_the_loop_with_size() {
// One ASCII byte then a two-byte UTF-8 character: three bytes, two characters.
stream = "aé";
assert(stream.size() == 3, "the byte count is 3");
assert(stream.len() == 2, "the character count is 2 — the two disagree");
// The correct loop reaches every byte.
seen = 0;
for i in 0..stream.size() {
b = ssh::byte_at(i, stream);
assert(b >= 0, "every index below size() is a real byte");
seen += 1;
}
assert(seen == 3, "size() visits all three bytes");
// Bounding by len() stops one byte short, and nothing says so.
assert(ssh::byte_at(stream.len(), stream) == 0xA9,
"the byte a len()-bounded loop drops is a real one, not padding");
// Past the end is -1, distinct from a 0 byte the stream may genuinely carry.
assert(ssh::byte_at(stream.size(), stream) == -1, "one past the end answers -1");
}