Fix/allow entry selection #21
+19
-9
@@ -56,11 +56,13 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Discover launchable deployments on disk under `deployments_base`, one
|
||||
/// entry per (deployment, virtualenv) pair, labelled `"<deployment>/<venv>"`.
|
||||
/// 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 (`"<deployment>/<venv>"`) 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),
|
||||
|
||||
+19
-11
@@ -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<String>,
|
||||
/// Deployment to launch. Defaults to the detected beamline.
|
||||
#[arg(short = 'd', long)]
|
||||
deployment: Option<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
/// Deployment to launch. Defaults to the detected beamline.
|
||||
#[arg(short = 'd', long)]
|
||||
deployment: Option<String>,
|
||||
|
||||
/// Env file (`key=value` per line) added to the server's environment.
|
||||
/// Ignored with a warning if it does not exist.
|
||||
@@ -80,10 +80,15 @@ 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 => {
|
||||
@@ -96,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)?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-3
@@ -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
|
||||
@@ -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));
|
||||
@@ -158,8 +161,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 +171,14 @@ impl EntriesTab {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn centered_rect(width_pct: u16, height_pct: u16, area: Rect) -> Rect {
|
||||
let [area] = Layout::vertical([Constraint::Percentage(height_pct)])
|
||||
@@ -178,3 +189,20 @@ fn centered_rect(width_pct: u16, height_pct: u16, area: Rect) -> Rect {
|
||||
.areas(area);
|
||||
area
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::sort_key;
|
||||
|
||||
#[test]
|
||||
fn production_sorts_before_others() {
|
||||
// Production wins even though its label sorts later alphabetically.
|
||||
assert!(sort_key("x10sa") < sort_key("aaa"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_ordered_alphabetically() {
|
||||
assert!(sort_key("x06da") < sort_key("x06sa"));
|
||||
assert!(sort_key("x06sa") < sort_key("x10sa"));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user