diff --git a/TESTING.md b/TESTING.md index e6c8585..dbf523d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -75,6 +75,10 @@ representative repositories. - [ ] Server profiles can be added, authenticated, selected, edited, renamed, and deleted. Blank token on edit preserves the existing token; removing the last server leaves the server manager available. +- [ ] Select a different server, quit, and relaunch without `--server`; the TUI + restores that server. Relaunch with `--server NAME`; the explicit server + wins and becomes the restored server after a clean exit. Renaming or + deleting the remembered profile leaves a valid selection. - [ ] Editors move between fields with Tab/Shift-Tab, support cursor movement, Unicode insertion, Delete, and Backspace without navigating back, mask tokens, save with Ctrl-S, and cancel with Esc. diff --git a/crates/gitea/src/config.rs b/crates/gitea/src/config.rs index 341aa9c..8cc0574 100644 --- a/crates/gitea/src/config.rs +++ b/crates/gitea/src/config.rs @@ -30,6 +30,8 @@ pub struct TuiPreferences { pub refresh_seconds: u64, #[serde(default)] pub favorites: BTreeSet, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_server: Option, } impl Default for TuiPreferences { @@ -37,6 +39,7 @@ impl Default for TuiPreferences { Self { refresh_seconds: default_refresh_seconds(), favorites: BTreeSet::new(), + last_server: None, } } } @@ -150,6 +153,9 @@ impl Config { if old_url != new_url { migrate_favorites(&mut updated.tui.favorites, &old_url, new_url); } + if updated.tui.last_server.as_deref() == Some(original_name) { + updated.tui.last_server = Some(name.into()); + } updated.save()?; *self = updated; Ok(()) @@ -169,6 +175,9 @@ impl Config { { remove_favorites(&mut updated.tui.favorites, &removed_url); } + if updated.tui.last_server.as_deref() == Some(name) { + updated.tui.last_server = updated.servers.keys().next().cloned(); + } updated.save()?; *self = updated; Ok(()) @@ -182,6 +191,22 @@ impl Config { Ok(()) } + pub fn set_tui_last_server(&mut self, name: Option<&str>) -> Result<()> { + if let Some(name) = name + && !self.servers.contains_key(name) + { + return Err(format!("server profile {name:?} does not exist")); + } + if self.tui.last_server.as_deref() == name { + return Ok(()); + } + let mut updated = self.clone(); + updated.tui.last_server = name.map(str::to_owned); + updated.save()?; + *self = updated; + Ok(()) + } + pub fn is_tui_favorite(&self, pane: &str, server_url: &str, repository: &RepositoryId) -> bool { self.tui .favorites @@ -455,9 +480,11 @@ mod tests { .login("code.example", "secret", Provider::Forgejo) .unwrap(); config.set_tui_refresh_seconds(9).unwrap(); + config.set_tui_last_server(Some("code.example")).unwrap(); let mut loaded = Config::load_from(path).unwrap(); assert_eq!(loaded.tui.refresh_seconds, 9); + assert_eq!(loaded.tui.last_server.as_deref(), Some("code.example")); assert_eq!(loaded.servers["code.example"].token, "secret"); assert_eq!(loaded.servers["code.example"].provider, Provider::Forgejo); assert_eq!( @@ -484,6 +511,7 @@ mod tests { assert!(loaded.is_tui_favorite("issues", "https://new.example", &repository)); assert!(!loaded.servers.contains_key("code.example")); assert_eq!(loaded.servers["new.example"].token, "new-secret"); + assert_eq!(loaded.tui.last_server.as_deref(), Some("new.example")); assert!( fs::read_to_string(&loaded.path) .unwrap() @@ -506,6 +534,8 @@ mod tests { fs::metadata(&loaded.path).unwrap().permissions().mode() & 0o777, 0o600 ); + loaded.logout("new.example").unwrap(); + assert_eq!(loaded.tui.last_server, None); fs::remove_dir_all(directory).unwrap(); } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 7089ae1..0eda38f 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -257,6 +257,15 @@ impl App { }) } + pub fn persist_selected_server(&mut self) -> Result<(), String> { + let selected = self + .config + .servers + .contains_key(&self.server_name) + .then_some(self.server_name.as_str()); + self.config.set_tui_last_server(selected) + } + pub async fn handle_key(&mut self, key: KeyEvent) { self.last_input = Instant::now(); self.status_deadline = None; @@ -1675,6 +1684,14 @@ fn initial_selection(config: &Config, requested: Option<&str>) -> Result String { mod tests { use super::*; + fn config_with_servers() -> Config { + let mut config = Config::default(); + config.servers = [ + ( + "first.example".into(), + gotcha_gitea::ServerProfile { + url: "https://first.example".into(), + token: "first".into(), + provider: Provider::Gitea, + }, + ), + ( + "second.example".into(), + gotcha_gitea::ServerProfile { + url: "https://second.example".into(), + token: "second".into(), + provider: Provider::Forgejo, + }, + ), + ] + .into(); + config + } + #[test] fn changed_files_open_only_their_own_diff() { let diff = "diff --git a/one.txt b/one.txt\n--- a/one.txt\n+++ b/one.txt\n+one\ndiff --git a/two.txt b/two.txt\n--- a/two.txt\n+++ b/two.txt\n+two\n"; @@ -1941,6 +1982,29 @@ mod tests { ); } + #[test] + fn startup_prefers_the_last_server_unless_explicitly_overridden() { + let mut config = config_with_servers(); + config.tui.last_server = Some("second.example".into()); + + assert_eq!( + initial_selection(&config, None).unwrap().name.as_deref(), + Some("second.example") + ); + assert_eq!( + initial_selection(&config, Some("first.example")) + .unwrap() + .name + .as_deref(), + Some("first.example") + ); + config.tui.last_server = Some("removed.example".into()); + assert_eq!( + initial_selection(&config, None).unwrap().name.as_deref(), + Some("first.example") + ); + } + #[test] fn list_clicks_follow_the_rendered_offset_and_rows() { let area = ratatui::layout::Rect::new(10, 4, 30, 10); diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 1351d4e..2a7d26c 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -44,7 +44,9 @@ async fn main() -> Result<(), Box> { let result = run(&mut terminal, &mut app).await; execute!(io::stdout(), DisableMouseCapture)?; ratatui::restore(); - result + result?; + app.persist_selected_server()?; + Ok(()) } async fn run(