Screenshot automation (--connect repeatable, --do, --answer, --shot-delay), SFTP Browser palette entry, centred dialogs, IRC opens the first joined room; demo_data example
This commit is contained in:
+33
-10
@@ -23,8 +23,14 @@ pub struct Options {
|
|||||||
/// Open a window at startup: editor, quick, settings[:tab], keys, snippets, triggers,
|
/// Open a window at startup: editor, quick, settings[:tab], keys, snippets, triggers,
|
||||||
/// recordings, palette, export, import (screenshot checks).
|
/// recordings, palette, export, import (screenshot checks).
|
||||||
pub open: Option<String>,
|
pub open: Option<String>,
|
||||||
/// Connect a saved profile (by name or id) at startup.
|
/// Connect saved profiles (by name or id) at startup.
|
||||||
pub connect: Option<String>,
|
pub connect: Vec<String>,
|
||||||
|
/// Command-palette actions (by label) to run once sessions have had time to connect.
|
||||||
|
pub actions: Vec<String>,
|
||||||
|
/// Replies to the first credential prompts, in order (screenshots of token-protected sessions).
|
||||||
|
pub answers: Vec<String>,
|
||||||
|
/// How long to wait before taking the screenshot.
|
||||||
|
pub shot_delay: std::time::Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors from worker threads, drained into toasts each frame.
|
/// Errors from worker threads, drained into toasts each frame.
|
||||||
@@ -108,8 +114,10 @@ pub struct App {
|
|||||||
pub tabs_ui: tabs::TabsUi,
|
pub tabs_ui: tabs::TabsUi,
|
||||||
pub latency: Option<(String, Result<i64, String>)>,
|
pub latency: Option<(String, Result<i64, String>)>,
|
||||||
pub update: Option<nexus_core::updater::UpdateInfo>,
|
pub update: Option<nexus_core::updater::UpdateInfo>,
|
||||||
screenshot: Option<(PathBuf, u32)>,
|
screenshot: Option<(PathBuf, std::time::Instant, std::time::Duration)>,
|
||||||
shot_settle: u32,
|
shot_settle: u32,
|
||||||
|
actions: Vec<String>,
|
||||||
|
answers: Vec<String>,
|
||||||
run_local: Option<(String, Option<String>)>,
|
run_local: Option<(String, Option<String>)>,
|
||||||
/// Apply `prefs.sidebar_width` to the panel on the next frame (after Settings changed it).
|
/// Apply `prefs.sidebar_width` to the panel on the next frame (after Settings changed it).
|
||||||
resize_sidebar: bool,
|
resize_sidebar: bool,
|
||||||
@@ -155,13 +163,15 @@ impl App {
|
|||||||
tabs_ui: Default::default(),
|
tabs_ui: Default::default(),
|
||||||
latency: None,
|
latency: None,
|
||||||
update: None,
|
update: None,
|
||||||
screenshot: opts.screenshot.map(|p| (p, 0)),
|
screenshot: opts.screenshot.map(|p| (p, std::time::Instant::now(), opts.shot_delay)),
|
||||||
shot_settle: 0,
|
shot_settle: 0,
|
||||||
|
actions: opts.actions,
|
||||||
|
answers: opts.answers,
|
||||||
run_local: opts.run_local.map(|c| (c, None)),
|
run_local: opts.run_local.map(|c| (c, None)),
|
||||||
resize_sidebar: false,
|
resize_sidebar: false,
|
||||||
};
|
};
|
||||||
if let Some(c) = opts.connect.as_deref() {
|
for c in &opts.connect {
|
||||||
match app.s.profiles.iter().find(|p| p.id == c || p.display_name == c).map(|p| p.id.clone()) {
|
match app.s.profiles.iter().find(|p| &p.id == c || &p.display_name == c).map(|p| p.id.clone()) {
|
||||||
Some(id) => app.connect_profile(&id, true),
|
Some(id) => app.connect_profile(&id, true),
|
||||||
None => eprintln!("--connect: no profile named {c:?}"),
|
None => eprintln!("--connect: no profile named {c:?}"),
|
||||||
}
|
}
|
||||||
@@ -479,6 +489,9 @@ impl App {
|
|||||||
self.s.success(format!("Recording saved ({:.0}s, {events} events)", duration));
|
self.s.success(format!("Recording saved ({:.0}s, {events} events)", duration));
|
||||||
}
|
}
|
||||||
Event::SftpProgress { session, progress } => self.sftp.on_progress(&session, progress),
|
Event::SftpProgress { session, progress } => self.sftp.on_progress(&session, progress),
|
||||||
|
Event::SecretPrompt { session, title, message, reply } if !self.answers.is_empty() => {
|
||||||
|
let _ = (session, title, message, reply.send(self.answers.remove(0)));
|
||||||
|
}
|
||||||
Event::SecretPrompt { session, title, message, reply } => {
|
Event::SecretPrompt { session, title, message, reply } => {
|
||||||
self.prompts.push_back(Prompt::Secret { session, title, message, value: String::new(), reply });
|
self.prompts.push_back(Prompt::Secret { session, title, message, value: String::new(), reply });
|
||||||
}
|
}
|
||||||
@@ -807,11 +820,21 @@ impl App {
|
|||||||
// --- screenshot automation ---------------------------------------------------------
|
// --- screenshot automation ---------------------------------------------------------
|
||||||
|
|
||||||
fn screenshot(&mut self, ctx: &egui::Context) {
|
fn screenshot(&mut self, ctx: &egui::Context) {
|
||||||
let Some((path, frames)) = &mut self.screenshot else {
|
let Some((path, started, delay)) = &mut self.screenshot else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
*frames += 1;
|
let (elapsed, delay) = (started.elapsed(), *delay);
|
||||||
if *frames < 40 {
|
if !self.actions.is_empty() && elapsed >= delay / 2 {
|
||||||
|
for a in std::mem::take(&mut self.actions) {
|
||||||
|
if !chrome::run_labelled(self, ctx, &a) {
|
||||||
|
eprintln!("--do: no palette command {a:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = path.clone();
|
||||||
|
if elapsed < delay {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(15));
|
std::thread::sleep(std::time::Duration::from_millis(15));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
return;
|
return;
|
||||||
@@ -824,7 +847,7 @@ impl App {
|
|||||||
if let Some(img) = shot {
|
if let Some(img) = shot {
|
||||||
let [w, h] = img.size;
|
let [w, h] = img.size;
|
||||||
let raw: Vec<u8> = img.pixels.iter().flat_map(|p| p.to_array()).collect();
|
let raw: Vec<u8> = img.pixels.iter().flat_map(|p| p.to_array()).collect();
|
||||||
if let Err(e) = image::save_buffer(&*path, &raw, w as u32, h as u32, image::ExtendedColorType::Rgba8) {
|
if let Err(e) = image::save_buffer(&path, &raw, w as u32, h as u32, image::ExtendedColorType::Rgba8) {
|
||||||
eprintln!("screenshot: {e}");
|
eprintln!("screenshot: {e}");
|
||||||
}
|
}
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ enum Cmd {
|
|||||||
Theme(ThemeKind),
|
Theme(ThemeKind),
|
||||||
About,
|
About,
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
|
Sftp(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fuzzy subsequence match with bonuses for consecutive and leading hits (utils.js fuzzyMatch).
|
/// Fuzzy subsequence match with bonuses for consecutive and leading hits (utils.js fuzzyMatch).
|
||||||
@@ -503,14 +504,29 @@ fn commands(app: &App) -> Vec<(Cmd, String, &'static str, &'static str, String)>
|
|||||||
if app.s.focused_session().and_then(|f| app.s.pane(&f)).is_some_and(|p| matches!(p.view, View::Terminal(_))) {
|
if app.s.focused_session().and_then(|f| app.s.pane(&f)).is_some_and(|p| matches!(p.view, View::Terminal(_))) {
|
||||||
v.push((Cmd::SplitRight, "Split Pane Right".into(), "Terminal", icon::SQUARE_SPLIT_HORIZONTAL, keys("Mod+Shift+D")));
|
v.push((Cmd::SplitRight, "Split Pane Right".into(), "Terminal", icon::SQUARE_SPLIT_HORIZONTAL, keys("Mod+Shift+D")));
|
||||||
v.push((Cmd::SplitDown, "Split Pane Down".into(), "Terminal", icon::SQUARE_SPLIT_VERTICAL, keys("Mod+Shift+E")));
|
v.push((Cmd::SplitDown, "Split Pane Down".into(), "Terminal", icon::SQUARE_SPLIT_VERTICAL, keys("Mod+Shift+E")));
|
||||||
|
if let Some(id) = app.s.focused_session().filter(|f| app.s.pane(f).is_some_and(|p| p.info.protocol == Protocol::Ssh)) {
|
||||||
|
v.push((Cmd::Sftp(id), "SFTP Browser".into(), "Terminal", icon::FOLDER_OPEN, String::new()));
|
||||||
|
}
|
||||||
v.push((Cmd::Clear, "Clear Terminal".into(), "Terminal", icon::ERASER, String::new()));
|
v.push((Cmd::Clear, "Clear Terminal".into(), "Terminal", icon::ERASER, String::new()));
|
||||||
v.push((Cmd::Reset, "Reset Terminal".into(), "Terminal", icon::ARROW_COUNTER_CLOCKWISE, String::new()));
|
v.push((Cmd::Reset, "Reset Terminal".into(), "Terminal", icon::ARROW_COUNTER_CLOCKWISE, String::new()));
|
||||||
}
|
}
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the palette command with this label (case-insensitive); false when there is none.
|
||||||
|
pub fn run_labelled(app: &mut App, ctx: &egui::Context, label: &str) -> bool {
|
||||||
|
match commands(app).into_iter().find(|c| c.1.eq_ignore_ascii_case(label)) {
|
||||||
|
Some(c) => {
|
||||||
|
run(app, ctx, c.0);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run(app: &mut App, ctx: &egui::Context, c: Cmd) {
|
fn run(app: &mut App, ctx: &egui::Context, c: Cmd) {
|
||||||
match c {
|
match c {
|
||||||
|
Cmd::Sftp(id) => app.s.request(Request::Sftp(id)),
|
||||||
Cmd::Connect(id) => app.connect_profile(&id, false),
|
Cmd::Connect(id) => app.connect_profile(&id, false),
|
||||||
Cmd::NewProfile(p) => app.s.request(Request::NewProfile { protocol: Some(p), folder: String::new() }),
|
Cmd::NewProfile(p) => app.s.request(Request::NewProfile { protocol: Some(p), folder: String::new() }),
|
||||||
Cmd::Quick => app.quick.open(),
|
Cmd::Quick => app.quick.open(),
|
||||||
|
|||||||
@@ -191,7 +191,12 @@ impl IrcView {
|
|||||||
pub fn on_event(&mut self, event: irc::Event) {
|
pub fn on_event(&mut self, event: irc::Event) {
|
||||||
match route(&event) {
|
match route(&event) {
|
||||||
Route::Rooms => {
|
Route::Rooms => {
|
||||||
|
// Show the first room once auto-join lands, rather than leaving the server log up.
|
||||||
|
let first = self.joined.is_empty() && self.active == SERVER;
|
||||||
self.joined = event.rooms;
|
self.joined = event.rooms;
|
||||||
|
if first && let Some(r) = self.joined.first().map(|r| r.name.clone()) {
|
||||||
|
self.set_active(&r);
|
||||||
|
}
|
||||||
for r in self.joined.clone() {
|
for r in self.joined.clone() {
|
||||||
self.target(&r.name);
|
self.target(&r.name);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-3
@@ -33,7 +33,16 @@ use nexus_core::service::Service;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let mut opts = app::Options { screenshot: None, theme: None, run_local: None, open: None, connect: None };
|
let mut opts = app::Options {
|
||||||
|
screenshot: None,
|
||||||
|
theme: None,
|
||||||
|
run_local: None,
|
||||||
|
open: None,
|
||||||
|
connect: vec![],
|
||||||
|
actions: vec![],
|
||||||
|
answers: vec![],
|
||||||
|
shot_delay: std::time::Duration::from_millis(600),
|
||||||
|
};
|
||||||
let mut size = [1360.0, 860.0];
|
let mut size = [1360.0, 860.0];
|
||||||
let mut data_dir = None;
|
let mut data_dir = None;
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
@@ -42,7 +51,14 @@ fn main() -> eframe::Result {
|
|||||||
"--theme" => opts.theme = args.next(),
|
"--theme" => opts.theme = args.next(),
|
||||||
"--run-local" => opts.run_local = args.next(),
|
"--run-local" => opts.run_local = args.next(),
|
||||||
"--open" => opts.open = args.next(),
|
"--open" => opts.open = args.next(),
|
||||||
"--connect" => opts.connect = args.next(),
|
"--connect" => opts.connect.extend(args.next()),
|
||||||
|
"--do" => opts.actions.extend(args.next()),
|
||||||
|
"--answer" => opts.answers.extend(args.next()),
|
||||||
|
"--shot-delay" => {
|
||||||
|
if let Some(ms) = args.next().and_then(|s| s.parse().ok()) {
|
||||||
|
opts.shot_delay = std::time::Duration::from_millis(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
"--screenshot" => opts.screenshot = args.next().map(Into::into),
|
"--screenshot" => opts.screenshot = args.next().map(Into::into),
|
||||||
// Use another data directory (testing, screenshots, portable installs).
|
// Use another data directory (testing, screenshots, portable installs).
|
||||||
"--data-dir" => data_dir = args.next(),
|
"--data-dir" => data_dir = args.next(),
|
||||||
@@ -57,7 +73,7 @@ fn main() -> eframe::Result {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!(
|
println!(
|
||||||
"usage: nexus-terminal [--theme pro|midnight|light] [--data-dir DIR] [--connect NAME] [--open WINDOW] [--run-local CMD] [--screenshot out.png] [--size WxH] [--version]"
|
"usage: nexus-terminal [--theme pro|midnight|light] [--data-dir DIR] [--connect NAME]... [--open WINDOW] [--do PALETTE-COMMAND]... [--answer SECRET]... [--shot-delay MS] [--run-local CMD] [--screenshot out.png] [--size WxH] [--version]"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ impl Recordings {
|
|||||||
let mut open = self.open;
|
let mut open = self.open;
|
||||||
egui::Window::new(format!("{} Session Recordings", icon::RECORD))
|
egui::Window::new(format!("{} Session Recordings", icon::RECORD))
|
||||||
.id(egui::Id::new("recordings"))
|
.id(egui::Id::new("recordings"))
|
||||||
|
.pivot(egui::Align2::CENTER_CENTER)
|
||||||
|
.default_pos(ctx.content_rect().center())
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.default_size([640.0, 440.0])
|
.default_size([640.0, 440.0])
|
||||||
@@ -298,6 +300,8 @@ impl Recordings {
|
|||||||
let mut open = true;
|
let mut open = true;
|
||||||
egui::Window::new(format!("{} {}", icon::PLAY_CIRCLE, p.title))
|
egui::Window::new(format!("{} {}", icon::PLAY_CIRCLE, p.title))
|
||||||
.id(egui::Id::new("rec-playback"))
|
.id(egui::Id::new("rec-playback"))
|
||||||
|
.pivot(egui::Align2::CENTER_CENTER)
|
||||||
|
.default_pos(ctx.content_rect().center())
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.default_size([820.0, 520.0])
|
.default_size([820.0, 520.0])
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ impl Snippets {
|
|||||||
let mut open = self.open;
|
let mut open = self.open;
|
||||||
egui::Window::new(format!("{} Snippets", icon::CODE))
|
egui::Window::new(format!("{} Snippets", icon::CODE))
|
||||||
.id(egui::Id::new("snippets"))
|
.id(egui::Id::new("snippets"))
|
||||||
|
.pivot(egui::Align2::CENTER_CENTER)
|
||||||
|
.default_pos(ctx.content_rect().center())
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.default_size([720.0, 440.0])
|
.default_size([720.0, 440.0])
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ impl Triggers {
|
|||||||
let mut open = self.open;
|
let mut open = self.open;
|
||||||
egui::Window::new(format!("{} Trigger Rules", icon::LIGHTNING))
|
egui::Window::new(format!("{} Trigger Rules", icon::LIGHTNING))
|
||||||
.id(egui::Id::new("triggers"))
|
.id(egui::Id::new("triggers"))
|
||||||
|
.pivot(egui::Align2::CENTER_CENTER)
|
||||||
|
.default_pos(ctx.content_rect().center())
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.default_size([620.0, 380.0])
|
.default_size([620.0, 380.0])
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
//! Fills a data directory with demo profiles, snippets, known hosts, trigger rules and recordings
|
||||||
|
//! for the user-guide screenshots (docs/guide/screenshots.sh).
|
||||||
|
//!
|
||||||
|
//! cargo run -p nexus-core --example demo_data -- DATA_DIR SSH_KEY
|
||||||
|
|
||||||
|
use nexus_core::config::AppConfig;
|
||||||
|
use nexus_core::models::*;
|
||||||
|
use nexus_core::service::Service;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let (Some(dir), Some(key)) = (args.next(), args.next()) else {
|
||||||
|
eprintln!("usage: demo_data DATA_DIR SSH_KEY");
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
let config = AppConfig::in_dir(dir.into());
|
||||||
|
std::fs::create_dir_all(&config.vault_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&config.recordings_dir).unwrap();
|
||||||
|
let svc = Service::open(config).unwrap();
|
||||||
|
let user = std::env::var("USER").unwrap_or_else(|_| "andy".into());
|
||||||
|
|
||||||
|
let mk = |proto, name: &str, host: &str, port: i64, folder: &str, fav: bool| {
|
||||||
|
let mut p = Profile::new(proto);
|
||||||
|
p.display_name = name.into();
|
||||||
|
p.host = host.into();
|
||||||
|
if port != 0 {
|
||||||
|
p.port = port;
|
||||||
|
}
|
||||||
|
p.folder = folder.into();
|
||||||
|
p.favourite = fav;
|
||||||
|
p
|
||||||
|
};
|
||||||
|
let ssh = |name: &str, folder: &str, fav: bool, tags: &[&str]| {
|
||||||
|
let mut p = mk(Protocol::Ssh, name, "127.0.0.1", 42022, folder, fav);
|
||||||
|
p.username = user.clone();
|
||||||
|
p.tags = tags.iter().map(|t| t.to_string()).collect();
|
||||||
|
let s = p.ssh.as_mut().unwrap();
|
||||||
|
s.private_key_path = key.clone();
|
||||||
|
s.auth_methods = vec!["publickey".into()];
|
||||||
|
s.host_key_policy = HostKeyPolicy::AcceptNew;
|
||||||
|
p
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut profiles = vec![];
|
||||||
|
let mut atlas = ssh("atlas", "Production", true, &["web", "eu-west"]);
|
||||||
|
atlas.description = "Primary web node".into();
|
||||||
|
atlas.notes = "Deploys go out from here. Be nice to it.".into();
|
||||||
|
profiles.push(atlas);
|
||||||
|
let mut db = ssh("db-primary", "Production", false, &["db"]);
|
||||||
|
db.description = "Postgres primary, via the bastion".into();
|
||||||
|
db.ssh.as_mut().unwrap().jump_hosts = vec![JumpHost {
|
||||||
|
host: "bastion.example.net".into(),
|
||||||
|
port: 22,
|
||||||
|
username: user.clone(),
|
||||||
|
auth_method: "publickey".into(),
|
||||||
|
secret_mode: SecretMode::Saved,
|
||||||
|
private_key_path: key.clone(),
|
||||||
|
}];
|
||||||
|
db.ssh.as_mut().unwrap().port_forwards = vec![PortForward {
|
||||||
|
id: new_id(),
|
||||||
|
direction: ForwardDirection::Local,
|
||||||
|
bind_host: "127.0.0.1".into(),
|
||||||
|
bind_port: 15432,
|
||||||
|
dest_host: "127.0.0.1".into(),
|
||||||
|
dest_port: 5432,
|
||||||
|
enabled: true,
|
||||||
|
label: "Postgres".into(),
|
||||||
|
}];
|
||||||
|
profiles.push(db);
|
||||||
|
let mut hello = ssh("hello-ssh", "Production", false, &["demo"]);
|
||||||
|
hello.ssh.as_mut().unwrap().host_key_policy = HostKeyPolicy::Ask;
|
||||||
|
hello.description = "Same box, but asks about its host key".into();
|
||||||
|
profiles.push(hello);
|
||||||
|
profiles.push(ssh("build-runner", "Lab", false, &["ci"]));
|
||||||
|
|
||||||
|
let mut router = mk(Protocol::Telnet, "Core router", "192.0.2.1", 0, "Lab", false);
|
||||||
|
router.description = "Out-of-band console".into();
|
||||||
|
profiles.push(router);
|
||||||
|
let mut console = mk(Protocol::Serial, "Switch console", "/dev/ttyUSB0", 0, "Lab", false);
|
||||||
|
console.serial.as_mut().unwrap().baud_rate = 9600;
|
||||||
|
profiles.push(console);
|
||||||
|
let mut sensor = mk(Protocol::Raw, "Sensor feed", "127.0.0.1", 42023, "Lab", false);
|
||||||
|
let raw = sensor.raw.as_mut().unwrap();
|
||||||
|
raw.show_timestamps = true;
|
||||||
|
raw.show_direction = true;
|
||||||
|
raw.line_ending = LineEnding::Lf;
|
||||||
|
raw.payload_presets = vec![
|
||||||
|
PayloadPreset { id: new_id(), name: "Status".into(), payload: "U1RBVFVT".into(), format: "text".into() },
|
||||||
|
PayloadPreset { id: new_id(), name: "Reset".into(), payload: "UkVTRVQ=".into(), format: "text".into() },
|
||||||
|
];
|
||||||
|
profiles.push(sensor);
|
||||||
|
|
||||||
|
let mut irc = mk(Protocol::Irc, "Libera Chat", "127.0.0.1", 42667, "Home", true);
|
||||||
|
let ic = irc.irc.as_mut().unwrap();
|
||||||
|
ic.nick = "andy".into();
|
||||||
|
ic.real_name = "Andy Dixon".into();
|
||||||
|
ic.use_tls = false;
|
||||||
|
ic.auto_join_rooms = vec!["#nexus".into(), "#rust".into()];
|
||||||
|
profiles.push(irc);
|
||||||
|
let mut gh = mk(Protocol::Github, "GitHub", "github.com", 0, "Home", false);
|
||||||
|
gh.github.as_mut().unwrap().base_url = "http://127.0.0.1:42080".into();
|
||||||
|
profiles.push(gh);
|
||||||
|
let mut vnc = mk(Protocol::Vnc, "Studio desktop", "127.0.0.1", 42900, "Home", false);
|
||||||
|
vnc.vnc.as_mut().unwrap().vnc_host = "127.0.0.1".into();
|
||||||
|
vnc.vnc.as_mut().unwrap().vnc_port = 42900;
|
||||||
|
vnc.vnc.as_mut().unwrap().scale_to_fit = true;
|
||||||
|
profiles.push(vnc);
|
||||||
|
|
||||||
|
for p in profiles {
|
||||||
|
svc.create_profile(p).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, cmd, desc, folder) in [
|
||||||
|
("Disk usage", "df -h | sort -k5 -r | head", "Fullest filesystems first", "Diagnostics"),
|
||||||
|
("Who's hogging CPU", "ps aux --sort=-%cpu | head -n 8", "Top processes by CPU", "Diagnostics"),
|
||||||
|
("Tail a service", "journalctl -fu {{service}}", "Asks which service", "Logs"),
|
||||||
|
("Stamp the log", "echo '--- {{hostname}} {{date}} {{time}} ---'", "Drop a marker in the scrollback", "Logs"),
|
||||||
|
("Restart nginx", "sudo systemctl restart nginx && systemctl status nginx --no-pager", "", "Deploy"),
|
||||||
|
] {
|
||||||
|
svc.save_snippet(Snippet {
|
||||||
|
id: String::new(),
|
||||||
|
name: name.into(),
|
||||||
|
command: cmd.into(),
|
||||||
|
description: desc.into(),
|
||||||
|
folder: folder.into(),
|
||||||
|
tags: vec![],
|
||||||
|
created_at: now(),
|
||||||
|
updated_at: now(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (host, port, kind, fp) in [
|
||||||
|
("bastion.example.net", 22, "ssh-ed25519", "SHA256:x1rT0pV6c1m9QbHq4nWJcF5gk1Yt3uZ3w8sLrM2dE0Q"),
|
||||||
|
("192.0.2.40", 22, "ecdsa-sha2-nistp256", "SHA256:9Hq0b2VtZr6y1oQwA7mXJ4sEo5kP8fN3cLdT6uB2iRg"),
|
||||||
|
("git.example.org", 2222, "ssh-rsa", "SHA256:Qm7cV2pK9zR1tY5uW3xN8bL0aE6sD4fG2hJ1kM7nP5o"),
|
||||||
|
] {
|
||||||
|
let n = now();
|
||||||
|
svc.store
|
||||||
|
.save_known_host(&KnownHost {
|
||||||
|
id: String::new(),
|
||||||
|
host: host.into(),
|
||||||
|
port,
|
||||||
|
key_type: kind.into(),
|
||||||
|
fingerprint: fp.into(),
|
||||||
|
public_key: String::new(),
|
||||||
|
first_seen: n,
|
||||||
|
last_seen: n,
|
||||||
|
trusted: true,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
svc.config.data_dir.join("triggers.json"),
|
||||||
|
r#"[{"name":"Errors","pattern":"(?i)\\b(error|fatal|panic)\\b","isRegex":true,"action":"both","caseSensitive":false,"enabled":true},
|
||||||
|
{"name":"Build finished","pattern":"BUILD SUCCESSFUL","isRegex":false,"action":"sound","caseSensitive":true,"enabled":true},
|
||||||
|
{"name":"Disk nearly full","pattern":"9[0-9]%","isRegex":true,"action":"highlight","caseSensitive":false,"enabled":false}]"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let start = 1_791_633_486u64;
|
||||||
|
for (i, (title, lines)) in [
|
||||||
|
("atlas — deploy", &["$ ./deploy.sh\r\n", "\x1b[32m✔\x1b[0m built in 41s\r\n", "\x1b[32m✔\x1b[0m rolled out to 4/4 nodes\r\n"][..]),
|
||||||
|
("Switch console", &["Switch> enable\r\n", "Switch# show version\r\n", "Uptime is 212 days\r\n"][..]),
|
||||||
|
("Local Machine", &["$ cargo test\r\n", "test result: \x1b[32mok\x1b[0m. 135 passed\r\n"][..]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let mut body = format!(
|
||||||
|
"{{\"version\":2,\"width\":100,\"height\":30,\"timestamp\":{},\"title\":\"{title}\",\"env\":{{\"TERM\":\"xterm-256color\"}}}}\n",
|
||||||
|
start - i as u64 * 86_400 * 3
|
||||||
|
);
|
||||||
|
for (j, l) in lines.iter().enumerate() {
|
||||||
|
body += &format!("[{}.5,\"o\",{}]\n", j * 2, serde_json::to_string(l).unwrap());
|
||||||
|
}
|
||||||
|
std::fs::write(svc.config.recordings_dir.join(format!("demo-{i}.cast")), body).unwrap();
|
||||||
|
}
|
||||||
|
println!("demo data written to {}", svc.config.data_dir.display());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user