From 7d14746d0d4a79cde474d13229b949f2c1c4fb9d Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 14 Aug 2026 14:20:11 +0200 Subject: [PATCH 1/3] fix allow axbitrary entries --- src/main.rs | 8 +++++++- src/tui/entries.rs | 32 +++++++++++++++++++++++++++++--- src/tui/mod.rs | 6 +++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 245b9ea..d8151fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,11 +80,17 @@ fn main() -> Result<()> { let config_path = resolve_config_path(cli.config); let json = std::fs::read_to_string(&config_path) .with_context(|| format!("failed to read config file {}", config_path.display()))?; - let config = load_config(&json)?; + let mut config = load_config(&json)?; check_base_exists("deployments_base", &config.deployments_base, &config_path)?; check_base_exists("venvs_base", &config.deployments_base, &config_path)?; + // Populate the config with deployments found on disk so they are available + // to both `lookup_entry` and the TUI. Configured entries take precedence. + for (label, entry) in config.discovered_entries() { + config.entries.entry(label).or_insert(entry); + } + match cli.command { Command::Pick => { if let Some(launch) = tui::run(&config, DEFAULT_ENV_FILE)? { diff --git a/src/tui/entries.rs b/src/tui/entries.rs index 5729e38..46c92c6 100644 --- a/src/tui/entries.rs +++ b/src/tui/entries.rs @@ -11,8 +11,8 @@ use crate::config::LauncherEntry; /// Which entry point to launch for the selected entry. #[derive(Clone, Copy, PartialEq)] enum LaunchChoice { - Gui, Server, + Gui, } /// The entries list: configured entries plus discovered deployments, with a @@ -117,7 +117,7 @@ impl EntriesTab { let items: Vec = self .entries .iter() - .map(|(label, _)| ListItem::new(label.as_str())) + .map(|(label, _)| ListItem::new(display_label(label))) .collect(); let list = List::new(items) .block(Block::default().borders(Borders::ALL).title("Entries")) @@ -158,8 +158,8 @@ impl EntriesTab { }; let text = Text::from(vec![ - Line::styled(" Launch GUI ", gui_style), Line::styled(" Launch Server ", server_style), + Line::styled(" Launch GUI ", gui_style), Line::from(""), Line::from("↑/↓ choose · Enter confirm · Esc cancel"), ]); @@ -168,6 +168,12 @@ impl EntriesTab { } } +/// The label shown in the list: the default `/.venv` virtualenv suffix (used by +/// discovered deployments) is dropped for readability. +fn display_label(label: &str) -> &str { + label.strip_suffix("/.venv").unwrap_or(label) +} + /// A rectangle centered within `area`, sized as a percentage of it. fn centered_rect(width_pct: u16, height_pct: u16, area: Rect) -> Rect { let [area] = Layout::vertical([Constraint::Percentage(height_pct)]) @@ -178,3 +184,23 @@ fn centered_rect(width_pct: u16, height_pct: u16, area: Rect) -> Rect { .areas(area); area } + +#[cfg(test)] +mod tests { + use super::display_label; + + #[test] + fn strips_default_venv_suffix() { + assert_eq!(display_label("mydeploy/.venv"), "mydeploy"); + } + + #[test] + fn keeps_named_venv() { + assert_eq!(display_label("mydeploy/pxii"), "mydeploy/pxii"); + } + + #[test] + fn leaves_plain_labels_untouched() { + assert_eq!(display_label("x10sa"), "x10sa"); + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 6960587..78e52b7 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -36,13 +36,13 @@ struct App { impl App { fn new(config: &Config, env_file: &str) -> Self { - // Configured entries first, then the deployments discovered on disk. - let mut entries: Vec<(String, LauncherEntry)> = config + // Entries (configured + deployments discovered at startup) come straight + // from the config. + let entries: Vec<(String, LauncherEntry)> = config .entries .iter() .map(|(label, entry)| (label.clone(), entry.clone())) .collect(); - entries.extend(config.discovered_entries()); App { entries: EntriesTab::new(entries), -- 2.54.0 From 50569b1d37518aaad88fba281f3379ad4a47ddcc Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 14 Aug 2026 14:31:51 +0200 Subject: [PATCH 2/3] fix: allow more than one venv per deployment --- src/config.rs | 28 +++++++++++++++++++--------- src/main.rs | 1 - src/tui/entries.rs | 34 ++++++++++++++++++---------------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0240671..c6dddd5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,11 +56,13 @@ pub struct Config { } impl Config { - /// Discover launchable deployments on disk under `deployments_base`, one - /// entry per (deployment, virtualenv) pair, labelled `"/"`. + /// Discover launchable deployments on disk under `deployments_base`. /// /// These are the "custom" deployments that are present on the machine but - /// not named in the config. Entry points use the configured defaults. + /// not named in the config; entry points use the configured defaults. Each + /// is labelled by its deployment name, with the virtualenv kept inside the + /// entry — only when a deployment has more than one virtualenv is the venv + /// name appended (`"/"`) to keep the labels unique. pub fn discovered_entries(&self) -> Vec<(String, LauncherEntry)> { let mut found = Vec::new(); let Ok(dirs) = fs::read_dir(&self.deployments_base) else { @@ -74,13 +76,21 @@ impl Config { let dir = deployment.path(); let name = deployment.file_name().to_string_lossy().into_owned(); - for venv in detect_venvs(&dir).unwrap_or_default() { - let venv_name = venv - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); + let venvs = detect_venvs(&dir).unwrap_or_default(); + let disambiguate = venvs.len() > 1; + + for venv in venvs { + let label = if disambiguate { + let venv_name = venv + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + format!("{name}/{venv_name}") + } else { + name.clone() + }; found.push(( - format!("{name}/{venv_name}"), + label, LauncherEntry { python: venv.join("bin/python"), gui_file: join_relative(&dir, &self.default_gui_file), diff --git a/src/main.rs b/src/main.rs index d8151fe..baf6546 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,7 +83,6 @@ fn main() -> Result<()> { let mut config = load_config(&json)?; check_base_exists("deployments_base", &config.deployments_base, &config_path)?; - check_base_exists("venvs_base", &config.deployments_base, &config_path)?; // Populate the config with deployments found on disk so they are available // to both `lookup_entry` and the TUI. Configured entries take precedence. diff --git a/src/tui/entries.rs b/src/tui/entries.rs index 46c92c6..b28f294 100644 --- a/src/tui/entries.rs +++ b/src/tui/entries.rs @@ -27,7 +27,10 @@ pub struct EntriesTab { } impl EntriesTab { - pub fn new(entries: Vec<(String, LauncherEntry)>) -> Self { + pub fn new(mut entries: Vec<(String, LauncherEntry)>) -> Self { + // Production deployments first, then the rest, alphabetical within each. + entries.sort_by(|a, b| sort_key(&a.0).cmp(&sort_key(&b.0))); + let mut list_state = ListState::default(); if !entries.is_empty() { list_state.select(Some(0)); @@ -117,7 +120,7 @@ impl EntriesTab { let items: Vec = self .entries .iter() - .map(|(label, _)| ListItem::new(display_label(label))) + .map(|(label, _)| ListItem::new(label.as_str())) .collect(); let list = List::new(items) .block(Block::default().borders(Borders::ALL).title("Entries")) @@ -168,10 +171,12 @@ impl EntriesTab { } } -/// The label shown in the list: the default `/.venv` virtualenv suffix (used by -/// discovered deployments) is dropped for readability. -fn display_label(label: &str) -> &str { - label.strip_suffix("/.venv").unwrap_or(label) +/// Production deployment labels, pinned to the top of the list. +const PRODUCTION: [&str; 3] = ["x06da", "x06sa", "x10sa"]; + +/// Sort key placing production deployments first, then alphabetical by label. +fn sort_key(label: &str) -> (bool, &str) { + (!PRODUCTION.contains(&label), label) } /// A rectangle centered within `area`, sized as a percentage of it. @@ -187,20 +192,17 @@ fn centered_rect(width_pct: u16, height_pct: u16, area: Rect) -> Rect { #[cfg(test)] mod tests { - use super::display_label; + use super::sort_key; #[test] - fn strips_default_venv_suffix() { - assert_eq!(display_label("mydeploy/.venv"), "mydeploy"); + fn production_sorts_before_others() { + // Production wins even though its label sorts later alphabetically. + assert!(sort_key("x10sa") < sort_key("aaa")); } #[test] - fn keeps_named_venv() { - assert_eq!(display_label("mydeploy/pxii"), "mydeploy/pxii"); - } - - #[test] - fn leaves_plain_labels_untouched() { - assert_eq!(display_label("x10sa"), "x10sa"); + fn production_ordered_alphabetically() { + assert!(sort_key("x06da") < sort_key("x06sa")); + assert!(sort_key("x06sa") < sort_key("x10sa")); } } -- 2.54.0 From ffa13a2a8ab0589430bdcd9a2f3cba331d382d60 Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 14 Aug 2026 14:36:10 +0200 Subject: [PATCH 3/3] fix: rename -e to -d --- src/main.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index baf6546..0d5985e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,16 +56,16 @@ enum Command { /// Launch the AareGUI with default settings #[command(alias = "g")] Gui { - /// Config entry (deployment) to launch. Defaults to the detected beamline. - #[arg(long)] - entry: Option, + /// Deployment to launch. Defaults to the detected beamline. + #[arg(short = 'd', long)] + deployment: Option, }, /// Launch the AareDAQ server with default settings #[command(alias = "s")] Server { - /// Config entry (deployment) to launch. Defaults to the detected beamline. - #[arg(long)] - entry: Option, + /// Deployment to launch. Defaults to the detected beamline. + #[arg(short = 'd', long)] + deployment: Option, /// Env file (`key=value` per line) added to the server's environment. /// Ignored with a warning if it does not exist. @@ -101,10 +101,13 @@ fn main() -> Result<()> { launcher::launch_with_env(&launch.python, &launch.entrypoint, &env)?; } } - Command::Gui { entry } => launcher::launch_gui(lookup_entry(&config, entry)?)?, - Command::Server { entry, env_file } => { + Command::Gui { deployment } => launcher::launch_gui(lookup_entry(&config, deployment)?)?, + Command::Server { + deployment, + env_file, + } => { let env = env_file::load_env_file(&env_file); - launcher::launch_server(lookup_entry(&config, entry)?, &env)? + launcher::launch_server(lookup_entry(&config, deployment)?, &env)? } } -- 2.54.0