fix Down nav bound in restore pickers, formatting cleanup

This commit is contained in:
2026-06-09 17:34:42 +02:00
parent 40663a88ed
commit d0c5d006bd
3 changed files with 56 additions and 34 deletions
Generated
+1 -1
View File
@@ -540,7 +540,7 @@ dependencies = [
[[package]]
name = "notalterra"
version = "0.4.1"
version = "0.4.2"
dependencies = [
"anyhow",
"chrono",
+1
View File
@@ -45,6 +45,7 @@ After CI completes:
- **Sign once.** Wait for CI to go green on the unsigned commit, **then** amend with `--gpg-sign` + force-push. Do not amend between CI runs — each amend creates a new SHA, which triggers a new CI run and leaves stale Pages deployments that block the next run.
- Normal push for fast-forward commits
- `--force-with-lease` for amended commits or tag refreshes
- **Start from a clean tag.** Before re-tagging, always delete the old tag locally AND remotely (`git tag -d v0.x.y && git push --delete origin v0.x.y`). If the old tag still exists, `git tag -s` will fail with "tag already exists" and you'll end up with a tag pointing to the wrong commit.
- If force-pushing, delete old tag and re-tag after the new commit lands
- Tag once, push once. Re-signing an existing tag pushes a new SHA and triggers another CI run with the same deployment conflict risk.
+54 -33
View File
@@ -143,9 +143,7 @@ fn refresh_stats(tui_state: &mut tui::AppState, save_folder: Option<&Path>) {
tui_state.live_save_count = live;
tui_state.backup_count = bak;
tui_state.has_ini_backup = ini;
tui_state.context_path = Some(
crate::config::get_backup_root().display().to_string(),
);
tui_state.context_path = Some(crate::config::get_backup_root().display().to_string());
}
// ── main loop ──────────────────────────────────────────────────────────────
@@ -245,15 +243,17 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<()> {
// Main menu: show path based on highlighted item
{
let sel = menu_state.selected().unwrap_or(0);
let br = crate::config::get_backup_root().to_string_lossy().to_string();
let br = crate::config::get_backup_root()
.to_string_lossy()
.to_string();
let ini_path_str = app.save_folder.as_ref().and_then(|sf| {
discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string())
});
app.tui_state.context_path = match sel {
0 | 1 => None, // save path (fallback)
3..=5 => Some(br), // backup root
7 => ini_path_str, // ini Config\Windows path
_ => Some(String::new()), // blank / disclaimer / exit → no path
0 | 1 => None, // save path (fallback)
3..=5 => Some(br), // backup root
7 => ini_path_str, // ini Config\Windows path
_ => Some(String::new()), // blank / disclaimer / exit → no path
};
}
terminal.draw(|f| {
@@ -924,14 +924,18 @@ fn action_restore_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App)
let header = format!(" {:<38} {:>8}", "Backup", "Size");
let mut items: Vec<String> = vec![header, String::new()];
items.extend(backups
.iter()
.map(|p| {
let name = p.file_name().unwrap().to_string_lossy();
let size = std::fs::metadata(p).map(|m| format_size(m.len())).unwrap_or("?".into());
format!(" {:<42} {:>8}", format_backup_label(&name), size)
})
.collect::<Vec<String>>());
items.extend(
backups
.iter()
.map(|p| {
let name = p.file_name().unwrap().to_string_lossy();
let size = std::fs::metadata(p)
.map(|m| format_size(m.len()))
.unwrap_or("?".into());
format!(" {:<42} {:>8}", format_backup_label(&name), size)
})
.collect::<Vec<String>>(),
);
// Prepend empty description entries for header + blank
let descs: Vec<String> = std::iter::once(String::new())
.chain(std::iter::once(String::new()))
@@ -955,7 +959,9 @@ fn action_restore_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App)
loop {
// Restore picker: show backup root in header
app.tui_state.context_path = Some(
crate::config::get_backup_root().to_string_lossy().to_string(),
crate::config::get_backup_root()
.to_string_lossy()
.to_string(),
);
terminal.draw(|f| {
tui::draw_picker(f, &app.tui_state, &item_refs, &desc_refs, &mut state, true);
@@ -968,21 +974,26 @@ fn action_restore_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App)
}
KeyCode::Down => {
let i = state.selected().unwrap_or(0);
state.select(Some((i + 1).min(backups.len().saturating_sub(1))));
state.select(Some((i + 1).min(items.len().saturating_sub(1))));
}
KeyCode::Enter => {
let idx = state.selected().unwrap_or(2).max(2).saturating_sub(2);
if idx >= backups.len() { continue; }
if idx >= backups.len() {
continue;
}
let chosen = &backups[idx];
let name = chosen.file_name().unwrap().to_string_lossy().to_string();
// Block restore on corrupt archives
if !ops::check_tar_gz_integrity(chosen) {
ok_dialog(terminal, app, "Corrupt Backup",
ok_dialog(
terminal,
app,
"Corrupt Backup",
"This backup archive appears to be corrupt or\n\
incomplete and cannot be restored.\n\
\n\
Create a new full backup to replace it."
Create a new full backup to replace it.",
)?;
continue;
}
@@ -1071,18 +1082,21 @@ fn run_ini_submenu<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Res
const INI_SKIP: &[usize] = &[3];
let ini_max = 4usize;
let ini_path_str = app.save_folder.as_ref().and_then(|sf| {
discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string())
});
let ini_path_str = app
.save_folder
.as_ref()
.and_then(|sf| discovery::derive_ini_path(sf).map(|p| p.to_string_lossy().to_string()));
loop {
// Ini submenu: show path based on highlighted item
{
let sel = state.selected().unwrap_or(0);
let br = crate::config::get_backup_root().to_string_lossy().to_string();
let br = crate::config::get_backup_root()
.to_string_lossy()
.to_string();
app.tui_state.context_path = match sel {
0 | 1 => Some(br), // backup root (backup/restore)
2 => ini_path_str.clone(), // Config\Windows path (delete)
_ => Some(String::new()), // blank / back → nothing
0 | 1 => Some(br), // backup root (backup/restore)
2 => ini_path_str.clone(), // Config\Windows path (delete)
_ => Some(String::new()), // blank / back → nothing
};
}
terminal.draw(|f| {
@@ -1212,20 +1226,27 @@ fn ini_restore_action<B: Backend>(
let mut items: Vec<String> = vec![header, String::new()];
items.extend(backups.iter().map(|p| {
let name = p.file_name().unwrap().to_string_lossy();
let size = std::fs::metadata(p).map(|m| format_size(m.len())).unwrap_or("?".into());
let size = std::fs::metadata(p)
.map(|m| format_size(m.len()))
.unwrap_or("?".into());
format!(" {:<42} {:>8}", format_backup_label(&name), size)
}));
let item_refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
let ini_descs: Vec<&str> = std::iter::once("")
.chain(std::iter::once(""))
.chain(std::iter::repeat_n("Restore .ini files from this backup", backups.len()))
.chain(std::iter::repeat_n(
"Restore .ini files from this backup",
backups.len(),
))
.collect::<Vec<_>>();
let mut state = ListState::default().with_selected(Some(2)); // skip header + blank
loop {
// Restore picker: show backup root in header
app.tui_state.context_path = Some(
crate::config::get_backup_root().to_string_lossy().to_string(),
crate::config::get_backup_root()
.to_string_lossy()
.to_string(),
);
terminal.draw(|f| {
tui::draw_picker(f, &app.tui_state, &item_refs, &ini_descs, &mut state, true);
@@ -1238,7 +1259,7 @@ fn ini_restore_action<B: Backend>(
}
KeyCode::Down => {
let i = state.selected().unwrap_or(0);
state.select(Some((i + 1).min(backups.len().saturating_sub(1))));
state.select(Some((i + 1).min(items.len().saturating_sub(1))));
}
KeyCode::Enter => {
let idx = state.selected().unwrap_or(0);
@@ -1693,7 +1714,7 @@ fn format_backup_label(filename: &str) -> String {
let date_str = if let Some(pos) = rest.find(|c: char| c.is_ascii_digit()) {
let slice = &rest[pos..];
if slice.len() >= 17 {
let date_part = &slice[..10]; // 2026-06-09
let date_part = &slice[..10]; // 2026-06-09
let time_part = &slice[11..17]; // 125430
let h = &time_part[..2];
let m = &time_part[2..4];