Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dd7de89
Add in-app SSH terminal with WebView and persistent sessions
pappz May 10, 2026
2258890
Add an SSH button to each peer row
pappz Aug 9, 2026
1d9b791
Drop the password field from the SSH connect dialog
pappz Aug 9, 2026
93c3680
Prompt for the SSH password in the terminal and allow reconnecting
pappz Aug 9, 2026
6aa14e4
Persist the SSH session list per profile
pappz Aug 9, 2026
69d1fa2
Give the SSH session list disconnect, reconnect and dark-theme fixes
pappz Aug 9, 2026
c47e96f
Bump netbird submodule for the SSH password and error changes
pappz Aug 9, 2026
59de9fd
Allow duplicating an SSH session to the same host
pappz Aug 9, 2026
c1078cb
Match the SSH session FAB to the profiles page design
pappz Aug 10, 2026
90d7c00
Update the terminal to xterm.js 6.0
pappz Aug 11, 2026
8b01650
Configure the SSH terminal and fix its keyboard handling
pappz Aug 11, 2026
eea15ea
Remember the SSH username instead of defaulting to one
pappz Aug 11, 2026
5766890
Bump netbird submodule to dismiss the SSH auth browser
pappz Aug 11, 2026
dfdb1d2
Run the SSO Custom Tab calls on the main thread
pappz Aug 11, 2026
54bdfcb
Let a stored SSH session be edited
pappz Aug 11, 2026
482a7a8
Allow rotation while the SSH terminal is open
pappz Aug 11, 2026
74cf7ef
Prompt to trust an unknown SSH host key
pappz Aug 11, 2026
0161889
Localize the SSH feature into every supported language
pappz Aug 11, 2026
734295a
Bump the netbird submodule to the main merge
pappz Aug 12, 2026
7ddab90
Bump netbird submodule for the SSH close and reconnect fixes
pappz Aug 12, 2026
f905956
Keep the terminal key bar above the navigation bar
pappz Aug 12, 2026
dbfd2c8
Give the SSH session list a title bar and room to breathe
pappz Aug 12, 2026
09b8254
Call deleting a session what it is
pappz Aug 12, 2026
5493060
Refuse an SSH redial the stopped engine cannot serve
pappz Aug 12, 2026
9be5b7b
Give the settings list a title bar too
pappz Aug 12, 2026
3f26700
Fix threading and lifecycle faults around SSH sessions
pappz Aug 12, 2026
b7786b8
Reject an out-of-range port in the SSH connect dialog
pappz Aug 12, 2026
677ed09
Localize the SSH status and state text
pappz Aug 12, 2026
74fdcd9
Make the SSH rows reachable and match the peer list
pappz Aug 12, 2026
17dd2ba
Update netbird submodule to deduplicated SSH client
pappz Aug 14, 2026
4380065
Update netbird submodule to serialized wasm SSH startup
pappz Aug 14, 2026
a4945d0
Post the device-code login opener's UI work to the main thread
pappz Aug 14, 2026
334ed78
Bump netbird for constructor-style login hints
pappz Aug 14, 2026
159cca7
Bump netbird to the main merge
pappz Aug 14, 2026
791fae7
Default the peer SSH port to 22
pappz Aug 14, 2026
e64e115
Store SSH sessions and known hosts in the profile's preferences
pappz Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions app/src/main/assets/terminal/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>NetBird SSH</title>
<link rel="stylesheet" href="xterm.css">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #000;
overflow: hidden;
}
#terminal {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 6px;
}
/* Without this the .xterm element keeps its initial height, so shrinking the
container (the keyboard opening) never reduces the row count. */
.xterm {
height: 100%;
}
/* xterm 6 draws its own scrollbar element instead of using the native one. */
.xterm-viewport::-webkit-scrollbar { width: 0; }
.xterm .xterm-scrollable-element > .scrollbar { display: none; }
</style>
</head>
<body>
<div id="terminal"></div>
<script src="xterm.js"></script>
<script src="xterm-addon-fit.js"></script>
<script src="xterm-addon-webgl.js"></script>
<script>
// Without an explicit palette the 16 ANSI colours fall back to the WebView
// defaults, whose blues and greys are close to unreadable on black. These are
// the standard xterm hues raised for contrast against a dark background.
var TERMINAL_THEME = {
background: '#000000',
foreground: '#e6e6e6',
cursor: '#e6e6e6',
cursorAccent: '#000000',
selectionBackground: '#4d4d4d',
black: '#2e3436',
red: '#e05561',
green: '#8cc265',
yellow: '#d5b06b',
blue: '#5c9fd6',
magenta: '#c162de',
cyan: '#42b3c2',
white: '#d7dae0',
brightBlack: '#6b7280',
brightRed: '#ff6d75',
brightGreen: '#a5e075',
brightYellow: '#f0c674',
brightBlue: '#7fb5e8',
brightMagenta: '#d67bef',
brightCyan: '#5ecbd9',
brightWhite: '#ffffff',
};
</script>
<script>
(function () {
var bridge = window.AndroidSSH;
var term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
// Makes it obvious when the terminal has lost focus to another view.
cursorInactiveStyle: 'outline',
// ui-monospace picks the platform terminal face where the WebView has one
// and falls back to the generic family elsewhere.
fontFamily: 'ui-monospace, monospace',
// Every point costs about 4 columns: 12 gives 67 on a 411dp-wide screen,
// 14 gives 57, 15 only 53. Wrapped lines hurt more than small glyphs do.
fontSize: 12,
letterSpacing: 0,
theme: TERMINAL_THEME,
// SSH servers send CRLF, so lone \n must not be rewritten: doing so
// breaks cursor positioning in full-screen TUIs.
convertEol: false,
scrollback: 10000,
// Lifts unreadable pairings (dark blue on black) to a legible ratio
// without discarding the server's colour choices entirely.
minimumContrastRatio: 4.5,
// Opens up the buffer and parser APIs.
allowProposedApi: true,
});
var fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(document.getElementById('terminal'));

