-
-
Notifications
You must be signed in to change notification settings - Fork 0
Adding a New Tool
This walks through wiring up a brand-new tool end to end, using the same
shape as the existing ones (mysql_mgr is the reference example below —
it's a mid-complexity tool: a config-backed list of connection profiles plus
a second tab that does real work over one of them). Skim Architecture
first if you haven't — this page assumes you know the App/Screen
dispatch pattern.
Create src/<tool>/mod.rs plus whatever split makes sense — the existing
convention is config.rs for the on-disk profile/settings CRUD and
client.rs (DB/HTTP tools) or exec.rs (SSH-driven tools) for the actual
work. No ratatui/crossterm imports here — this module has to be
usable from a screen or a CLI subcommand without caring which.
- Persisted config →
src/config.rs'sconfig_file("<tool>.json"), aserde::{Serialize, Deserialize}struct, loaded/saved as plainserde_json. Look atmysql_mgr::config::{Config, Connection, load, save}for the shape. - Any password/token that needs to persist → encrypt it with
crate::secret::encrypt/encrypt_optionalbefore it goes in the struct that gets serialized, decrypt withdecrypt/decrypt_optionalwhen building an in-memory "with secrets" view for actual use. See Config and Secrets andmysql_mgr::config::ConnectionWithSecretsfor the pattern (store_encryptedfields on disk, hand out aWithSecretsstruct in memory only, never the reverse). - Talks to a remote host over SSH → build on
crate::ssh_exec::SshSession(or the one-shotcrate::ssh_exec::run_commands) rather than shelling out to a realsshbinary or rolling your own socket code. If it needs to reach something (a DB, an internal service) that's only reachable from the remote side, usecrate::ssh_tunnel::open— see SSH and Remote Execution.
mod <tool>;added next to the other mod lines (alphabetical order is the existing
convention, not enforced by anything).
This is the only file that's allowed to import both crate::<tool>::* and
ratatui/crossterm. It must expose exactly this surface — App in
tui/mod.rs calls these by name, so getting the signatures right is what
makes step 4 a one-line-per-method change:
pub struct <Tool>Screen { /* your UI state: Input fields, selected index,
which tab, an Option<FilePicker>/HostPicker
for any modal in progress, etc. */ }
impl <Tool>Screen {
pub fn new() -> Self { /* load config, set defaults */ }
pub fn tick(&mut self) { /* poll a background thread's mpsc::Receiver,
advance a spinner — no-op is fine if the
tool has nothing async */ }
/// Return `true` to pop back to the Home screen (Esc from the top
/// level); `false` to stay (Esc closes a modal / clears a field first).
pub fn handle_key(&mut self, key: KeyEvent) -> bool { ... }
pub fn handle_mouse(&mut self, me: MouseEvent, area: Rect) { ... }
pub fn draw(&self, f: &mut Frame, area: Rect) { ... }
}Build the UI out of the shared primitives in tui/widgets.rs rather than
raw ratatui widgets wherever one fits — see TUI Conventions for the
full list (Input, input_span, btn_span, tab_span, theme_block,
draw_modal, draw_history/copy_history_to_clipboard) and the color
functions (bg(), fg(), accent(), ...) — never hardcode a Color,
always call the theme function, so F9 theme-cycling repaints your screen
too. For mouse hit-testing, reuse tui/mouse.rs's helpers
(table_row_hit, button_row_hit, label_row_hit, scroll_delta) instead
of writing new coordinate math — see TUI Conventions for why
table_row_hit in particular exists (it replicates ratatui's own
scroll-to-keep-selection-visible math; naive y - top math selects the
wrong row once a list has scrolled).
If the tool needs to pick an SSH host from ~/.ssh/config, a file from the
filesystem, or a set of DB privileges, reuse host_picker::HostPicker,
file_picker::FilePicker, or priv_picker::PrivPicker instead of building
another modal from scratch — every existing DB tool (mysql_screen.rs,
postgresql_screen.rs, clickhouse_screen.rs) composes all three.
Four small additions, each exactly mirroring what every other tool already has at that spot:
-
mod <tool>_screen;at the top with the other screen modules. - A new
Screen::<Tool>variant. - A new
<tool>: Option<<tool>_screen::<Tool>Screen>field onApp, plus<tool>: NoneinApp::new(). - One arm each in
App::enter(lazily construct on first visit),App::tick,App::take_pending_action(only if your screen can produce one — see Architecture's note onPendingAction, most tools don't),App::handle_mouse,App::handle_key, andApp::draw. Every arm is a one-liner that forwards to the field you just added — copy theScreen::SslCert => { ... }arm in each match as a template, it's the newest tool and therefore the most representative of current style.
- One new
HomeItem { title, desc, bin, screen: Screen::<Tool> }entry inDEFAULT_ITEMS.binis cosmetic (shown in parens next to the title) — by convention it names what the tool would be called as a standalone binary/command, even thoughatkships as one binary. - One arm each in
screen_key(Screen → string, used for the persistedmenu_order.json) andscreen_from_key(the inverse). Pick a short, stable lowercase key — this string is what's already written to disk for every existing user'smenu_order.json, so once it ships, don't rename it.
Note only tools 1–9 in the default order get a digit shortcut
(Char(c) if c.is_ascii_digit() in HomeState::handle_key) — a 10th+ tool
is reachable by arrow keys / mouse / Enter same as any other, just not by a
single keypress. (This is already the situation for the SSL Certificate
Manager, item 10.)
If the tool should also be usable non-interactively (atk <tool> ... in a
script/cron job, no TUI), add a clap::Subcommand in main.rs — see
CLI Subcommands for the full pattern, using the existing sshuser
subcommand as the template. This is optional and most tools don't have one;
only add it if there's an actual non-interactive use case, not by default.
Bump whatever "N tools" count is in the intro, add a section describing the tool (what it does, what it needs configured, any gotchas), and if you record a demo GIF, drop it next to the others and reference it the same way.
-
src/<tool>/— no ratatui/crossterm imports -
mod <tool>;inmain.rs -
src/tui/<tool>_screen.rsimplementingnew/tick/handle_key/handle_mouse/draw -
mod <tool>_screen;,Screen::<Tool>,Option<...>field +Nonedefault, and all sixmatcharms intui/mod.rs -
HomeItementry +screen_key/screen_from_keyarms intui/home.rs - Colors via
widgets::{bg, fg, accent, ...}, never a hardcodedColor - Config (if any) via
config::config_file, secrets viasecret::{encrypt,decrypt}* -
cargo buildclean, manually exercised every code path once in a real terminal - README updated