diff --git a/data/defaults.json b/data/defaults.json index 6afdaa1..718f8d1 100644 --- a/data/defaults.json +++ b/data/defaults.json @@ -1,5 +1,5 @@ { - "deployments_base": "/sls/mx/applications/aaredaq", + "deployments_base": "/sls/mx/applications/AareDAQ", "default_venv_name": ".venv", "default_gui_file": "src/aare/gui/gui.py", "default_server_file": "src/aare/daq/server.py", diff --git a/src/config.rs b/src/config.rs index a8a83ab..fb488ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -192,4 +192,53 @@ mod tests { "expected field path in error, got: {message}" ); } + + #[test] + fn join_relative_keeps_relative_paths() { + assert_eq!( + join_relative(Path::new("/base"), Path::new("sub/x.py")), + PathBuf::from("/base/sub/x.py") + ); + } + + #[test] + fn join_relative_strips_leading_slash() { + assert_eq!( + join_relative(Path::new("/base"), Path::new("/sub/x.py")), + PathBuf::from("/base/sub/x.py") + ); + } + + #[test] + fn absolute_entry_points_resolve_relative_to_dir() { + // A leading slash on an entry point is treated as relative to `dir`, + // not as an absolute path. + let json = r#"{ + "deployments_base": "/apps", + "default_venv_name": ".venv", + "default_gui_file": "src/gui.py", + "default_server_file": "src/server.py", + "map": { "x10sa": { "dir": "d", "gui_file": "/abs/gui.py" } } + }"#; + + let config = load_config(json).unwrap(); + assert_eq!( + config.entries["x10sa"].gui_file, + PathBuf::from("/apps/d/abs/gui.py") + ); + } + + #[test] + fn custom_venv_overrides_default() { + let config = load_config(SAMPLE).unwrap(); + // x10sa sets "venv": "pxii"; x06da falls back to ".venv". + assert_eq!( + config.entries["x10sa"].python, + PathBuf::from("/apps/aaredaq-pxii/pxii/bin/python") + ); + assert_eq!( + config.entries["x06da"].python, + PathBuf::from("/apps/aaredaq-pxiii/.venv/bin/python") + ); + } } diff --git a/src/main.rs b/src/main.rs index 7bb350d..d345401 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,17 +10,37 @@ use clap::{Parser, Subcommand}; use config::{Config, LauncherEntry, load_config}; use detect::detect_beamline; +/// Config file installed by the RPM, preferred when it exists. +const INSTALLED_CONFIG: &str = "/etc/aarel/defaults.json"; +/// Config file shipped in the source tree, used as a fallback. +const LOCAL_CONFIG: &str = "data/defaults.json"; + #[derive(Parser)] #[command(name = "aarel")] struct Cli { - /// Path to the launcher config file. - #[arg(long, default_value = "data/defaults.json")] - config: PathBuf, + /// Path to the launcher config file. Defaults to /etc/aarel/defaults.json, + /// falling back to ./data/defaults.json. + #[arg(long)] + config: Option, #[command(subcommand)] command: Command, } +/// Resolve the config path: the explicit `--config`, else the installed config +/// if present, else the local one. +fn resolve_config_path(config: Option) -> PathBuf { + if let Some(path) = config { + return path; + } + let installed = PathBuf::from(INSTALLED_CONFIG); + if installed.is_file() { + installed + } else { + PathBuf::from(LOCAL_CONFIG) + } +} + #[derive(Subcommand)] enum Command { /// Enter the TUI to choose what to launch @@ -45,12 +65,13 @@ enum Command { fn main() -> Result<()> { let cli = Cli::parse(); - let json = std::fs::read_to_string(&cli.config) - .with_context(|| format!("failed to read config file {}", cli.config.display()))?; + 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)?; - check_base_exists("deployments_base", &config.deployments_base, &cli.config)?; - check_base_exists("venvs_base", &config.deployments_base, &cli.config)?; + check_base_exists("deployments_base", &config.deployments_base, &config_path)?; + check_base_exists("venvs_base", &config.deployments_base, &config_path)?; match cli.command { Command::Pick => { diff --git a/src/tui/custom.rs b/src/tui/custom.rs index 29d35b3..c50e8f2 100644 --- a/src/tui/custom.rs +++ b/src/tui/custom.rs @@ -155,3 +155,54 @@ fn dir_list<'a>(title: &'a str, items: &'a [String], focused: bool) -> List<'a> .highlight_style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED)) .highlight_symbol("> ") } + +#[cfg(test)] +mod tests { + use super::*; + + fn items(n: usize) -> Vec { + (0..n).map(|i| i.to_string()).collect() + } + + #[test] + fn initial_state_selects_first_when_nonempty() { + assert_eq!(initial_state(&items(3)).selected(), Some(0)); + } + + #[test] + fn initial_state_selects_nothing_when_empty() { + assert_eq!(initial_state(&items(0)).selected(), None); + } + + #[test] + fn select_relative_moves_within_bounds() { + let items = items(3); + let mut state = initial_state(&items); + select_relative(&items, &mut state, 1); + assert_eq!(state.selected(), Some(1)); + } + + #[test] + fn select_relative_wraps_forward_past_end() { + let items = items(3); + let mut state = ListState::default(); + state.select(Some(2)); + select_relative(&items, &mut state, 1); + assert_eq!(state.selected(), Some(0)); + } + + #[test] + fn select_relative_wraps_backward_past_start() { + let items = items(3); + let mut state = initial_state(&items); + select_relative(&items, &mut state, -1); + assert_eq!(state.selected(), Some(2)); + } + + #[test] + fn select_relative_on_empty_is_noop() { + let mut state = ListState::default(); + select_relative(&items(0), &mut state, 1); + assert_eq!(state.selected(), None); + } +}