docs: illustrated LaTeX user guide (docs/guide) with generated screenshots

This commit is contained in:
Andy Dixon
2026-10-11 19:41:31 +01:00
parent ce9c5c6b44
commit 8766846a3b
55 changed files with 1947 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Builds nexus-guide.pdf with LuaLaTeX (three passes: contents, page references, then page overlays settle).
# Needs LuaLaTeX with luaotfload (Debian/Ubuntu: texlive-luatex texlive-latex-extra).
# Screenshots: tools/screenshots.sh (needs xvfb-run, sshd, python3).
OUT ?= build
nexus-guide.pdf: nexus-guide.tex nexus-guide.sty chapters/*.tex img/*
mkdir -p $(OUT)
lualatex -interaction=nonstopmode -halt-on-error -output-directory=$(OUT) nexus-guide.tex > /dev/null || (grep -A8 '^!' $(OUT)/nexus-guide.log; false)
for i in 1 2; do lualatex -interaction=nonstopmode -halt-on-error -output-directory=$(OUT) nexus-guide.tex > /dev/null; done
cp $(OUT)/nexus-guide.pdf .
clean:
rm -rf $(OUT)
+111
View File
@@ -0,0 +1,111 @@
\guidepart{slate}{Extras}{The extras}
{The cheat sheet, the command line, and words you might trip over.}
{\partitem{\iKeyboard}{Keyboard cheat sheet}
\partitem{\iTerminal}{Command-line options}
\partitem{\iBrain}{Glossary}}
% =========================================================================================
\chapter{Keyboard cheat sheet}\label{app:keys}
\chapmeta{print it}{Stick this page next to your monitor}
\newcommand\shortcut[2]{#1 & #2\\}
\newcommand\sheet[3]{%
\begin{tcolorbox}[enhanced, arc=8pt, boxrule=0pt, colback=#1!7, left=10pt, right=10pt, top=8pt, bottom=8pt,
before skip=0pt, after skip=10pt, fontupper=\small]
{\heavy\large\color{#1!85!black}#2}\par\vspace{4pt}
\renewcommand\arraystretch{1.5}\begin{tabularx}{\linewidth}{@{}X>{\raggedleft\arraybackslash}p{36mm}@{}}#3\end{tabularx}
\end{tcolorbox}}
\begin{multicols}{2}
\sheet{accent}{\iRocket\enspace Everywhere}{%
\shortcut{Command palette}{\keys{\mod+K}}
\shortcut{New connection}{\keys{\mod+N}}
\shortcut{Quick Connect}{\keys{\mod+Shift+Q}}
\shortcut{Settings}{\keys{\mod+,}}
\shortcut{Full screen}{\kbd{F11}}}
\sheet{violet}{\iSplit\enspace Tabs \& panes}{%
\shortcut{Close tab}{\keys{\mod+W}}
\shortcut{Next tab}{\keys{\mod+Tab}}
\shortcut{Previous tab}{\keys{\mod+Shift+Tab}}
\shortcut{Go to tab 1–9}{\keys{\mod+1}…\kbd{9}}
\shortcut{Split right}{\keys{\mod+Shift+D}}
\shortcut{Split down}{\keys{\mod+Shift+E}}}
\sheet{teal}{\iWand\enspace Power tools}{%
\shortcut{Broadcast on/off}{\keys{\mod+Shift+B}}
\shortcut{Snippets}{\keys{\mod+Shift+S}}
\shortcut{Record on/off}{\keys{\mod+Shift+R}}}
\sheet{amber}{\iTerminal\enspace In the terminal}{%
\shortcut{Copy selection}{\keys{\mod+C}}
\shortcut{Paste}{\keys{\mod+V}}
\shortcut{Find}{\keys{\mod+F}}
\shortcut{Next / previous match}{\kbd{Enter} / \keys{Shift+Enter}}
\shortcut{Scroll a page}{\keys{Shift+PageUp}}
\shortcut{Top / bottom}{\keys{Shift+Home} / \kbd{End}}
\shortcut{Open link}{\kbd{Ctrl}/\kbd{\ph{E1C4}} + click}}
\sheet{pink}{\iMap\enspace Mouse moves}{%
\shortcut{Select word / line}{double / triple-click}
\shortcut{Extend selection}{\kbd{Shift} + click}
\shortcut{Select several profiles}{\kbd{Ctrl}/\kbd{\ph{E1C4}} + click}
\shortcut{Everything else}{right-click it}}
\end{multicols}
% =========================================================================================
\chapter{Command-line options}\label{app:cli}
\chapmeta{1 minute}{You'll script, demo and test with a few handy flags}
\renewcommand\arraystretch{1.5}
\begin{tabularx}{\linewidth}{@{}>{\leavevmode\ttfamily\color{ink}}p{52mm}X@{}}
\toprule
--data-dir DIR & Use another data folder (portable installs, demos, testing).\\
--theme pro|midnight|light & Start with this theme.\\
--connect NAME & Connect to a saved profile by name at startup. Repeat it for several.\\
--open WINDOW & Open a window at startup: \code{settings}, \code{settings:security}, \code{snippets},
\code{keys}, \code{recordings}, \code{palette}, \code{quick}…\\
--version & Print the version and exit.\\
\bottomrule
\end{tabularx}
\begin{sidequest}[How the screenshots in this guide were made]
Every screenshot here is the real app, driven by \code{docs/guide/tools/screenshots.sh}: it starts throwaway
SSH, IRC, VNC and GitHub-ish servers, fills a demo data folder, and runs Nexus Terminal headless with
\code{--screenshot}, \code{--do} (run a palette command) and \code{--answer} (fill a password prompt). Change the app,
rerun the script, rebuild the guide.
\end{sidequest}
% =========================================================================================
\chapter{Glossary}\label{app:glossary}
\chapmeta{dip in}{Words you might trip over, in one line each}
\begin{multicols}{2}\small
\begin{description}[leftmargin=0pt, itemsep=5pt]
\item[Agent] A background program holding your unlocked SSH keys, so you type the passphrase once.
\item[Asciicast] The file format for terminal recordings. Plays in Nexus Terminal and \code{asciinema}.
\item[Bastion / jump host] A server you go \emph{through} to reach the ones behind it.
\item[Baud rate] How fast a serial line talks. Both ends must agree.
\item[Broadcast] Typing into several sessions at once.
\item[DTR / RTS] Serial control lines. Some boards use them for reset or flashing.
\item[Fingerprint] A short summary of a server's host key, used to spot imposters.
\item[Host key] A server's identity. Changes when it's rebuilt, or when someone's pretending.
\item[Keychain] Your operating system's password store.
\item[Port forward] A tunnel carrying one TCP port through SSH.
\item[Profile] A saved connection.
\item[RAW TCP] A plain socket: bytes in, bytes out, no terminal rules.
\item[SFTP] File transfer over SSH.
\item[Snippet] A saved command, optionally with blanks to fill in.
\item[TOTP] Time-based one-time passwords: the six digits from your authenticator app.
\item[Trigger] A rule that watches output and reacts to a pattern.
\item[Vault] Nexus Terminal's encrypted password store, for when there's no keychain.
\item[VNC] A way of seeing and controlling another computer's desktop.
\end{description}
\end{multicols}
\vfill
\begin{center}
\includegraphics[width=16mm]{img/logo.pdf}\par\vspace{6pt}
{\heavy\color{ink}That's the lot.}\par
{\small\color{muted}Go and connect to something. \iRocket}
\end{center}
+248
View File
@@ -0,0 +1,248 @@
\guidepart{accent}{Get going}{Get going}
{From zero to connected before your tea goes cold.}
{\partitem{\iRocket}{Five-minute start}
\partitem{\iMap}{The grand tour}
\partitem{\iList}{Profiles: your address book}}
% =========================================================================================
\chapter{Five-minute start}\label{ch:start}
\chapmeta{5 minutes}{You'll be connected to something and know the one shortcut that matters}
\begin{tldr}
Install it, open it, press \keys{\mod+N}, fill in a host, hit \ui{Create \& Connect}. Done.
Lost? Press \keys{\mod+K} and type what you want.
\end{tldr}
\section{Install it}
Grab the zip for your machine from \code{dixon.cx/downloads/nexus}. They're named
\code{NexusTerminal-<os>-<arch>.zip}, so a modern Mac wants \code{darwin-arm64} and most PCs want \code{amd64}.
\begin{description}
\item[\iDesktop\enspace Windows] Unzip it and run \code{NexusTerminal-windows-amd64.exe}. No installer, no admin rights,
nothing in the registry. Pop it somewhere sensible like \code{C:\textbackslash Tools} and pin it to the taskbar.
\item[\iDesktop\enspace macOS] Unzip it and drag \code{NexusTerminal-darwin-arm64.app} into \ui{Applications}. If macOS
grumbles that it can't check the developer, right-click the app, choose \ui{Open}, then \ui{Open} again. You only
have to do that once.
\item[\iDesktop\enspace Linux] Unzip it and run \code{./install.sh}. It copies \code{nexus-terminal} into
\code{~/.local/bin} and adds a launcher entry with the icon, so it turns up in your app menu.
(Make sure \code{~/.local/bin} is on your \code{PATH}.)
\end{description}
\begin{sidequest}[Build it yourself]
It's a single Rust program. With Rust installed: \code{cd rust \&\& cargo run --release}. The finished binary lands in
\code{rust/target/release/nexus-terminal}. The build guide in \code{docs/build-and-release.md} has the full story.
\end{sidequest}
\section{Open it}
The first time you open Nexus Terminal you get a calm, empty welcome screen. Three buttons, four shortcuts. That's it.
\shot{welcome.png}{The welcome screen. Your saved connections will live down the left; here there are a few already.}
\begin{headsup}[Coming from Alchementary Link?]
Nexus Terminal spots your old data on first run and copies your profiles, vault and recordings across
automatically. You'll see a little toast saying so. Your old data is left exactly where it was.
The details are in \hyperref[ch:move]{Moving house} on p.\,\pageref{ch:move}.
\end{headsup}
\section{Make your first connection}
\begin{enumerate}
\item Click \ui{New Connection}, or press \keys{\mod+N}.
\item Pick the protocol along the top: \proto[teal]{SSH}, \proto[amber]{Telnet}, \proto[violet]{RAW TCP},
\proto[slate]{GitHub}, \proto[rose]{VNC}, \proto[amber]{IRC} or \proto[accent]{Serial}.
\item Fill in the \ui{General} bit: a \ui{Display Name} you'll recognise, the \ui{Host}, the \ui{Port}
and your \ui{Username}.
\item Click \ui{Create \& Connect}.
\end{enumerate}
That's genuinely it. A tab opens, you log in, and next time it's one click in the sidebar.
\crop{editor-new.png}{1515}{40}{0}{70}{0.52\linewidth}{A brand-new SSH profile. Only the top few fields are needed to get going.}
\begin{quickwin}
Click \ui{Test} before you save. Nexus Terminal checks the host and port are reachable and tells you the
round-trip time. If that fails, no amount of password-fiddling will help, so it's a great first check.
\end{quickwin}
\section{Two shortcuts for impatient people}
\subsection{Quick Connect}
Press \keys{\mod+Shift+Q}.
For a one-off session you don't want to save. Pick SSH, Telnet, RAW, IRC or Serial, type the host (or serial
device), hit \kbd{Enter}. Nothing gets saved.
\crop{quick.png}{951}{620}{946}{481}{0.5\linewidth}{Quick Connect. In, out, nobody gets hurt.}
\subsection{Local terminal}
Click \ui{Local Machine} at the top of the sidebar (or \ui{Local} on the welcome screen) for a shell on
\emph{this} computer. When you type \code{exit}, the tab says \code{[Exited - press enter to restart]}.
Press \kbd{Enter} and you've got a fresh shell in the same tab.
\section{The one shortcut to rule them all}
\begin{center}
\begin{tcolorbox}[enhanced, width=0.8\linewidth, arc=10pt, boxrule=0pt, colback=ink, fontupper=\color{white},
halign=center, top=10pt, bottom=10pt, drop fuzzy shadow]
{\scalebox{2.4}{\keys{\mod+K}}}\par\vspace{6pt}\par\vspace{4pt}
{\color{white!70!ink} The command palette. If you remember nothing else, remember this.}
\end{tcolorbox}
\end{center}
Type a few letters of \emph{anything}: a connection name, a tab, ``theme'', ``split'', ``lock'', ``snippets''. Use
\kbd{↑}\kbd{↓} to pick, \kbd{Enter} to go, \kbd{Esc} to bail. It's fuzzy, so ``atl'' finds \emph{atlas} and
``sft'' finds the \emph{SFTP Browser}.
\crop{palette.png}{755}{732}{733}{154}{0.72\linewidth}{The command palette: every connection, tab and action in one searchable list.}
\begin{quickwin}[Your five-minute checklist]
\begin{checklist}
\item Installed and opened Nexus Terminal
\item Opened a \ui{Local Machine} tab and typed \code{exit}, then \kbd{Enter} to get it back
\item Made a profile with \keys{\mod+N} and clicked \ui{Test}
\item Connected to it
\item Opened the palette with \keys{\mod+K} and found something by typing three letters
\end{checklist}
All ticked? You're officially more productive than you were five minutes ago. \iConfetti
\end{quickwin}
% =========================================================================================
\chapter{The grand tour}\label{ch:tour}
\chapmeta{4 minutes}{You'll know what every bit of the window is for}
\begin{tldr}
Connections on the left. Sessions in tabs across the top. The terminal in the middle. Status along the bottom.
Everything else is one \keys{\mod+K} away.
\end{tldr}
\begin{annotated}{ssh.png}
\callout{1}{0.124}{0.022}
\callout{2}{0.66}{0.022}
\callout{3}{0.888}{0.022}
\callout{4}{0.22}{0.067}
\callout{5}{0.03}{0.152}
\callout{6}{0.53}{0.063}
\callout{7}{0.966}{0.063}
\callout{8}{0.62}{0.42}
\callout{9}{0.16}{0.945}
\callout{10}{0.26}{0.985}
\callout{11}{0.86}{0.985}
\end{annotated}
\begin{multicols}{2}\small
\begin{description}[leftmargin=0pt, labelsep=0.4em]
\item[\num{1} Menus] \menu{File}, \menu{Session}, \menu{View} and \menu{Help}. Drag the title bar to move the
window; double-click it to maximise.
\item[\num{2} Palette] The search box is the command palette. Click it or press \keys{\mod+K}.
\item[\num{3} Light/dark] Flip between the dark and light themes in one click.
\item[\num{4} Search] Fuzzy-search your connections by name, host, tag or protocol.
\item[\num{5} Sections] \ui{Local Machine}, \ui{Favourites} \iStar, \ui{Recent} (your last five), then one section
per group. Click a header to fold it away.
\item[\num{6} Tabs] One per session, with a coloured state dot, protocol badge and close button. SSH tabs get a
little \iFolder{} for files. A blue dot means ``new output while you weren't looking''.
\item[\num{7} New \& broadcast] \ui{+} opens a new connection; \iBroadcast{} toggles broadcast mode.
\item[\num{8} Workspace] Your terminal (or IRC chat, VNC desktop, GitHub list…). Split it into panes, too.
\item[\num{9} Footer] New connection, palette, import, export, recordings and settings.
\item[\num{10} Status] State, protocol and live latency for SSH. Shows \ui{REC} while recording and
\ui{BROADCAST} when broadcasting.
\item[\num{11} Sessions \& vault] How many sessions are connected and whether the vault is locked. Click the vault to
open security settings.
\end{description}
\end{multicols}
\begin{protip}
Drag the right-hand edge of the sidebar to make it wider or narrower. Toasts (the little pop-up messages in the
top-right) tidy themselves away after a few seconds; click \ui{×} if you're impatient.
\end{protip}
\section{Three looks}
Nexus Terminal comes in \ui{Pro} (the default, a soft charcoal), \ui{Midnight} (darker, bluer, for 2\,a.m.
deploys) and \ui{Light} (for sunny days and projectors). Switch with the \iBulb{} button in the title bar, the
\menu{View} menu, or type ``theme'' into the palette.
\shotpair{welcome-midnight.png}{Midnight}{ssh-light.png}{Light, with a session open}
The \emph{terminal} has its own colour schemes too: eleven of them, including \ui{Dracula}, \ui{Nord},
\ui{Tokyo Night}, \ui{Catppuccin Mocha} and \ui{Rosé Pine}. Those live in \menu{Settings\then Terminal} and the \menu{View} menu.
% =========================================================================================
\chapter{Profiles: your address book}\label{ch:profiles}
\chapmeta{6 minutes}{You'll keep dozens of connections tidy without thinking about it}
\begin{tldr}
A \emph{profile} is a saved connection. Give it a name and a group, star the ones you use daily, and
right-click for everything else. Ctrl-click to select lots at once.
\end{tldr}
\section{Create, edit, copy}
\begin{itemize}
\item \textbf{New:} \ui{+} in the sidebar footer or tab strip, \keys{\mod+N}, or right-click a group header and choose
\ui{New connection in …} (it lands in that group automatically).
\item \textbf{Edit:} double-click a profile, or right-click\then\ui{Edit}. The editor slides in on the right.
\item \textbf{Copy:} right-click\then\ui{Duplicate}. Great for ``same as web-01 but web-02''.
\item \textbf{Delete:} right-click\then\ui{Delete}. This also deletes the saved passwords for that profile.
\end{itemize}
\crop{editor-ssh.png}{1515}{40}{0}{70}{0.52\linewidth}{Editing an existing SSH profile. The editor scrolls; sections are grouped so you can ignore most of them.}
\subsection{What goes in a profile}
Every profile has the same \ui{General} section, then sections specific to its protocol (Part 2 covers each
one). The common bits are:
\renewcommand\arraystretch{1.4}
\begin{tabularx}{\linewidth}{@{}>{\leavevmode\heavy\color{ink}}p{32mm}X@{}}
\toprule
Display Name & What you'll see in the sidebar. Make it obvious: ``db-primary'', not ``server 3''.\\
Host / Port & Where to connect. Serial profiles have a \ui{Device} instead.\\
Group & Pick an existing group or type a new name to create one.\\
Description & One line shown on hover.\\
Terminal & Font, size (8–36), scrollback (0–100\,000 lines) and cursor style, just for this profile.\\
Tags \& Notes & Comma-separated tags (searchable!) and free-form notes for future-you.\\
\bottomrule
\end{tabularx}
Each protocol adds its own sections underneath. Here are four of them side by side; Part~2 goes through each one.
\begin{center}
\newcommand\mini[2]{\begin{minipage}[t]{0.24\linewidth}\centering\includegraphics[width=\linewidth, trim=1515 600 0 70, clip]{img/#1}\par\vspace{2pt}{\scriptsize\color{muted}#2}\end{minipage}}
\mini{editor-telnet.png}{Telnet}\hfill\mini{editor-raw.png}{RAW TCP}\hfill\mini{editor-vnc.png}{VNC}\hfill\mini{editor-irc.png}{IRC}
\end{center}
\begin{protip}[Tags are secretly brilliant]
Tag things by where they live and what they do (\code{eu-west}, \code{db}, \code{customer-x}). The sidebar search
matches tags, so typing ``db'' instantly narrows hundreds of profiles to the database servers.
\end{protip}
\section{Favourites, groups and Recent}
\begin{itemize}
\item Right-click\then\ui{Add to Favourites} pins a profile into the \ui{\iStar\ Favourites} section at the top.
\item \ui{Recent} always shows the last five things you connected to. You don't have to do anything.
\item Groups are just the \ui{Group} field. No group? It goes in \ui{Ungrouped}.
\end{itemize}
\section{Do lots at once}
Hold \kbd{Ctrl} (\kbd{\ph{E1C4}} on a Mac) and click several profiles. A bar appears at the bottom of the sidebar:
\begin{center}\small
\begin{tabular}{@{}ll@{\qquad}ll@{}}
\ui{Connect all} & opens them all in tabs & \ui{Move group} & moves them (blank = Ungrouped)\\
\ui{Disconnect} & closes their sessions & \ui{Delete} & deletes them (asks first)\\
\ui{Tags} & add or replace tags & \ui{Clear} & forget the selection\\
\end{tabular}
\end{center}
\begin{quickwin}
Select every web server, choose \ui{Connect all}, then turn on broadcast (\keys{\mod+Shift+B}). You're now typing into
all of them at once. Chapter~\ref{ch:repeat} has the details, and the safety advice.
\end{quickwin}
\section{``It's already open…''}
Click a profile that already has a live session and Nexus Terminal asks whether to \ui{Use Existing} (jump to that
tab), open a \ui{New Connection} alongside it, or \ui{Cancel}. No more accidental duplicate sessions.
+273
View File
@@ -0,0 +1,273 @@
\guidepart{teal}{Connect}{Connect to all the things}
{Eight ways in. Pick the one you need; skip the rest.}
{\partitem{\iTerminal}{SSH: the big one}
\partitem{\iUsb}{Serial: cables and consoles}
\partitem{\iPlug}{Telnet and RAW TCP}
\partitem{\iDesktop}{VNC remote desktops}
\partitem{\iChat}{IRC}
\partitem{\iGithub}{GitHub}}
% =========================================================================================
\chapter{SSH: the big one}\label{ch:ssh}
\chapmeta{10 minutes (or 2 for the basics)}{You'll log in with keys, hop through bastions and never type a 2FA code again}
\begin{tldr}
Host, username, key, done. Everything else (jump hosts, proxies, forwards, 2FA, login scripts) is optional and
lives in its own fold-away section of the editor. Open only the ones you need.
\end{tldr}
\begin{glance}{teal}{SSH}
Default port & 22\\
Logs in with & Password, public key, keyboard-interactive, SSH agent\\
Extras & SFTP file browser, port forwards, live latency, agent forwarding, TOTP 2FA\\
Good for & Servers, VMs, Raspberry Pis, anything with \code{sshd}\\
\end{glance}
\section{The two-minute version}
\begin{enumerate}
\item \keys{\mod+N}, leave the protocol on \proto[teal]{SSH}.
\item \ui{Host}, \ui{Username}. Leave the port on 22 unless you know otherwise.
\item Under \ui{Key \& Passphrase}, pick your key from the suggestions (Nexus Terminal lists the keys in your
\code{\textasciitilde/.ssh} folder) and check for a little green \ui{Key verified}.
\item \ui{Create \& Connect}. The first time, you'll be asked to trust the server's host key. Say yes if you
were expecting it.
\end{enumerate}
\crop{hostkey.png}{842}{720}{845}{589}{0.66\linewidth}{First time connecting? You'll be shown the server's fingerprint. \ui{Accept \& Save} if it's the server you meant.}
\begin{headsup}
Type the full path to a key if you're not using the suggestions. Nexus Terminal doesn't expand \code{\textasciitilde}
in that box, so write \code{/home/you/.ssh/id\_ed25519} (or \code{C:\textbackslash Users\textbackslash you\textbackslash .ssh\textbackslash id\_ed25519}).
\end{headsup}
\section{How it logs in}
Under \ui{Authentication}, tick which \ui{Auth Methods} to try: \ui{Public key}, \ui{Password} and
\ui{Keyboard-interactive}. Under \ui{SSH Agent}, tick \ui{Use SSH Agent for Authentication} to use keys already
loaded into \code{ssh-agent} (or Pageant, or the Windows OpenSSH agent).
Every password and passphrase has a \emph{mode}: save it securely, remember it until you quit, or ask every time.
That's all explained in \hyperref[ch:secrets]{Passwords \& the vault} (p.\,\pageref{ch:secrets}).
\begin{protip}[Agent forwarding]
Tick \ui{Forward Agent to Remote} (it's \code{ssh -A}) and you can \code{git pull} or hop onwards from the server using
the keys on \emph{your} machine. Only do it on servers you trust: anyone with root there can use your agent while
you're connected.
\end{protip}
\section{Jump hosts: getting through the bastion}
Lots of servers hide behind a bastion. Add one or more hops under \ui{Jump Hosts}, each with its own user, host,
port and optional key. Nexus Terminal tunnels through them in order, so the chain
\textit{you\then bastion\then internal-box\then db-primary} is just three rows.
\section{Proxies}
Stuck behind a corporate proxy? Under \ui{Proxy}, choose \ui{SOCKS5} or \ui{HTTP CONNECT} and fill in the host,
port and (optional) username and password. The proxy is used to reach the SSH server, or the first jump host if
you have some.
\section{2FA without the phone faff}\label{sec:totp}
If your server asks for a one-time code, paste the Base32 TOTP secret (the text version of the QR code) into
\ui{TOTP Secret} and hit \ui{Save}. From then on, Nexus Terminal answers the ``Verification code:'' prompt itself.
\ui{Test} shows today's code so you can check it matches your phone.
\begin{headsup}
Save the profile first, then save the TOTP secret. And make sure your computer's clock is right: TOTP codes are
time-based, so a clock that's a minute out will produce wrong codes.
\end{headsup}
\section{Do things automatically on login}
\begin{description}
\item[Startup] A \ui{Startup Command} to run once you're in (say, \code{tmux attach}), plus
\ui{Environment Variables} as \code{KEY=VALUE}, one per line.
\item[Login Script] For old-school boxes that ask questions. Add steps: \ui{Wait for} some text, then
\ui{Send} a reply. Repeat as needed.
\end{description}
\section{Keeping it alive}
Under \ui{Advanced}: the \ui{Terminal Type}, the \ui{Keepalive Interval} and \ui{Max Missed Keepalives}, compression,
and \ui{Auto-Reconnect} with a maximum number of attempts (1–20). Reconnection backs off politely: 1\,s, 2\,s,
4\,s… up to 30\,s.
\begin{sidequest}[What's that number in the status bar?]
For SSH sessions the status bar shows live latency, the round trip to the server. Green is snappy. If it's
in the hundreds and your typing feels sticky, it's the network, not you.
\end{sidequest}
% =========================================================================================
\chapter{Serial: cables and consoles}\label{ch:serial}
\chapmeta{4 minutes}{You'll be talking to a switch, router or microcontroller over USB}
\begin{tldr}
Plug in the adapter, pick the device, match the baud rate. 115200 8N1 is the default and usually right.
Right-click the tab to send a break or wiggle DTR/RTS.
\end{tldr}
\begin{glance}{accent}{Serial}
Device & \code{/dev/ttyUSB0}, \code{/dev/cu.usbserial-…} or \code{COM3}\\
Defaults & 115200 baud, 8 data bits, no parity, 1 stop bit, no flow control, CR line endings, DTR and RTS on\\
Extras & Send Break, toggle DTR/RTS, auto-reconnect when the cable comes back\\
Good for & Network gear consoles, Arduinos, ESP32s, Raspberry Pi UARTs, 3D printers\\
\end{glance}
\crop{editor-serial.png}{1515}{40}{0}{70}{0.52\linewidth}{A serial profile for a switch console at 9600 baud. Click refresh after plugging in an adapter.}
\section{Setting it up}
\begin{description}
\item[Device] Pick it from the list (hit refresh after plugging in) or type the path.
\item[Line Settings] \ui{Baud Rate} (a preset from 300 to 1\,000\,000, or \ui{Custom}), \ui{Data Bits} (5–8),
\ui{Parity} (none, odd, even), \ui{Stop Bits} (1 or 2) and \ui{Flow Control} (none, XON/XOFF, RTS/CTS).
\item[Behaviour] \ui{Line Ending}, \ui{Encoding}, \ui{Local Echo} (for devices that don't echo what you type),
\ui{Assert DTR on connect}, \ui{Assert RTS on connect} and \ui{Auto-reconnect}.
\end{description}
\section{While you're connected}
Right-click the tab for \ui{Send Break} (handy for getting into a router's ROMMON) and to toggle \ui{DTR} and
\ui{RTS} (handy for resetting boards that wire those to reset). Pull the cable out and the session disconnects; with
auto-reconnect on, it picks up again when you plug it back in.
Serial tabs are proper terminals, so recording, triggers, broadcast, snippets, find and split panes all work.
\begin{headsup}[Gibberish on screen?]
Garbled characters nearly always mean the baud rate, parity or stop bits don't match the device. Check the
device's manual. 9600 and 115200 are the usual suspects.
\end{headsup}
\begin{protip}[Linux and macOS quirks]
On Linux, add yourself to the \code{dialout} group (\code{uucp} on some distributions) and log out and in again.
On a Mac, use the \code{/dev/cu.*} device, not \code{/dev/tty.*}.
\end{protip}
% =========================================================================================
\chapter{Telnet and RAW TCP}\label{ch:raw}
\chapmeta{4 minutes}{You'll poke at old kit and raw sockets without reaching for netcat}
\begin{tldr}
Telnet is a terminal for old devices. RAW is a packet-level conversation with any TCP port, with a hex view.
\end{tldr}
\section{Telnet}
\begin{glance}{amber}{Telnet}
Default port & 23\\
Options & Local echo, line mode, terminal type, encoding (UTF-8, ASCII, ISO 8859-1), line endings, login script\\
Extras & Send Break from the tab menu, \ui{Debug Negotiation} for when options go wrong\\
\end{glance}
\begin{danger}[Telnet is plain text]
Everything, including your password, crosses the network unencrypted. Fine on a lab bench, not fine over the
internet or café Wi-Fi.
\end{danger}
\section{RAW TCP}
RAW connects to any TCP port and shows you exactly what goes back and forth. Instead of a terminal you get a
traffic log with \ui{Text}, \ui{Hex} and \ui{Split} views.
\shot{raw.png}{A RAW session to a chatty sensor, with timestamps and direction markers turned on.}
\begin{itemize}
\item \textbf{Send bar:} pick \ui{Text}, \ui{Hex} or \ui{Escaped} (\code{\textbackslash r\textbackslash n} and friends),
type, press \kbd{Enter}. One-click buttons send \ui{NUL}, \ui{CR}, \ui{LF}, \ui{ETX} and \ui{EOT}.
\item \textbf{Presets:} save payloads you send often in the profile under \ui{Payload Presets}, then pick them from
\ui{Presets…}.
\item \textbf{Capture:} \ui{Pause}, \ui{Clear}, and \ui{Save} (writes the whole conversation as JSON).
\item \textbf{Profile options:} encoding (UTF-8, ASCII, Hex), line ending, timestamps, direction, timeouts and
auto-reconnect.
\end{itemize}
\begin{quickwin}
Point a RAW profile at a web server on port 80 and send \code{HEAD / HTTP/1.0\textbackslash r\textbackslash n\textbackslash r\textbackslash n}
in \ui{Escaped} mode. Instant HTTP headers, no browser required.
\end{quickwin}
% =========================================================================================
\chapter{VNC remote desktops}\label{ch:vnc}
\chapmeta{4 minutes}{You'll see and drive a remote desktop, safely}
\begin{tldr}
VNC shows you another computer's screen. Tunnel it through SSH (tick a box), because plain VNC isn't encrypted.
\end{tldr}
\begin{glance}{rose}{VNC}
Default port & 5900 (plus the display number: display \code{:1} is 5901)\\
Display & \ui{View Only}, \ui{Scale to Fit}, \ui{Shared Session}, \ui{Quality (0-9)}\\
Security & Optional SSH tunnel with password, key or agent login\\
\end{glance}
\shot{vnc.png}{A remote desktop, scaled to fit the tab.}
\section{The toolbar}
Connection state and desktop size on the left. On the right: switch between scaled and 1:1, refresh,
\ui{Paste Clipboard} (sends your clipboard to the remote machine) and \ui{Ctrl+Alt+Del}. Click the desktop to give
it your keyboard. Anything the remote machine copies lands on your clipboard automatically.
\section{Tunnel it}
Tick \ui{Enable SSH Tunnel} and fill in the SSH host, port, username and login method. The VNC traffic then
travels inside an encrypted SSH connection. If you connect to a VNC profile \emph{without} a tunnel, Nexus Terminal
warns you and offers \ui{Connect Anyway}, \ui{Edit Profile} or \ui{Cancel}.
\begin{headsup}[Connecting to a Mac?]
Turn on \ui{VNC viewers may control screen with password} in macOS Screen Sharing settings. Nexus Terminal uses
standard VNC password authentication, not Apple's own login method.
\end{headsup}
% =========================================================================================
\chapter{IRC}\label{ch:irc}
\chapmeta{3 minutes}{You'll be chatting in your project's channels from the same window as your servers}
\begin{tldr}
Nick, server, rooms to join. Tabs for each room, \code{/commands} as you'd expect, \kbd{Tab} completes nicks.
\end{tldr}
\begin{glance}{amber}{IRC}
Default port & 6667, or 6697 with \ui{Use TLS} (use TLS!)\\
Extras & NickServ identify on connect, auto-join rooms, room browser, SSH tunnel\\
\end{glance}
\shot{irc.png}{Chatting in \#nexus. Rooms down the right, users in the room below them.}
\begin{itemize}
\item A \ui{Server} tab plus one per room or private conversation. A dot means unread messages.
\item Commands: \code{/join}, \code{/part}, \code{/msg}, \code{/me}, \code{/notice}, \code{/nick}, \code{/topic},
\code{/list}, \code{/names}, \code{/whois}, \code{/who}, \code{/raw}, \code{/quit} and \code{/help}.
\item \kbd{Tab} completes a nick; click a nick to mention them.
\item The side panel lists your rooms, a room browser with a filter (click to join) and who's in the current room.
\end{itemize}
% =========================================================================================
\chapter{GitHub}\label{ch:github}
\chapmeta{2 minutes}{You'll see all your repositories, freshest first}
\crop{editor-github.png}{1515}{780}{0}{70}{0.5\linewidth}{A GitHub profile: just the API address and a token.}
\begin{tldr}
Paste a personal access token with the \code{repo} scope. Get a tidy, filterable list of every repository you can see.
\end{tldr}
\shot{github.png}{Repositories grouped by owner, with the latest commit and who made it.}
\begin{itemize}
\item Grouped by owner or organisation, sorted by most recent activity.
\item Badges for \proto[rose]{Private}, \proto[violet]{Fork} and \proto[amber]{Archived}, plus language and stars.
\item Filter box, \ui{Refresh now}, \ui{Visit} (opens in your browser) and \ui{ZIP} (downloads an archive).
\item Works with GitHub Enterprise: set \ui{API Base URL} to your server's API address. Refreshes every 60–3600 seconds.
\end{itemize}
% =========================================================================================
\chapter{Your own computer}\label{ch:local}
\chapmeta{1 minute}{You'll stop alt-tabbing to a separate terminal app}
Click \ui{Local Machine} for a shell on this computer. It uses your system's default shell unless you choose
another in \menu{Settings\then General\then Local shell} (\code{pwsh}, \code{zsh}, \code{fish}, whatever you fancy).
\shot[0.86\linewidth]{local.png}{A local shell, with all the same terminal powers as SSH.}
+234
View File
@@ -0,0 +1,234 @@
\guidepart{violet}{Work faster}{Work faster}
{Fewer clicks, less typing, more getting on with it.}
{\partitem{\iSplit}{Tabs and split panes}
\partitem{\iKeyboard}{Terminal superpowers}
\partitem{\iWand}{Never type it twice}
\partitem{\iFolder}{Files: SFTP and drag-and-drop}
\partitem{\iArrows}{Port forwards}
\partitem{\iFilm}{Recordings}}
% =========================================================================================
\chapter{Tabs and split panes}\label{ch:tabs}
\chapmeta{4 minutes}{You'll juggle a dozen sessions without losing any of them}
\begin{tldr}
Right-click a tab for everything. \keys{\mod+Shift+D} splits right, \keys{\mod+Shift+E} splits down.
\keys{\mod+1} to \keys{\mod+9} jump straight to a tab.
\end{tldr}
\section{Tabs}
\begin{description}
\item[Move] Drag tabs about, or right-click\then\ui{Move Left} / \ui{Move Right}.
\item[Pin] Right-click\then\ui{Pin}. Pinned tabs stay at the front.
\item[Rename] Right-click\then\ui{Rename}. ``prod DB, DON'T TOUCH'' is a perfectly valid tab name.
\item[Duplicate] Opens another session to the same profile.
\item[Reconnect] Does what it says.
\item[Peek] Hover a tab for its name, host, state and when it connected.
\item[Close] The \ui{×}, \keys{\mod+W}, or right-click\then\ui{Close}. Connected tabs ask first (you can turn that
off in Settings).
\item[Switch] \keys{\mod+Tab} and \keys{\mod+Shift+Tab}, or \keys{\mod+1} to \keys{\mod+9}.
\end{description}
Lots of tabs? Scroll the tab strip with your mouse wheel or trackpad.
\section{Split panes}
Want two sessions side by side? Split the focused pane right with \keys{\mod+Shift+D} or down with
\keys{\mod+Shift+E} (also in the \menu{Session} menu, the right-click menu and the palette). The new pane asks
what to open: \ui{Local Machine} or any of your terminal profiles.
\shot{split.png}{A fresh split. Pick what goes in the new pane.}
Drag a divider to resize. Click \ui{×} on a pane's header to close it and its neighbour takes the space. Closing
the tab closes every pane in it.
\begin{quickwin}
Split right, open \code{htop} on one side and tail a log on the other. Now you look like a hacker in a film.
\end{quickwin}
% =========================================================================================
\chapter{Terminal superpowers}\label{ch:terminal}
\chapmeta{4 minutes}{You'll copy, search and scroll like you've used it for years}
\begin{tldr}
Select to copy, \keys{\mod+F} to search, Ctrl/\ph{E1C4}-click links. Right-click for the rest.
\end{tldr}
\section{Keyboard and mouse}
\renewcommand\arraystretch{1.45}
\begin{tabularx}{\linewidth}{@{}>{\leavevmode\heavy\color{ink}}p{24mm}X@{}}
\toprule
Copy & Select text, then \keys{\mod+C}. Nothing selected? \keys{Ctrl+C} interrupts the program as usual.\\
Paste & \keys{\mod+V}. Pastes are bracketed when the program asks for it, so editors won't auto-indent your paste
into a staircase.\\
Select & Drag for a range, double-click a word, triple-click a line, \kbd{Shift}-click to extend.\\
Links & Hold \kbd{Ctrl} (\kbd{\ph{E1C4}}) and click a URL to open it in your browser.\\
Scroll & Mouse wheel, or \keys{Shift+PageUp}, \keys{Shift+PageDown}, \keys{Shift+Home}, \keys{Shift+End}.\\
\bottomrule
\end{tabularx}
\begin{sidequest}[Why does the scroll wheel move the cursor in less?]
Full-screen programs (\code{less}, \code{vim}, \code{tmux}, \code{byobu}) use the terminal's ``alternate screen'', where the
wheel sends arrow keys instead of scrolling. Programs with mouse support (\code{htop}, \code{vim} with
\code{mouse=a}, \code{tmux} mouse mode) get your clicks directly. That's normal terminal behaviour, not a bug.
\end{sidequest}
\section{The right-click menu}
\ui{Copy}, \ui{Paste}, \ui{Select All}, \ui{Find}, \ui{Send Snippet}, \ui{Port Forwards} (SSH), \ui{Split Right},
\ui{Split Down}, \ui{Export Output} (saves the whole scrollback as a text file), \ui{Clear} and \ui{Reset}.
\section{Find}
\keys{\mod+F} opens the find bar. Toggle \ui{Aa} for case-sensitive and \ui{.*} for regular expressions.
\kbd{Enter} for the next match, \keys{Shift+Enter} for the previous one, \kbd{Esc} to close. It shows
``3 of 17'' so you know where you are.
\section{When the connection drops}
The pane dims and shows a \ui{Disconnected} card with \ui{Reconnect} and \ui{Close}. If auto-reconnect is on,
it says \ui{Reconnecting…} with a \ui{Cancel} button, and a toast tells you which attempt it's on.
If the problem was a password or host key, you get a friendly dialog explaining what happened, with
\ui{Retry}, \ui{Edit Profile}, \ui{Known Hosts} (for host-key trouble) and \ui{Close}.
% =========================================================================================
\chapter{Never type it twice}\label{ch:repeat}
\chapmeta{6 minutes}{You'll stop retyping the same commands, and let Nexus Terminal watch the logs for you}
\begin{tldr}
\textbf{Snippets} store commands. \textbf{Broadcast} types into many sessions at once. \textbf{Triggers} watch output and
tap you on the shoulder.
\end{tldr}
\section{Snippets: \keys{\mod+Shift+S}}
Save commands you use a lot, with a name, description and folder. Search them, click
\ui{Send to Session}, and the command is typed into the active session followed by \kbd{Enter}.
\crop{snippets.png}{633}{384}{611}{402}{0.8\linewidth}{The snippet library, organised into folders.}
Snippets can have blanks to fill in:
\begin{itemize}
\item \code{\{\{hostname\}\}}, \code{\{\{date\}\}}, \code{\{\{time\}\}} and \code{\{\{timestamp\}\}} are filled in for you.
\item Anything else, like \code{\{\{service\}\}}, is asked for when you send it.
\end{itemize}
So \code{journalctl -fu \{\{service\}\}} asks ``service?'' and tails whichever one you type.
\section{Broadcast: \keys{\mod+Shift+B}}
Turn broadcast on and everything you type goes to \emph{every} connected terminal. The status bar shows
\ui{BROADCAST} so you can't forget it's on.
\shot{broadcast.png}{Broadcast is on: the status bar says so in no uncertain terms.}
To broadcast to only some sessions, right-click the \iBroadcast{} button (or \menu{Session\then Broadcast
targets…}), tick the ones you want and click \ui{Apply \& Enable}.
\begin{danger}[With great power…]
Broadcast doesn't care which tab you \emph{meant}. Double-check the target list before you type anything
destructive. \code{sudo reboot} into twenty servers is very efficient and very memorable.
\end{danger}
\section{Triggers: let it watch for you}
\menu{Session\then Trigger Rules} (or the palette). Each rule has a pattern (plain text or a regular
expression), a case-sensitivity switch, an on/off switch, and an action:
\begin{center}\small
\begin{tabular}{@{}ll@{}}
\ui{Notify} & shows a toast\\
\ui{Highlight tab} & briefly outlines the pane\\
\ui{Notify + Highlight} & both\\
\ui{Sound} & asks your OS for attention (a bouncing dock icon or flashing taskbar button) and shows a toast\\
\end{tabular}
\end{center}
\crop{triggers.png}{725}{610}{725}{625}{0.75\linewidth}{Three rules: shout about errors, ping when a build finishes, and one switched off for now.}
Rules check each complete line of output, with colours stripped out first.
\begin{quickwin}
Add a rule for \code{BUILD SUCCESSFUL} with the \ui{Sound} action, start a long build, and go and make a cup of tea.
Nexus Terminal will tell you when it's done. (Or \code{(?i)error|fatal} for the opposite.)
\end{quickwin}
% =========================================================================================
\chapter{Files: SFTP and drag-and-drop}\label{ch:files}
\chapmeta{4 minutes}{You'll move files to and from servers without leaving the terminal}
\begin{tldr}
Click the \iFolder{} on an SSH tab. Or drop a file onto the terminal and choose \ui{Upload}.
\end{tldr}
\section{The SFTP browser}
Click the \iFolder{} folder icon on an SSH tab, \menu{Session\then SFTP browser}, or type ``sftp'' in the palette.
It opens in the side dock next to your terminal.
\shot{sftp.png}{Browsing the server's files next to the terminal.}
\begin{itemize}
\item Move around with the breadcrumb trail, the \code{..} entry or \ui{Go to path}.
\item Upload files; download one or many to a folder you choose.
\item Rename, delete (it asks first), make folders, refresh.
\item \iStar{} bookmarks the current folder, per profile.
\item Ctrl-click (\ph{E1C4}-click) or Shift-click to select several things. Transfers show progress as they go.
\end{itemize}
\section{Drag and drop}
Drop files onto a terminal and choose:
\begin{description}
\item[Upload to current directory] Pick the remote folder (SSH only).
\item[Paste contents] Types the file's contents into the terminal, as if you'd typed it.
\end{description}
The upload picker looks like the SFTP browser but shows folders only, labelled \proto[amber]{READ ONLY} if you can't
write there and \proto[rose]{NO READ} if you can't look inside. Keyboard fans: \kbd{↑}\kbd{↓} to move, \kbd{Enter} to
choose, \kbd{Backspace} to go up, \kbd{Esc} to cancel. If the SFTP browser is open, the picker starts in the same folder.
% =========================================================================================
\chapter{Port forwards}\label{ch:forwards}
\chapmeta{3 minutes}{You'll reach a database or web app that only listens on the server}
\begin{tldr}
\emph{Local} forwards bring a remote port to your machine. \emph{Remote} forwards do the opposite.
\end{tldr}
Forwards saved in an SSH profile (under \ui{Port Forwarding}, with \ui{Active} ticked) start when you connect.
To manage them on a live session, right-click the terminal\then\ui{Port Forwards}, or \menu{Session\then Port forwards}.
\begin{description}
\item[Local \iArrow] Listen on your computer, forward to somewhere the server can reach.
Example: \code{127.0.0.1:15432}\then\code{127.0.0.1:5432} lets your desktop database app talk to Postgres on
the server as if it were local.
\item[Remote \ph{E058}] Ask the server to listen and send connections back to somewhere \emph{your} computer can reach.
Great for showing a colleague the dev server running on your laptop.
\end{description}
Active forwards are listed with an \proto[accent]{L} or \proto[violet]{R} badge and a stop button. Toasts tell you when one
starts, stops or fails.
% =========================================================================================
\chapter{Recordings}\label{ch:recordings}
\chapmeta{2 minutes}{You'll have a replayable record of exactly what happened}
\keys{\mod+Shift+R} starts and stops recording the active session. A red dot appears on the tab and \ui{REC} in
the status bar. Recordings are standard asciicast v2 files, so other tools (like \code{asciinema}) can play them too.
\crop{recordings.png}{712}{392}{690}{415}{0.75\linewidth}{The recordings browser. Play, delete, or tidy up in bulk.}
The browser (sidebar footer, \menu{Session} menu or palette) shows how many recordings you have and how much space
they take. Play or delete each one, or use \ui{Clean up} to remove recordings older than 7, 30 or 90 days, or to keep
the total under 1\,GB or 5\,GB. Playback has play/pause, restart, a seek bar and speed control.
\begin{protip}
Record the next time you do something fiddly and rare (a database failover, a certificate rotation). Six months
later, ``how did we do that again?'' has an answer you can literally watch.
\end{protip}
+123
View File
@@ -0,0 +1,123 @@
\guidepart{amber}{Stay safe}{Stay safe}
{Passwords, keys and host checks, minus the jargon headache.}
{\partitem{\iLock}{Passwords and the vault}
\partitem{\iShield}{Host keys: is it really that server?}
\partitem{\iKey}{SSH keys}}
% =========================================================================================
\chapter{Passwords and the vault}\label{ch:secrets}
\chapmeta{5 minutes}{You'll know exactly where your passwords live and who can read them}
\begin{tldr}
Passwords go in your OS keychain. If there isn't one, they go in an encrypted vault protected by a master password.
For each password you choose: save it, remember it until you quit, or ask every time.
\end{tldr}
\section{Where secrets live}
\begin{center}
\begin{tikzpicture}[
box/.style={rounded corners=6pt, fill=#1!12, draw=#1!60, line width=0.6pt, minimum height=13mm, text width=40mm,
align=center, font=\small},
arr/.style={-latex, line width=0.8pt, color=muted}]
\node[box=accent] (need) {\heavy A session needs\\a password};
\node[box=green, right=12mm of need] (kc) {\heavy OS keychain\\{\footnotesize Keychain · Credential Manager · Secret Service}};
\node[box=amber, right=12mm of kc] (vault) {\heavy Encrypted vault\\{\footnotesize AES-256-GCM, Argon2id}};
\node[box=rose, below=9mm of kc] (ask) {\heavy Ask you\\{\footnotesize then carry on}};
\draw[arr] (need) -- (kc);
\draw[arr] (kc) -- node[above, font=\scriptsize]{not there?} (vault);
\draw[arr] (vault) |- node[pos=0.25, right, font=\scriptsize]{still not there?} (ask);
\end{tikzpicture}
\end{center}
\begin{description}
\item[OS keychain first] macOS Keychain, Windows Credential Manager, or the Linux Secret Service (GNOME Keyring,
KWallet and friends).
\item[Encrypted vault as backup] If there's no keychain, or it refuses, secrets go into Nexus Terminal's own
vault: AES-256-GCM with a key derived from your master password by Argon2id using 128\,MiB of memory. In
plain English: very, very expensive to brute-force.
\end{description}
\section{Setting up the vault}
\menu{Settings\then Security\then Initialise Secret Store}. Choose a master password; the strength meter goes from
\proto[rose]{Weak} through \proto[amber]{Fair} and \proto[accent]{Good} to \proto[green]{Strong}. Unlock and lock it
from the same page, from the palette, or by clicking the vault badge in the status bar. It locks itself when you quit.
\crop{settings-security.png}{581}{253}{576}{285}{0.8\linewidth}{Security settings on a machine without an OS keychain: the vault does the job instead.}
\begin{danger}[There's no ``forgot password'' link]
Lose the master password and the secrets in the vault are gone for good. That's the point of encryption. Put it in
your password manager.
\end{danger}
\section{Three ways to handle each password}
Every password, passphrase, token or TOTP secret has its own setting:
\begin{center}
\begin{tabular}{@{}p{0.3\linewidth}p{0.3\linewidth}p{0.3\linewidth}@{}}
\begin{tcolorbox}[colback=green!8, boxrule=0pt, arc=6pt, fontupper=\small, height=33mm]
{\heavy\color{green!60!black}\iLock\ Save securely}\par\smallskip Stored in the keychain or vault and used automatically. Most convenient.
\end{tcolorbox} &
\begin{tcolorbox}[colback=accent!8, boxrule=0pt, arc=6pt, fontupper=\small, height=33mm]
{\heavy\color{accent!80!black}\iClock\ Remember for this session}\par\smallskip Asked once, kept in memory, never written to disk.
\end{tcolorbox} &
\begin{tcolorbox}[colback=amber!10, boxrule=0pt, arc=6pt, fontupper=\small, height=33mm]
{\heavy\color{amber!75!black}\iDetective\ Ask every time}\par\smallskip Asked on every single connection. Most paranoid.
\end{tcolorbox}
\end{tabular}
\end{center}
When Nexus Terminal needs a secret it doesn't have, it asks. Press \kbd{Esc} or \ui{Cancel} to give up on that
connection.
\begin{sidequest}[What gets written down?]
Connections, disconnections, profile changes, credential access, imports and exports go into an audit log in the
database. Secrets themselves never do. TOTP secrets live in the keychain or vault too, never in the profile.
\end{sidequest}
% =========================================================================================
\chapter{Host keys: is it really that server?}\label{ch:hostkeys}
\chapmeta{3 minutes}{You'll understand the scary fingerprint dialog and what to do about it}
\begin{tldr}
Every SSH server has a fingerprint. Nexus Terminal remembers it the first time and shouts if it ever changes.
A changed fingerprint means a reinstalled server, or someone in the middle. Find out which before carrying on.
\end{tldr}
Pick a policy per SSH profile under \ui{Host Verification}:
\renewcommand\arraystretch{1.5}
\begin{tabularx}{\linewidth}{@{}>{\leavevmode\heavy\color{ink}}p{48mm}X@{}}
\toprule
Strict (known hosts only) & Only connects to servers you've already trusted.\\
Ask on new or changed keys & Shows the fingerprint and asks. \ui{Accept \& Save} or \ui{Reject}. The default.\\
Accept new, reject changed & Trusts first-time servers silently, still refuses changed keys.\\
Insecure (accept anything) & Exactly what it says. Lab use only.\\
\bottomrule
\end{tabularx}
Under every policy except \emph{Insecure}, a key that differs from the saved one is rejected.
\crop{settings-knownhosts.png}{581}{253}{576}{285}{0.8\linewidth}{Known hosts. Click a fingerprint to copy it; \ui{Remove} to forget a server.}
\begin{headsup}
Nexus Terminal keeps its own list of known hosts. It doesn't read \code{\textasciitilde/.ssh/known\_hosts}, so the
first connection to each server asks once, even if your command-line \code{ssh} already knows it.
\end{headsup}
% =========================================================================================
\chapter{SSH keys}\label{ch:keys}
\chapmeta{2 minutes}{You'll have a key pair without memorising ssh-keygen flags}
\menu{Session\then SSH key manager} (or the palette) lists the key pairs in \code{\textasciitilde/.ssh} with their type. Copy a
public key to the clipboard (ready to paste into GitHub or \code{authorized\_keys}), delete a pair, or generate a new
\proto[teal]{Ed25519}, \proto[accent]{RSA} (2048 or 4096 bits) or \proto[violet]{ECDSA} key with an optional passphrase.
\crop{keys.png}{725}{523}{725}{546}{0.75\linewidth}{The SSH key manager.}
\begin{protip}
Not sure which type? Pick Ed25519. It's small, fast and secure. Key generation uses your system's
\code{ssh-keygen}, which comes with macOS, Linux and Windows 10 and later.
\end{protip}
+107
View File
@@ -0,0 +1,107 @@
\guidepart{pink}{Make it yours}{Make it yours}
{Settings, backups, and moving in from Alchementary Link.}
{\partitem{\iGear}{Settings}
\partitem{\iUpload}{Import and export}
\partitem{\iPlane}{Moving house}}
% =========================================================================================
\chapter{Settings}\label{ch:settings}
\chapmeta{3 minutes}{You'll know where every setting lives}
\keys{\mod+,} opens settings (also the \iGear{} in the sidebar footer and \menu{File\then Settings}). Change things,
then click \ui{Save}.
\begin{center}\begin{minipage}[t]{0.49\linewidth}\crop{settings-general.png}{581}{253}{576}{285}{\linewidth}{General}\end{minipage}\hfill\begin{minipage}[t]{0.49\linewidth}\crop{settings-terminal.png}{581}{253}{576}{285}{\linewidth}{Terminal}\end{minipage}\end{center}
\begin{description}
\item[General] App theme (Pro, Midnight, Light), default protocol for new profiles, whether to ask before closing or
disconnecting, status bar on or off, sidebar width, and the local shell (leave it empty for the system default,
or \ui{Browse…} for one).
\item[Terminal] Colour scheme, font, size, scrollback and cursor style. These are the defaults for new profiles;
each profile can override them.
\item[Security] Keychain and vault status; set up, unlock or lock the vault.
\item[Known Hosts] Saved server fingerprints, with first-seen and last-seen times.
\item[Help] Built-in help: connection types, shortcuts and troubleshooting, when this guide isn't to hand.
\item[About] Version, \ui{Check for Updates} and \ui{Import \textasciitilde/.ssh/config}.
\end{description}
\crop{settings-help.png}{581}{253}{576}{285}{0.8\linewidth}{The built-in help, for when this guide isn't to hand.}
\section{Updates}
Nexus Terminal checks once at launch. If there's a newer version the status bar says \ui{Update available}; click it to
see the release notes and a \ui{View release} button. Nothing downloads or installs behind your back.
% =========================================================================================
\chapter{Import and export}\label{ch:transfer}
\chapmeta{3 minutes}{You'll back up your profiles, or copy them to another computer}
\begin{tldr}
\menu{File\then Export profiles…} makes a password-protected \code{.nexus} file. \menu{File\then Import profiles…}
reads it back. Already have an \code{\textasciitilde/.ssh/config}? Import that too.
\end{tldr}
\begin{description}
\item[Encrypted export] Choose the profiles, set a password, save a \code{.nexus} file. Saved passwords aren't
included, so the file is safe to email to yourself.
\item[Encrypted import] Pick a \code{.nexus} (or an old \code{.link}) file, enter its password and decide what happens to
profiles that already exist: \ui{Skip existing}, \ui{Overwrite existing} or \ui{Create duplicates}.
\item[JSON] \ui{Export Profiles (JSON)} and \ui{Import Profiles (JSON)} are in the palette. Tick
\ui{Include secrets} to keep proxy passwords and TOTP secrets, which are otherwise left out.
\item[OpenSSH config] \menu{File\then Import \textasciitilde/.ssh/config} turns your \code{Host} blocks into profiles,
with host names, users, ports, identity files and jump hosts.
\end{description}
\crop{export.png}{894}{357}{872}{380}{0.55\linewidth}{Exporting profiles. Everything's ticked to start with; untick what you don't want.}
\begin{danger}[JSON with secrets is not encrypted]
A JSON export with \ui{Include secrets} ticked has those secrets in plain text. Use the encrypted \code{.nexus} export to
move things between machines.
\end{danger}
% =========================================================================================
\chapter{Moving house}\label{ch:move}
\chapmeta{2 minutes}{You'll know what came over from Alchementary Link and what didn't}
\begin{tldr}
It's automatic. First run copies your Alchementary Link profiles, vault and recordings. Your old folder is left
alone. Recreate trigger rules and SFTP bookmarks by hand.
\end{tldr}
When Nexus Terminal starts for the first time and finds an \code{AlchementaryLink} folder next to where its own data
goes, it \emph{copies} \code{link.db} to \code{nexus.db}, along with the \code{vault/} and \code{recordings/} folders,
and shows a toast saying where they came from.
\begin{checklist}
\item[\color{green}\iCheck] Profiles, groups, favourites, known hosts, snippets and preferences
\item[\color{green}\iCheck] Vault (same master password as before)
\item[\color{green}\iCheck] Recordings
\item[\color{green}\iCheck] Keychain passwords: picked up the first time each one is needed, then copied across
\item[\color{green}\iCheck] Old \code{.link} export files still import
\item[\color{rose}\ph{E4F6}] Trigger rules and SFTP bookmarks (they lived in the old app's web storage)
\end{checklist}
\begin{headsup}
If a \code{NexusTerminal/nexus.db} already exists (say, from before the Alchementary Link rebrand), it's used as it is
and nothing is copied.
\end{headsup}
\section{Where everything lives}
\renewcommand\arraystretch{1.45}
\begin{tabularx}{\linewidth}{@{}>{\leavevmode\heavy\color{ink}}p{20mm}X@{}}
\toprule
macOS & \code{\textasciitilde/Library/Application Support/NexusTerminal}\\
Windows & \code{\%APPDATA\%\textbackslash NexusTerminal}\\
Linux & \code{\$XDG\_CONFIG\_HOME/NexusTerminal}, usually \code{\textasciitilde/.config/NexusTerminal}\\
\bottomrule
\end{tabularx}
Inside: \code{nexus.db} (profiles, known hosts, preferences, snippets, IRC state and the audit log),
\code{vault/vault.json}, \code{recordings/}, \code{triggers.json} and \code{sftp-bookmarks.json}. Keychain entries are
stored under the service name \code{NexusTerminal}.
\begin{protip}[Try things without touching your real data]
Start Nexus Terminal with \code{--data-dir /some/other/folder} and it uses that folder instead. Brilliant for
experiments, demos and a portable install on a USB stick.
\end{protip}
+88
View File
@@ -0,0 +1,88 @@
\guidepart{rose}{Fix it}{When things go wrong}
{Symptom first, fix second. No essays.}
{\partitem{\iLifebuoy}{Troubleshooting, by symptom}}
\label{part:fix}
% =========================================================================================
\chapter{Troubleshooting, by symptom}\label{ch:trouble}
\chapmeta{as long as it takes}{You'll find your symptom and the fix in one place}
\begin{tldr}
Find the heading that matches what you're seeing. Try the fixes in order. Most problems are the network, the
password, or the baud rate.
\end{tldr}
\section{Connecting}
\fix{``Authentication failed''}{%
\begin{itemize}
\item Check the username, password, key path and the ticked \ui{Auth Methods}.
\item If the key has a passphrase, check how the passphrase is stored (\ui{Passphrase Mode}).
\item Two-factor server? Click \ui{Test} next to the TOTP secret and compare with your phone. Check your clock.
\item Saved password not being used? The vault might be locked: click \ui{Vault locked} in the status bar.
\end{itemize}}
\fix{``Host key verification failed''}{%
The server's fingerprint changed since you last connected. That happens when a server is rebuilt, but it's also exactly
what an attack looks like. \textbf{Check with whoever runs the server.} If it's legitimate:
\menu{Settings\then Known Hosts}, \ui{Remove} the entry, reconnect and accept the new key.}
\fix{It just says ``Connecting…'' forever}{%
\begin{itemize}
\item Click \ui{Test} in the profile editor. If that fails too, it's the network, a firewall or the wrong port.
\item Behind a corporate proxy? Set one up under \ui{Proxy} (SSH profiles).
\item Going through a bastion? Check each jump host on its own first.
\end{itemize}}
\fix{The session keeps reconnecting}{%
Check the network and the credentials, then the profile's auto-reconnect settings. Reconnection backs off (1\,s, 2\,s,
4\,s… up to 30\,s) and stops after the maximum number of attempts.}
\section{Serial}
\fix{The port won't open}{%
\begin{itemize}
\item Is the adapter plugged in? Hit refresh in the device list.
\item \textbf{Permission denied} on Linux: add yourself to \code{dialout} (or \code{uucp}), then log out and back in.
\item On a Mac, use \code{/dev/cu.*}, not \code{/dev/tty.*}.
\item \textbf{Port is in use by another program}: close \code{screen}, \code{minicom}, the Arduino IDE serial monitor or
whatever else has it open.
\end{itemize}}
\fix{Gibberish on screen}{The baud rate, parity or stop bits don't match the device. Check its manual.}
\section{Remote desktops}
\fix{VNC won't connect}{%
\begin{itemize}
\item VNC ports are 5900 plus the display number: \code{:0} is 5900, \code{:1} is 5901.
\item Check the password and how it's stored.
\item Using an SSH tunnel? Check the tunnel's host, user, login method and key, and the host key policy.
\item Connecting to a Mac? Turn on password access in macOS Screen Sharing settings (see p.\,\pageref{ch:vnc}).
\end{itemize}}
\section{Files}
\fix{Upload is greyed out or fails}{%
The folder is probably \proto[amber]{READ ONLY} for your user. Pick another, or check permissions on the server. Uploads
need an SSH session; other protocols can't do it.}
\fix{``NO READ'' on a folder}{You don't have permission to look inside. Try the parent folder, or ask the server's admin.}
\section{The terminal itself}
\fix{The terminal is the wrong size}{%
Resize the window or nudge a split divider; the terminal re-fits itself. Still wrong? Reconnect. Very old servers may
prefer a different \ui{Terminal Type} in the profile (try \code{vt100}).}
\fix{The scroll wheel moves the cursor instead of scrolling}{%
You're in a full-screen program (\code{less}, \code{vim}, \code{tmux}). That's how terminals work: the wheel sends arrow keys
there. Quit the program, or use its own scrolling (in \code{tmux}: \keys{Ctrl+B} then \kbd{[}).}
\fix{Emoji are in black and white}{Known limitation: colour emoji show as monochrome glyphs. Everything else renders
normally.}
\begin{quickwin}[Still stuck?]
Start Nexus Terminal with \code{--data-dir} pointing at an empty folder and try the same connection there. If it
works, something in your profile is off; duplicate the working one and compare.
\end{quickwin}
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Romet Tagobert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.
+286
View File
@@ -0,0 +1,286 @@
% Nexus Terminal user guide — look and feel. Build with lualatex (see Makefile).
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{nexus-guide}
\RequirePackage[a4paper,top=24mm,bottom=26mm,inner=22mm,outer=26mm,headsep=8mm,footskip=12mm]{geometry}
% Latin Modern OpenType isn't always installed, and the kernel selects it once before the guide's own
% fonts take over. Point that one use at Inter so the build only needs fonts from the repo.
\DeclareFontShape{TU}{lmr}{m}{n}{<->"[../../rust/app/assets/fonts/Inter-Regular.ttf]:mode=node;"}{}
\RequirePackage{fontspec}
\RequirePackage[dvipsnames]{xcolor}
\RequirePackage{graphicx}
\RequirePackage[most]{tcolorbox}
\RequirePackage{tikz}
\usetikzlibrary{calc,positioning,shadows.blur,fadings}
\RequirePackage{titlesec}
\RequirePackage{fancyhdr}
\RequirePackage{enumitem}
\RequirePackage{tabularx}
\RequirePackage{booktabs}
\RequirePackage{multicol}
\RequirePackage{eso-pic}
\RequirePackage{etoolbox}
\RequirePackage{needspace}
\RequirePackage{microtype}
\RequirePackage[hidelinks]{hyperref}
\RequirePackage{bookmark}
% --- Fonts: the app's own Inter + JetBrains Mono, and its Phosphor icons ----------------
\defaultfontfeatures{Ligatures=TeX}
\setmainfont{Inter-Regular.ttf}[
Path=../../rust/app/assets/fonts/,
BoldFont=Inter-SemiBold.ttf,
ItalicFont=Inter-Regular.ttf, ItalicFeatures={FakeSlant=0.18},
BoldItalicFont=Inter-SemiBold.ttf, BoldItalicFeatures={FakeSlant=0.18}]
\setsansfont{Inter-Regular.ttf}[Path=../../rust/app/assets/fonts/, BoldFont=Inter-SemiBold.ttf]
\setmonofont{JetBrainsMono-Regular.ttf}[Path=../../rust/app/assets/fonts/, Scale=0.92]
\newfontfamily\heavy{Inter-SemiBold.ttf}[Path=../../rust/app/assets/fonts/]
\newfontfamily\phfont{Phosphor.ttf}[Path=fonts/]
\newfontfamily\phfill{Phosphor-Fill.ttf}[Path=fonts/]
\linespread{1.18}
\setlength{\parindent}{0pt}
\setlength{\parskip}{0.62em}
\raggedbottom
\sloppy
% --- Palette (mirrors the app's Pro theme) ---------------------------------------------
\definecolor{ink}{HTML}{1B1B1F}
\definecolor{ink2}{HTML}{26262C}
\definecolor{body}{HTML}{2A2A33}
\definecolor{muted}{HTML}{6B6B78}
\definecolor{rule}{HTML}{E4E4EA}
\definecolor{paper}{HTML}{F7F7FA}
\definecolor{accent}{HTML}{3B82F6}
\definecolor{teal}{HTML}{14B8A6}
\definecolor{violet}{HTML}{8B5CF6}
\definecolor{amber}{HTML}{F59E0B}
\definecolor{rose}{HTML}{F43F5E}
\definecolor{pink}{HTML}{EC4899}
\definecolor{green}{HTML}{22C55E}
\definecolor{slate}{HTML}{64748B}
\color{body}
% Page-level drawing. Everything that paints relative to the page goes through here, drawn at shipout
% from the page's lower-left corner, so it never depends on remember-picture positions settling.
% Inside, the node (pg) is the whole page: use pg.north west, pg.south east and so on.
\newcommand\pgnode{\node[inner sep=0pt, minimum width=\paperwidth, minimum height=\paperheight, anchor=south west] (pg) at (0,0) {};}
\newcommand\pagecanvas[1]{\AddToShipoutPictureBG*{\AtPageLowerLeft{\begin{tikzpicture}[overlay]\pgnode#1\end{tikzpicture}}}}
% Current part colour (set by \guidepart).
\colorlet{partcol}{accent}
\newcounter{guidepart}
\newcommand\partshort{}
% --- Icons ------------------------------------------------------------------------------
\newcommand\ph[1]{{\phfont\symbol{"#1}}}
\newcommand\phf[1]{{\phfill\symbol{"#1}}}
\newcommand\iTerminal{\ph{EAE8}} \newcommand\iBolt{\ph{E2DE}} \newcommand\iBrain{\ph{E74E}}
\newcommand\iWarn{\ph{E4E0}} \newcommand\iBulb{\ph{E2DC}} \newcommand\iCoffee{\ph{E1C2}}
\newcommand\iRocket{\ph{E3FE}} \newcommand\iCheck{\ph{E184}} \newcommand\iTimer{\ph{E492}}
\newcommand\iKeyboard{\ph{E2D8}} \newcommand\iSparkle{\ph{E6A2}} \newcommand\iShield{\ph{E40C}}
\newcommand\iWand{\ph{E6B6}} \newcommand\iBug{\ph{E5F4}} \newcommand\iStar{\phf{E46A}}
\newcommand\iSiren{\ph{E9B8}} \newcommand\iWave{\ph{E580}} \newcommand\iTarget{\ph{E47C}}
\newcommand\iMap{\ph{E31A}} \newcommand\iList{\ph{EADC}} \newcommand\iBox{\ph{E45E}}
\newcommand\iKey{\ph{E2D6}} \newcommand\iPlugs{\ph{EB5A}} \newcommand\iFolder{\ph{E256}}
\newcommand\iBroadcast{\ph{E0F2}} \newcommand\iFilm{\ph{E792}} \newcommand\iCode{\ph{E1BC}}
\newcommand\iGear{\ph{E270}} \newcommand\iLock{\ph{E2FA}} \newcommand\iGithub{\ph{E576}}
\newcommand\iChat{\ph{E16C}} \newcommand\iDesktop{\ph{E560}} \newcommand\iUsb{\ph{E956}}
\newcommand\iTree{\ph{E67C}} \newcommand\iArrows{\ph{E0A0}} \newcommand\iCmd{\ph{E1C4}}
\newcommand\iSearch{\ph{E30C}} \newcommand\iSplit{\ph{E870}} \newcommand\iLifebuoy{\ph{E63A}}
\newcommand\iInfo{\ph{E2CE}} \newcommand\iPin{\ph{E3E2}} \newcommand\iConfetti{\ph{E81A}}
\newcommand\iDetective{\ph{E83E}} \newcommand\iCompass{\ph{E1C8}} \newcommand\iBinary{\ph{EE60}}
\newcommand\iPalette{\ph{E6C8}} \newcommand\iRecord{\ph{E3EE}} \newcommand\iArrow{\ph{E06C}}
\newcommand\iClock{\ph{E19A}} \newcommand\iFlag{\ph{EA38}} \newcommand\iPlug{\ph{E946}}
\newcommand\iGhost{\ph{E62A}} \newcommand\iBell{\ph{E5E8}} \newcommand\iHourglass{\ph{E2B2}}
\newcommand\iDownload{\ph{E20C}} \newcommand\iUpload{\ph{E4C0}} \newcommand\iPlane{\ph{E398}}
% --- Inline bits --------------------------------------------------------------------------
% \kbd{Ctrl} — a keycap. \keys{Ctrl+Shift+K} — keycaps joined with thin pluses.
\newtcbox{\kbd}{on line, boxrule=0.4pt, arc=2.2pt, colback=paper, colframe=rule!70!slate,
boxsep=0pt, left=3.2pt, right=3.2pt, top=1.6pt, bottom=1.2pt, fontupper=\ttfamily\footnotesize\color{ink}}
% \mod is "Ctrl, or ⌘ on a Mac": every shortcut in the guide is written \keys{\mod+K}.
\renewcommand\mod{\kbd{Ctrl\,/\,\ph{E1C4}}}
\def\keystop{\relax}\def\modtok{\mod}
\DeclareRobustCommand\keys[1]{\keysplit#1+\relax+}
\def\keysplit#1+{\def\kk{#1}\ifx\kk\keystop\else\keycap{#1}\expandafter\keysplitnext\fi}
\def\keysplitnext#1+{\def\kk{#1}\ifx\kk\keystop\else{\kern0.15em\scriptsize\color{muted}+\kern0.15em}\keycap{#1}\expandafter\keysplitnext\fi}
\newcommand\keycap[1]{\def\kc{#1}\ifx\kc\modtok\mod\else\kbd{#1}\fi}
\newcommand\ui[1]{{\heavy\color{ink}#1}} % on-screen labels
\newcommand\code[1]{{\ttfamily\color{ink}#1}} % file paths and commands
\newcommand\menu[1]{{\heavy\color{ink}#1}} % File → Thing
\newcommand\then{\,{\color{muted}\iArrow}\,}
\newcommand\proto[2][accent]{\tcbox[on line, arc=6pt, boxrule=0pt, colback=#1!14, boxsep=0pt,
left=4pt, right=4pt, top=1.2pt, bottom=1.2pt]{\scriptsize\heavy\color{#1!80!black}#2}}
% --- Callout boxes ----------------------------------------------------------------------
\tcbset{guidebox/.style={enhanced, breakable, arc=7pt, boxrule=0pt, left=12pt, right=12pt,
top=8pt, bottom=8pt, before skip=10pt, after skip=12pt, fontupper=\small,
parbox=false, before upper={\setlength{\parskip}{0.5em}}}}
\newcommand\boxhead[3]{{\heavy\color{#1}\large#2\enspace\normalsize#3}\par\vspace{2pt}}
\newtcolorbox{tldr}{guidebox, colback=partcol!8, borderline west={3pt}{0pt}{partcol},
before upper={\boxhead{partcol}{\iBolt}{TL;DR}\setlength{\parskip}{0.4em}}}
\newtcolorbox{quickwin}[1][Quick win]{guidebox, colback=green!8, borderline west={3pt}{0pt}{green},
before upper={\boxhead{green!65!black}{\iTarget}{#1}}}
\newtcolorbox{headsup}[1][Heads up]{guidebox, colback=amber!10, borderline west={3pt}{0pt}{amber},
before upper={\boxhead{amber!75!black}{\iWarn}{#1}}}
\newtcolorbox{protip}[1][Pro tip]{guidebox, colback=accent!7, borderline west={3pt}{0pt}{accent},
before upper={\boxhead{accent!80!black}{\iBulb}{#1}}}
\newtcolorbox{sidequest}[1][Side quest]{guidebox, colback=violet!7,
borderline west={3pt}{0pt}{violet}, borderline={0.5pt}{0pt}{violet!35,dashed},
before upper={\boxhead{violet}{\iCompass}{#1 {\normalfont\footnotesize\color{muted}\enspace·\enspace optional, skip it if you're in a hurry}}}}
\newtcolorbox{danger}[1][Careful]{guidebox, colback=rose!8, borderline west={3pt}{0pt}{rose},
before upper={\boxhead{rose!85!black}{\iSiren}{#1}}}
% "You are here": a one-line chapter header strip with reading time.
\newcommand\chapmeta[2]{%
\begin{tcolorbox}[enhanced, arc=6pt, boxrule=0pt, colback=paper, left=10pt, right=10pt,
top=5pt, bottom=5pt, before skip=0pt, after skip=10pt, fontupper=\small\color{muted}]
{\color{partcol}\iTimer}\enspace about \textbf{#1}\hfill{\color{partcol}\iTarget}\enspace #2
\end{tcolorbox}}
% Checklists.
\newlist{checklist}{itemize}{1}
\setlist[checklist]{label={\color{partcol}\large\iBox}, leftmargin=1.9em, itemsep=2pt, topsep=4pt}
\setlist[itemize]{label={\color{partcol}\small\textbullet}, leftmargin=1.4em, itemsep=1pt, topsep=3pt}
\setlist[enumerate]{label={\heavy\color{partcol}\arabic*}, leftmargin=1.6em, itemsep=2pt, topsep=4pt}
\setlist[description]{font=\heavy\color{ink}, leftmargin=1.2em, itemsep=2pt}
% --- Screenshots --------------------------------------------------------------------------
% \shot[width]{file}{caption}
\newcommand\shot[3][\linewidth]{%
\par\needspace{0.3\textheight}\begin{center}
\begin{tikzpicture}
\node[inner sep=0pt, rounded corners=6pt, blur shadow={shadow blur steps=8, shadow xshift=0pt,
shadow yshift=-2pt, shadow opacity=25}] (img) {\includegraphics[width=#1]{img/#2}};
\draw[rule!60!slate, line width=0.4pt, rounded corners=3pt] (img.south west) rectangle (img.north east);
\end{tikzpicture}\par\vspace{4pt}
{\footnotesize\color{muted}#3}\end{center}\vspace{-4pt}}
% Two screenshots side by side.
\newcommand\shotpair[4]{%
\par\needspace{0.22\textheight}\begin{center}
\begin{minipage}[t]{0.485\linewidth}\centering\includegraphics[width=\linewidth]{img/#1}\par\vspace{3pt}{\footnotesize\color{muted}#2}\end{minipage}\hfill
\begin{minipage}[t]{0.485\linewidth}\centering\includegraphics[width=\linewidth]{img/#3}\par\vspace{3pt}{\footnotesize\color{muted}#4}\end{minipage}
\end{center}}
% A crop of a screenshot: \crop{file}{left}{bottom}{right}{top}{width}{caption}, trims in px of the 2720x1720 image.
\newcommand\crop[7]{%
\par\needspace{0.2\textheight}\begin{center}
\begin{tikzpicture}
\node[inner sep=0pt, blur shadow={shadow blur steps=8, shadow yshift=-2pt, shadow opacity=25}] (img)
{\includegraphics[width=#6, trim=#2 #3 #4 #5, clip]{img/#1}};
\draw[rule!60!slate, line width=0.4pt] (img.south west) rectangle (img.north east);
\end{tikzpicture}\par\vspace{4pt}{\footnotesize\color{muted}#7}\end{center}\vspace{-4pt}}
% Annotated screenshot: \begin{annotated}{file} \callout{1}{x}{y} … \end{annotated}, x/y in 0–1 from top left.
\newenvironment{annotated}[1]{%
\par\begin{center}\begin{tikzpicture}
\node[anchor=north west, inner sep=0pt, blur shadow={shadow blur steps=8, shadow yshift=-2pt, shadow opacity=25}] (img)
{\includegraphics[width=\linewidth]{img/#1}};
\draw[rule!60!slate, line width=0.4pt] (img.south west) rectangle (img.north east);
\begin{scope}[x={(img.north east)}, y={($(img.north west)-(img.south west)$)}, shift={(img.north west)}]}
{\end{scope}\end{tikzpicture}\end{center}}
\newcommand\callout[3]{%
\node[circle, fill=partcol, text=white, font=\heavy\footnotesize, inner sep=0pt, minimum size=15pt,
draw=white, line width=1.4pt, blur shadow={shadow blur steps=5, shadow opacity=40}] at (#2,-#3) {#1};}
\newcommand\num[1]{\tikz[baseline=(n.base)]\node[circle, fill=partcol, text=white, font=\heavy\scriptsize,
inner sep=0pt, minimum size=13pt] (n) {#1};}
% "At a glance" fact card for each connection type.
\newenvironment{glance}[2]{%
\begin{tcolorbox}[enhanced, arc=8pt, boxrule=0pt, colback=#1!7, left=12pt, right=12pt, top=9pt, bottom=9pt,
before skip=6pt, after skip=12pt, fontupper=\small,
overlay={\node[anchor=north east, font=\heavy\tiny, text=#1!80!black, inner sep=8pt] at (frame.north east) {AT A GLANCE};}]
{\heavy\Large\color{#1!85!black}#2}\par\vspace{4pt}
\renewcommand\arraystretch{1.35}\begin{tabular}{@{}>{\leavevmode\color{muted}}p{0.24\linewidth}p{0.72\linewidth}@{}}}
{\end{tabular}\end{tcolorbox}}
% --- Headings --------------------------------------------------------------------------
\titleformat{\chapter}[display]{\normalfont}{%
\pagecanvas{\fill[partcol] (pg.north west) rectangle ([yshift=-9mm]pg.north east);
\fill[partcol!12] ([yshift=-9mm]pg.north west) rectangle ([yshift=-11mm]pg.north east);}%
{\heavy\small\color{partcol}\MakeUppercase{\partshort}\enspace·\enspace CHAPTER \thechapter}}{2pt}
{\heavy\fontsize{30}{34}\selectfont\color{ink}}
\titlespacing*{\chapter}{0pt}{0pt}{14pt}
\titleformat{\section}{\heavy\Large\color{ink}}{%
\tikz[baseline=(n.base)]\node[fill=partcol, rounded corners=3pt, text=white, font=\heavy\small, inner sep=3pt, minimum width=18pt] (n) {\thesection};}{8pt}{}
\titlespacing*{\section}{0pt}{18pt}{4pt}
\titleformat{\subsection}{\heavy\large\color{ink}}{}{0pt}{{\color{partcol}\iArrow}\enspace}
\titlespacing*{\subsection}{0pt}{12pt}{2pt}
\renewcommand\thesection{\arabic{chapter}.\arabic{section}}
\setcounter{secnumdepth}{1}
\setcounter{tocdepth}{1}
% --- Running heads + thumb tabs ---------------------------------------------------------
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\renewcommand\chaptermark[1]{\markboth{#1}{}}
\fancyhead[LE]{\footnotesize\color{muted}\heavy Nexus Terminal\enspace\normalfont·\enspace User Guide}
\fancyhead[RO]{\footnotesize\color{muted}\leftmark}
\fancyfoot[LE,RO]{\tikz[baseline=(n.base)]\node[fill=partcol, rounded corners=3pt, text=white,
font=\heavy\footnotesize, inner sep=3pt, minimum width=20pt] (n) {\thepage};}
\fancyfoot[RE,LO]{\footnotesize\color{muted}\partshort}
\fancypagestyle{plain}{\fancyhf{}\fancyfoot[LE,RO]{\tikz[baseline=(n.base)]\node[fill=partcol, rounded corners=3pt,
text=white, font=\heavy\footnotesize, inner sep=3pt, minimum width=20pt] (n) {\thepage};}}
% Thumb tabs on the page edge, one slot per part, so you can flick to a part in print.
\newif\ifthumbs
\AddToShipoutPictureBG{%
\ifthumbs\ifnum\value{guidepart}>0
\AtPageLowerLeft{\begin{tikzpicture}[overlay]\pgnode
\pgfmathsetmacro\slot{28 + (\value{guidepart}-1)*30}
\ifodd\value{page}
\fill[partcol, rounded corners=4pt] ([xshift=-7mm, yshift=-\slot mm]pg.north east) rectangle ++(9mm,-26mm);
\node[rotate=-90, text=white, font=\heavy\tiny] at ([xshift=-3.6mm, yshift=-\slot mm - 13mm]pg.north east) {\MakeUppercase{\partshort}};
\else
\fill[partcol, rounded corners=4pt] ([xshift=7mm, yshift=-\slot mm]pg.north west) rectangle ++(-9mm,-26mm);
\node[rotate=90, text=white, font=\heavy\tiny] at ([xshift=3.6mm, yshift=-\slot mm - 13mm]pg.north west) {\MakeUppercase{\partshort}};
\fi
\end{tikzpicture}}\fi\fi}
% \guidepart{colour}{short name}{Title}{tagline}{list of chapters}
\newcommand\guidepart[5]{%
\clearpage
\stepcounter{guidepart}\colorlet{partcol}{#1}\renewcommand\partshort{#2}
\phantomsection\addcontentsline{toc}{part}{\protect\textcolor{#1}{\arabic{guidepart}}\quad #3}
\thispagestyle{empty}\thumbsfalse
\pagecanvas{
\fill[ink] (pg.south west) rectangle (pg.north east);
\fill[#1, opacity=0.9] (pg.north west) rectangle ([yshift=-95mm]pg.north east);
\fill[#1!70!black, opacity=0.35] ([yshift=-95mm]pg.north west) -- ++(0,-18mm) -- ([yshift=-95mm]pg.north east) -- cycle;
\node[anchor=north west, text=white, font=\heavy\fontsize{150}{150}\selectfont, opacity=0.25]
at ([xshift=14mm, yshift=-4mm]pg.north west) {\arabic{guidepart}};
\node[anchor=south west, text=white, font=\heavy\small] at ([xshift=24mm, yshift=-72mm]pg.north west) {PART \arabic{guidepart}};
\node[anchor=north west, text=white, font=\heavy\fontsize{38}{42}\selectfont, text width=160mm]
at ([xshift=24mm, yshift=-73mm]pg.north west) {#3};
\node[anchor=north west, text=white!80!#1, font=\large, text width=150mm]
at ([xshift=24mm, yshift=-125mm]pg.north west) {#4};
\node[anchor=north west, text=white, font=\normalsize, text width=150mm]
at ([xshift=24mm, yshift=-145mm]pg.north west) {#5};
}\null\clearpage\thumbstrue}
\newcommand\partitem[2]{{\color{partcol}\large#1}\enspace #2\par\vspace{5pt}}
% Table of contents.
\RequirePackage{tocloft}
\renewcommand\cftpartfont{\heavy\large\color{ink}}
\renewcommand\cftpartpagefont{\heavy\color{ink}}
\renewcommand\cftchapfont{\color{body}}
\renewcommand\cftchappagefont{\color{muted}}
\renewcommand\cftchapleader{\cftdotfill{\cftdotsep}}
\setlength\cftbeforepartskip{14pt}
\setlength\cftbeforechapskip{3pt}
\renewcommand\cfttoctitlefont{\heavy\fontsize{30}{34}\selectfont\color{ink}}
% Symptom → fix cards for troubleshooting.
\newcommand\fix[2]{%
\begin{tcolorbox}[enhanced, breakable, arc=7pt, boxrule=0.5pt, colframe=rule, colback=white, left=11pt, right=11pt,
top=8pt, bottom=8pt, before skip=6pt, after skip=8pt, fontupper=\small, parbox=false,
title={\heavy\color{ink}\normalsize{\color{partcol}\iBug}\enspace #1}, coltitle=ink, colbacktitle=paper,
titlerule=0pt, toptitle=5pt, bottomtitle=5pt, attach title to upper={}, detach title, before upper={\tcbtitle\par\vspace{4pt}}]
#2\end{tcolorbox}}
% Big pull-quote line used to break up long stretches.
\newcommand\breather[1]{\par\vspace{6pt}{\centering\heavy\large\color{partcol}#1\par}\vspace{6pt}}
+100
View File
@@ -0,0 +1,100 @@
% Nexus Terminal — User Guide. Build: make (in docs/guide), or: lualatex nexus-guide && lualatex nexus-guide
\documentclass[10pt,twoside,openany]{book}
\usepackage{nexus-guide}
\hypersetup{pdftitle={Nexus Terminal: The User Guide}, pdfauthor={Andy Dixon},
pdfsubject={SSH, Telnet, RAW, Serial, VNC, IRC and GitHub in one native app}}
\begin{document}
\frontmatter
\pagestyle{empty}
% --- Cover ------------------------------------------------------------------------------
\pagecanvas{
\fill[ink] (pg.south west) rectangle (pg.north east);
\shade[left color=accent, right color=violet, opacity=0.55]
([yshift=-60mm]pg.north west) -- ([yshift=-20mm]pg.north east) --
([yshift=-130mm]pg.north east) -- ([yshift=-170mm]pg.north west) -- cycle;
\shade[left color=teal, right color=accent, opacity=0.35]
([yshift=-150mm]pg.north west) -- ([yshift=-120mm]pg.north east) --
([yshift=-150mm]pg.north east) -- ([yshift=-185mm]pg.north west) -- cycle;
\node[anchor=north east, inner sep=0pt] at ([xshift=-22mm, yshift=-24mm]pg.north east)
{\includegraphics[width=34mm]{img/logo.pdf}};
\node[anchor=north west, text=white!70!ink, font=\heavy\small] at ([xshift=22mm, yshift=-30mm]pg.north west)
{THE USER GUIDE};
\node[anchor=north west, text=white, font=\heavy\fontsize{58}{60}\selectfont, text width=170mm]
at ([xshift=22mm, yshift=-36mm]pg.north west) {Nexus\\[-2pt]Terminal};
\node[anchor=north west, text=white, font=\Large, text width=150mm]
at ([xshift=22mm, yshift=-88mm]pg.north west)
{Every terminal you need. One window.\\Zero faff.};
\node[anchor=north west, text=white!65!ink, font=\normalsize, text width=150mm]
at ([xshift=22mm, yshift=-110mm]pg.north west)
{SSH\enspace·\enspace Telnet\enspace·\enspace RAW TCP\enspace·\enspace Serial\enspace·\enspace VNC\enspace·\enspace IRC\enspace·\enspace GitHub\enspace·\enspace Local shells};
\node[anchor=south west, inner sep=0pt] at ([xshift=22mm, yshift=22mm]pg.south west)
{\begin{tikzpicture}
\node[inner sep=0pt, draw=white!25!ink, line width=0.6pt, rounded corners=4pt] {\includegraphics[width=166mm]{img/ssh.png}};
\end{tikzpicture}};
\node[anchor=south east, text=white!55!ink, font=\footnotesize, align=right]
at ([xshift=-22mm, yshift=24mm]pg.south east) {Version 0.1\\October 2026\\dixon.cx};
}\null
\cleardoublepage
% --- How to use this guide ----------------------------------------------------------------
\colorlet{partcol}{accent}
{\heavy\fontsize{30}{34}\selectfont\color{ink} Start here.\par}
\vspace{2pt}{\large\color{muted} (Yes, even if you never read manuals. \emph{Especially} then.)\par}
\vspace{10pt}
This guide is built for skimming. You do \textbf{not} have to read it in order, cover to cover, like it's
homework. Find the bit you need, grab it, get back to work. Every chapter opens with the time it takes and
what you'll be able to do afterwards, and a \textbf{TL;DR} if you only want the gist.
\vspace{4pt}
{\heavy\large\color{ink}\iMap\enspace I just want to…}\par\vspace{2pt}
\renewcommand\arraystretch{1.5}
\begin{tabularx}{\linewidth}{@{}X>{\raggedleft\arraybackslash}p{52mm}@{}}
\toprule
…get connected to something \emph{right now} & \hyperref[ch:start]{Five-minute start}, p.\,\pageref{ch:start}\\
…work out what all the buttons do & \hyperref[ch:tour]{The grand tour}, p.\,\pageref{ch:tour}\\
…SSH into a server (keys, jump hosts, 2FA) & \hyperref[ch:ssh]{SSH}, p.\,\pageref{ch:ssh}\\
…talk to a router or Arduino over a USB cable & \hyperref[ch:serial]{Serial}, p.\,\pageref{ch:serial}\\
…copy files to and from a server & \hyperref[ch:files]{Files}, p.\,\pageref{ch:files}\\
…type the same thing into ten servers at once & \hyperref[ch:repeat]{Never type it twice}, p.\,\pageref{ch:repeat}\\
…keep my passwords safe & \hyperref[ch:secrets]{Passwords \& the vault}, p.\,\pageref{ch:secrets}\\
…move over from Alchementary Link & \hyperref[ch:move]{Moving house}, p.\,\pageref{ch:move}\\
…fix something that's broken & \hyperref[part:fix]{When things go wrong}, p.\,\pageref{ch:trouble}\\
…just see the keyboard shortcuts & \hyperref[app:keys]{The cheat sheet}, p.\,\pageref{app:keys}\\
\bottomrule
\end{tabularx}
\vspace{8pt}
{\heavy\large\color{ink}\iSparkle\enspace The little boxes}\par\vspace{-2pt}
\begin{multicols}{2}\small
\begin{tldr}The whole chapter in three lines.\end{tldr}
\begin{quickwin}Something you can try in under a minute.\end{quickwin}
\begin{protip}A trick that saves you time later.\end{protip}
\columnbreak
\begin{headsup}A thing that might trip you up.\end{headsup}
\begin{danger}Could lose data or weaken security. Read it.\end{danger}
\begin{sidequest}Interesting, but not needed to get the job done.\end{sidequest}
\end{multicols}
\vspace{-6pt}
{\small\color{muted}\textbf{Shortcuts:} \mod{} means \kbd{Ctrl} on Windows and Linux, \kbd{\ph{E1C4}} (Command) on a Mac. So
\keys{\mod+K} is \kbd{Ctrl}+\kbd{K} or \kbd{\ph{E1C4}}+\kbd{K} depending on your machine.}
\clearpage
\pagestyle{plain}
{\hypersetup{linkcolor=body}\tableofcontents}
\clearpage
\mainmatter
\pagestyle{fancy}
\input{chapters/part1-start}
\input{chapters/part2-connect}
\input{chapters/part3-faster}
\input{chapters/part4-safe}
\input{chapters/part5-yours}
\input{chapters/part6-fix}
\input{chapters/appendix}
\end{document}
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# ForceCommand for the throwaway screenshot sshd: never a login shell (no ~/.profile, no byobu).
DEMO_HOME=$1 SFTP_SERVER=$2
export LC_ALL=C.UTF-8
case "$SSH_ORIGINAL_COMMAND" in
*sftp*) exec "$SFTP_SERVER" -d "$DEMO_HOME" ;;
?*) cd "$DEMO_HOME" && HOME="$DEMO_HOME" exec bash --norc --noprofile -c "$SSH_ORIGINAL_COMMAND" ;;
esac
cd "$DEMO_HOME" || exit 1
export HOME="$DEMO_HOME" PS1='\[\e[1;32m\]andy@atlas\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]\$ '
row() { # boxed line, padded on visible width
local plain; plain=$(printf '%b' "$1" | sed 's/\x1b\[[0-9;]*m//g')
printf '\e[1;36m│\e[0m %b%*s\e[1;36m│\e[0m\n' "$1" $((44 - ${#plain})) ''
}
printf '\e[1;36m╭──────────────────────────────────────────────╮\e[0m\n'
row 'Welcome to \e[1matlas\e[0m · Debian 13 · kernel 6.12'
row 'Load \e[32m0.42\e[0m Memory \e[33m61%\e[0m Disk \e[32m38%\e[0m'
row 'Last deploy: \e[1m2 hours ago\e[0m by andy'
printf '\e[1;36m╰──────────────────────────────────────────────╯\e[0m\n\n'
printf '\e[1;32m$\e[0m ls -l --color\n'; ls -l --color=always
printf '\n\e[1;32m$\e[0m ./deploy.sh --dry-run\n'
for s in "Resolving build" "Uploading artefacts" "Rolling out to 4 nodes" "Health checks"; do
printf ' \e[32m✔\e[0m %-26s \e[2m%s\e[0m\n' "$s" "$((RANDOM % 900 + 100))ms"
done
printf ' \e[33m⚠\e[0m 1 warning: \e[1mnginx.conf\e[0m has a deprecated directive\n'
printf ' \e[1;32mDone.\e[0m Nothing was actually deployed (dry run).\n\n'
exec bash --norc --noprofile -i
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Tiny fake servers for the user-guide screenshots: RAW sensor feed (42023), IRC (42667),
GitHub API (42080) and a VNC desktop (42900). Stdlib only. Runs until killed."""
import json, math, random, socket, socketserver, struct, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Threaded(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
daemon_threads = True
# --- RAW: a chatty sensor --------------------------------------------------------------
class Raw(socketserver.BaseRequestHandler):
def handle(self):
s = self.request
s.sendall(b"SENSOR-7 ready. Commands: STATUS, RESET\n")
t = 20.5
s.settimeout(0.25)
for i in range(400):
t += random.uniform(-0.3, 0.3)
s.sendall(f"temp={t:.2f}C hum={48 + 4 * math.sin(i / 3):.1f}% seq={i}\n".encode())
try:
if (cmd := s.recv(256)):
s.sendall(b"OK " + cmd.strip() + b"\n")
except socket.timeout:
pass
# --- IRC: a small, friendly network ----------------------------------------------------
CHAT = [
("ferris", "anyone tried the new Nexus build yet?"),
("crabby", "yep, the split panes are lovely"),
("ferris", "the serial console support saved my afternoon tbh"),
("lin", "does it do jump hosts?"),
("crabby", "lin: multiple hops, plus SOCKS5/HTTP proxies"),
("ferris", "and it's Rust all the way down now :)"),
]
class Irc(socketserver.StreamRequestHandler):
def send(self, line):
self.wfile.write((line + "\r\n").encode())
def handle(self):
nick = "andy"
for raw in self.rfile:
line = raw.decode(errors="replace").strip()
cmd, _, rest = line.partition(" ")
if cmd == "NICK":
nick = rest.strip()
elif cmd == "USER":
self.send(f":irc.demo 001 {nick} :Welcome to the Demo IRC Network, {nick}")
self.send(f":irc.demo 375 {nick} :- irc.demo Message of the Day -")
self.send(f":irc.demo 372 {nick} :- Be excellent to each other.")
self.send(f":irc.demo 376 {nick} :End of /MOTD command.")
elif cmd == "JOIN":
for room in rest.split()[0].split(","):
self.send(f":{nick}!u@demo JOIN {room}")
self.send(f":irc.demo 332 {nick} {room} :Nexus Terminal chat · be kind · no pasting walls of logs")
self.send(f":irc.demo 353 {nick} = {room} :@ferris +crabby lin {nick} octo rustacean")
self.send(f":irc.demo 366 {nick} {room} :End of /NAMES list.")
if room == "#nexus":
for who, msg in CHAT:
self.send(f":{who}!u@demo PRIVMSG #nexus :{msg}")
else:
self.send(f":octo!u@demo PRIVMSG {room} :borrow checker 1, me 0")
elif cmd == "LIST":
for room, n, topic in [("#nexus", 42, "Nexus Terminal"), ("#rust", 3110, "Rust"), ("#linux", 905, "Linux"), ("#homelab", 77, "Racks & routers")]:
self.send(f":irc.demo 322 {nick} {room} {n} :{topic}")
self.send(f":irc.demo 323 {nick} :End of /LIST")
elif cmd == "PING":
self.send(f":irc.demo PONG irc.demo {rest}")
elif cmd == "QUIT":
return
# --- GitHub API --------------------------------------------------------------------------
REPOS = [
("andydixon", "nexus-terminal", "SSH, Telnet, Serial, IRC and VNC in one tidy native app", "Rust", 128, False, False, False, 1),
("andydixon", "leviathan", "The egui design system Nexus borrows its looks from", "Rust", 64, False, False, False, 3),
("andydixon", "dotfiles", "Shell, editor and terminal config", "Shell", 12, False, False, False, 6),
("andydixon", "homelab-ansible", "Playbooks for the rack under the stairs", "Jinja", 7, True, False, False, 9),
("andydixon", "alchementary-link", "The Wails/Go predecessor to Nexus Terminal", "Go", 31, False, False, True, 40),
("alchementary", "website", "Marketing site", "TypeScript", 3, True, False, False, 2),
("alchementary", "infra", "Terraform for everything", "HCL", 2, True, False, False, 5),
("alchementary", "russh", "Fork with a jump-host fix", "Rust", 0, False, True, False, 15),
]
WHO = ["Andy Dixon", "ferris", "crabby", "dependabot[bot]"]
def iso(days):
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - days * 86400 - 3600 * 5))
class Github(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def reply(self, obj):
body = json.dumps(obj).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/user":
return self.reply({"login": "andydixon"})
if self.path.startswith("/user/repos"):
page = 1 if "page=1" in self.path else 2
if page != 1:
return self.reply([])
return self.reply([
{"full_name": f"{o}/{n}", "name": n, "description": d, "html_url": f"https://github.com/{o}/{n}",
"default_branch": "main", "private": p, "fork": f, "archived": a, "language": l,
"stargazers_count": s, "pushed_at": iso(age), "updated_at": iso(age), "owner": {"login": o}}
for o, n, d, l, s, p, f, a, age in REPOS])
if "/commits/" in self.path:
i = sum(map(ord, self.path)) % len(WHO)
age = next((r[8] for r in REPOS if f"/{r[1]}/" in self.path), 1)
return self.reply({"commit": {"committer": {"name": WHO[i], "date": iso(age)}, "author": {"name": WHO[i], "date": iso(age)}}})
self.send_error(404)
# --- VNC: a static "desktop" ---------------------------------------------------------------
W, H = 1280, 800
def desktop():
px = bytearray(W * H * 4)
wins = [(90, 90, 620, 420, (40, 44, 52)), (560, 260, 1180, 700, (250, 250, 252)), (140, 560, 460, 740, (32, 140, 120))]
for y in range(H):
for x in range(W):
# Diagonal violet → teal wallpaper.
k = (x + y) / (W + H)
r, g, b = int(88 * (1 - k) + 20 * k), int(40 * (1 - k) + 150 * k), int(160 * (1 - k) + 170 * k)
for x0, y0, x1, y1, c in wins:
if x0 <= x < x1 and y0 <= y < y1:
r, g, b = (200, 70, 90) if y < y0 + 28 else c
if y >= y0 + 28 and c == (40, 44, 52) and (y - y0) % 22 < 9 and x0 + 20 < x < x0 + 20 + ((y * 37) % 400):
r, g, b = (120, 220, 140) if (y // 22) % 3 else (230, 200, 120)
if y >= H - 40:
r, g, b = 24, 24, 30
i = (y * W + x) * 4
px[i:i + 4] = bytes((r, g, b, 0))
return bytes(px)
PIXELS = None
class Vnc(socketserver.BaseRequestHandler):
def recv(self, n):
b = b""
while len(b) < n:
c = self.request.recv(n - len(b))
if not c:
raise EOFError
b += c
return b
def handle(self):
s = self.request
s.sendall(b"RFB 003.008\n")
self.recv(12)
s.sendall(bytes([1, 1])) # one security type: None
self.recv(1)
s.sendall(struct.pack(">I", 0))
self.recv(1) # ClientInit
name = b"studio:0 (demo)"
s.sendall(struct.pack(">HH", W, H) + bytes([32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0]) + struct.pack(">I", len(name)) + name)
sent = False
try:
while True:
t = self.recv(1)[0]
if t == 0:
self.recv(19)
elif t == 2:
n = struct.unpack(">xH", self.recv(3))[0]
self.recv(4 * n)
elif t == 3:
self.recv(9)
if not sent:
s.sendall(struct.pack(">BxH", 0, 1) + struct.pack(">HHHHi", 0, 0, W, H, 0) + PIXELS)
sent = True
else:
time.sleep(0.5)
s.sendall(struct.pack(">BxH", 0, 0))
elif t == 4:
self.recv(7)
elif t == 5:
self.recv(5)
elif t == 6:
n = struct.unpack(">3xI", self.recv(7))[0]
self.recv(n)
else:
return
except (EOFError, ConnectionError):
pass
if __name__ == "__main__":
PIXELS = desktop()
for port, handler in [(42023, Raw), (42667, Irc), (42900, Vnc)]:
threading.Thread(target=Threaded(("127.0.0.1", port), handler).serve_forever, daemon=True).start()
print("fakes ready", flush=True)
ThreadingHTTPServer(("127.0.0.1", 42080), Github).serve_forever()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Regenerates docs/guide/img/*.png: a throwaway sshd (ForceCommand, never a login shell),
# fake RAW/IRC/GitHub/VNC servers, a demo data directory, then one headless app run per shot.
# Needs: xvfb-run, sshd, ssh-keygen, python3, ImageMagick. Usage: docs/guide/tools/screenshots.sh [shot...]
set -euo pipefail
cd "$(dirname "$0")/.."
GUIDE=$PWD
B=${NEXUS_GUIDE_TMP:-/tmp/nexus-guide}
IMG=$GUIDE/img
APP=$GUIDE/../../rust/target/debug/nexus-terminal
SCALE=${SCALE:-2}
mkdir -p "$IMG"
rm -rf "$B" && mkdir -p "$B/home/.ssh" "$B/sshd"
(cd ../../rust && cargo build -q -p nexus && cargo build -q -p nexus-core --example demo_data)
# Demo home: what the SFTP browser, the shell and the SSH key manager see.
H=$B/home
mkdir -p "$H"/{projects/nexus,backups,logs,www/static}
printf 'server {\n listen 443 ssl;\n}\n' > "$H/nginx.conf"
echo '#!/bin/sh' > "$H/deploy.sh"; chmod +x "$H/deploy.sh"
head -c 180000 /dev/urandom > "$H/backups/db-2026-10-10.sql.gz"
head -c 52000 /dev/urandom > "$H/logs/access.log"
echo "# Notes" > "$H/README.md"
ssh-keygen -q -t ed25519 -N '' -C 'andy@laptop' -f "$H/.ssh/id_ed25519"
ssh-keygen -q -t rsa -b 2048 -N '' -C 'andy@work' -f "$H/.ssh/work_rsa"
ssh-keygen -q -t ecdsa -N '' -C 'deploy-bot' -f "$H/.ssh/deploy_ecdsa"
# sshd on 42022 (ForceCommand keeps it away from ~/.profile).
S=$B/sshd
ssh-keygen -q -t ed25519 -N '' -f "$S/host"
cp "$H/.ssh/id_ed25519.pub" "$S/authorized_keys"
SFTP=$(ls /usr/lib/openssh/sftp-server /usr/libexec/openssh/sftp-server /usr/libexec/sftp-server 2>/dev/null | head -1 || true)
cat > "$S/config" <<CFG
Port 42022
ListenAddress 127.0.0.1
HostKey $S/host
AuthorizedKeysFile $S/authorized_keys
PidFile $S/pid
StrictModes no
UsePAM no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PrintMotd no
PrintLastLog no
Subsystem sftp $SFTP
ForceCommand $GUIDE/tools/demo-shell.sh $H $SFTP
CFG
"$(command -v sshd)" -f "$S/config" -E "$S/log"
python3 -I "$GUIDE/tools/fakes.py" > "$B/fakes.log" 2>&1 &
FAKES=$!
trap 'kill $FAKES 2>/dev/null; kill "$(cat "$S/pid" 2>/dev/null)" 2>/dev/null || true' EXIT
until grep -q ready "$B/fakes.log"; do sleep 0.2; done
"$(dirname "$APP")/examples/demo_data" "$B/data" "$H/.ssh/id_ed25519" > /dev/null
cp -r "$B/data" "$B/data.clean"
shot() { # name, then app args
local name=$1; shift
if [ ${#ONLY[@]} -gt 0 ] && [[ ! " ${ONLY[*]} " =~ " $name " ]]; then return 0; fi
rm -rf "$B/data" && cp -r "$B/data.clean" "$B/data"
HOME=$H WINIT_X11_SCALE_FACTOR=$SCALE timeout 60 xvfb-run -a -s "-screen 0 3200x2000x24" \
"$APP" --data-dir "$B/data" --size 1360x860 --screenshot "$IMG/$name.png" "$@" 2>> "$B/app.log" || echo " ! $name failed"
# 256-colour PNGs are a quarter of the size and look identical for UI shots.
[ -f "$IMG/$name.png" ] && magick "$IMG/$name.png" -colors 256 "PNG8:$IMG/$name.png"
echo " $name"
}
ONLY=("$@")
shot welcome
shot welcome-midnight --theme midnight
shot welcome-light --theme light
shot ssh --connect "Sensor feed" --connect "Libera Chat" --connect atlas --shot-delay 5500
shot ssh-light --theme light --connect atlas --shot-delay 5500
shot hostkey --connect hello-ssh --shot-delay 5000
shot split --connect atlas --do "Split Pane Right" --shot-delay 5500
shot sftp --connect atlas --do "SFTP Browser" --shot-delay 6000
shot broadcast --connect build-runner --connect atlas --do "Toggle Broadcast Mode" --shot-delay 5500
shot raw --connect "Sensor feed" --shot-delay 5500
shot irc --connect "Libera Chat" --shot-delay 5500
shot github --connect GitHub --answer ghp_demo --shot-delay 6000
shot vnc --connect "Studio desktop" --answer demo --shot-delay 6000
shot local --run-local 'printf "\e[1;35mhello from the local shell\e[0m\n"; ls -la --color=always' --shot-delay 5000
shot palette --open palette
shot quick --open quick
shot editor-ssh --open editor:ssh
shot editor-new --open editor:new
shot editor-serial --open editor:serial
shot editor-telnet --open editor:telnet
shot editor-raw --open editor:raw
shot editor-vnc --open editor:vnc
shot editor-irc --open editor:irc
shot editor-github --open editor:github
for t in general terminal security knownhosts help about; do shot "settings-$t" --open "settings:$t"; done
shot keys --open keys
shot snippets --open snippets
shot triggers --open triggers
shot recordings --open recordings
shot export --open export