josie / alder-tools

Fix TUI sweep not ticking (7z ran forever) + j/k nav in editor

Two bugs in one:
1. The sweep tick was inside the match &mut mode block, which only ran
   AFTER event::poll returned (either a key was pressed or the poll
   timed out + continue'd). When no key was pressed, the poll timed out
   → continue → the tick never ran → the sweep never advanced → 7z ran
   forever. Fix: move the tick to the TOP of the loop (before draw +
   poll) so it runs every iteration regardless of input.
2. The editor used KeyCode::Down for navigation but the test harness
   (and users) expect j/k to work (same as the picker). Added j/k as
   Down/Up aliases in the editor. The old j/k reorder handlers are now
   </> (intuitive for ordering) to avoid the collision.
Verified live via forkpty: picker → Custom → editor → Start → 7zip-bench
5s sweep ticks (elapsed/remaining advance) → STOPPED at 5s (7z overruns,
run loop stops it) → SweepDone → Done mode → q prints summary. 52/52
tests; clippy clean.

feb1f8ff6355e3506ac77049cb27b9a0f9e532fd
josie <administrator@josie-c.com> · 2026-09-01T22:19 · browse files at this commit

parents: f023dc2

diff --git a/src/ui.rs b/src/ui.rs
index b64c53f..9c01269 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -101,6 +101,24 @@ pub fn run_tui(cfg: Config) {
     let default_cfg = cfg;
 
     loop {
+        // Drive the sweep tick BEFORE drawing + polling, so the run
+        // advances at poll_ms cadence even when no key is pressed.
+        // (The picker + editor modes don't tick — they only react to
+        // keys, so the tick is a no-op for them.)
+        if let Mode::Sweep(app) = &mut mode {
+            let outcome = match app.tick() {
+                Ok(o) => o,
+                Err(e) => {
+                    eprintln!("run error: {e}");
+                    break;
+                }
+            };
+            if matches!(outcome, TickOutcome::SweepDone) {
+                let finished = std::mem::replace(app, App::empty());
+                mode = Mode::Done(finished);
+            }
+        }
+
         terminal.draw(|f| draw(f, &mode)).expect("draw failed");
         let poll_ms = match &mode {
             Mode::Sweep(app) | Mode::Done(app) => app.run.poll_ms(),
@@ -144,25 +162,10 @@ pub fn run_tui(cfg: Config) {
                 }
             }
             Mode::Sweep(app) => {
-                // Drive one tick before polling events so the sweep
-                // advances even while we're waiting for input.
-                let outcome = match app.tick() {
-                    Ok(o) => o,
-                    Err(e) => {
-                        eprintln!("run error: {e}");
-                        break;
-                    }
-                };
-                let done = matches!(outcome, TickOutcome::SweepDone);
-                if done {
-                    // Move the App out of the Sweep mode into Done. We
-                    // can't do this while `app` is borrowed, so use a
-                    // sentinel — replace with empty + push the real one
-                    // into Done below.
-                    let finished = std::mem::replace(app, App::empty());
-                    mode = Mode::Done(finished);
-                    continue;
-                }
+                // The tick already ran at the top of the loop (before
+                // draw + poll) so the sweep advances even without input.
+                // Here we only handle key events.
+                let _ = app; // borrow to keep the match arm exhaustive
                 match key.code {
                     KeyCode::Char('q') | KeyCode::Esc => break,
                     _ => {}
@@ -255,10 +258,10 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
 
     // Not editing — navigate fields + rows.
     match key {
-        KeyCode::Up => {
+        KeyCode::Up | KeyCode::Char('k') => {
             editor_move(state, -1);
         }
-        KeyCode::Down => {
+        KeyCode::Down | KeyCode::Char('j') => {
             editor_move(state, 1);
         }
         KeyCode::Left | KeyCode::Right => {
@@ -318,12 +321,13 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
             state.cursor = state.cfg.workloads.len() - 1;
             state.field = EditorField::Name;
         }
-        // Move the current workload row up/down (reorder).
-        KeyCode::Char('k') if state.cursor < state.cfg.workloads.len() && state.cursor > 0 => {
+        // Move the current workload row up/down (reorder). Uses `>`/`<`
+        // (intuitive for ordering) so j/k stay navigation-only.
+        KeyCode::Char('<') if state.cursor < state.cfg.workloads.len() && state.cursor > 0 => {
             state.cfg.workloads.swap(state.cursor, state.cursor - 1);
             state.cursor -= 1;
         }
-        KeyCode::Char('j') if state.cursor < state.cfg.workloads.len()
+        KeyCode::Char('>') if state.cursor < state.cfg.workloads.len()
             && state.cursor + 1 < state.cfg.workloads.len() =>
         {
             state.cfg.workloads.swap(state.cursor, state.cursor + 1);
@@ -672,7 +676,7 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
     f.render_widget(
         Paragraph::new(Line::from(vec![
             Span::styled(
-                "↑↓ row  ←→ field  Enter edit/toggle  a add  d del  j/k move  Esc back",
+                "j/k row  ←→ field  Enter edit/toggle  a add  d del  </> reorder  Esc back",
                 Style::default().fg(Color::DarkGray),
             ),
         ])),