// The DOM renderer repaints every cell as an element, which stutters under
// heavy output (top, cat of a large file). WebGL draws from a glyph atlas
// instead. The context can be lost when Android backgrounds the app or
// reclaims GPU memory, and the addon cannot recover on its own, so drop it
// and let xterm fall back to the DOM renderer.
try {
var webgl = new WebglAddon.WebglAddon();
webgl.onContextLoss(function () {
webgl.dispose();
});
term.loadAddon(webgl);
} catch (e) {
// No usable WebGL (emulator, blocklisted driver) — the DOM renderer stays.
}

function applyFit() {
try {
fit.fit();
} catch (e) {
// Layout not ready yet
return;
}
if (bridge && bridge.onResize) {
bridge.onResize(term.cols, term.rows);
}
}

// Forward keystrokes to native side
term.onData(function (data) {
if (bridge && bridge.onInput) {
bridge.onInput(data);
}
});

// Resize on viewport changes
window.addEventListener('resize', applyFit);

// When the keyboard opens, the native side pads the layout and the WebView
// itself gets shorter. That does not always surface as a window resize event,
// so watch the container directly. The callback runs before the .xterm child
// has been laid out at the new height, so the fit is deferred by a frame or
// it would measure the old size and keep the old row count.
if (window.ResizeObserver) {
new ResizeObserver(function () {
requestAnimationFrame(applyFit);
}).observe(document.getElementById('terminal'));
}
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', applyFit);
}

// Called from Java to clear the terminal before replaying the scrollback
// when the fragment re-attaches to an existing session.
window.resetTerminal = function () {
term.reset();
};

// Called from Java with base64-encoded UTF-8 bytes from the SSH session.
window.writeFromHost = function (b64) {
try {
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
term.write(bytes);
} catch (e) {
term.write('\r\n[terminal decode error]\r\n');
}
};

// Called from Java to print a status / error line in the terminal.
window.printStatus = function (text) {
term.write('\r\n\x1b[33m' + text + '\x1b[0m\r\n');
};

// Called from Java for the Copy key; returns '' when nothing is selected.
// Deliberately not named getSelection: that would shadow the DOM builtin
// xterm itself relies on.
window.getTerminalSelection = function () {
return term.getSelection();
};

// Called from Java for the Paste key, so pasted text takes the same path as
// typed input (including bracketed paste when the server asked for it).
window.pasteText = function (text) {
term.paste(text);
};

// Gboard otherwise autocorrects and capitalises what is typed into xterm's
// hidden textarea, which silently rewrites commands. These attributes are the
// only way to reach it: the textarea is created by xterm, not by this page,
// and a WebView-level input type does not apply to it.
var helper = document.querySelector('.xterm-helper-textarea');
if (helper) {
helper.setAttribute('autocorrect', 'off');
helper.setAttribute('autocapitalize', 'none');
helper.setAttribute('autocomplete', 'off');
helper.setAttribute('spellcheck', 'false');
}

// Initial fit and signal ready (which triggers SSH connect on the Java side).
setTimeout(function () {
applyFit();
if (bridge && bridge.onReady) {
bridge.onReady(term.cols, term.rows);
}
term.focus();
}, 50);
})();
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions app/src/main/assets/terminal/xterm-addon-fit.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/src/main/assets/terminal/xterm-addon-webgl.js

Large diffs are not rendered by default.

Loading