Skip to content

Non-blocking dialogs and interactive programs#28

Merged
kromych merged 10 commits into
mainfrom
tui-sync-threading
Jul 2, 2026
Merged

Non-blocking dialogs and interactive programs#28
kromych merged 10 commits into
mainfrom
tui-sync-threading

Conversation

@kromych

@kromych kromych commented Jul 2, 2026

Copy link
Copy Markdown
Owner

No description provided.

kromych and others added 10 commits June 30, 2026 16:19
Port the upstream fixes that ruf4 actually exercises:
- xterm mouse decoding + macOS touchpad horizontal scroll (#819, #794):
  new parse_xterm_mouse, Point::as_array, #[repr(C)] on Point/Size/Rect,
  InputMouse.drag field.
- clippy `as *mut _` -> `.cast()` (#730) across tui, stdext.
- stdext: loongarch64 feature gate; memset .cast() refactor.
- lsh: language-id `_`->`-`; doc wording.

Skip fixes that target tui machinery ruf4 bypasses (no-op buffer.paste
stub, own scroll handling): #818 multi-line paste, #812 modal scroll,
#810 redirected-stdin, #835 localization. Preserve the two local TUI
patches (InputKey visibility, Ctrl+Space remap; pub mod tables).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Long operations no longer block the UI. A single Job runs on a worker
thread (crates/ruf4/src/job.rs), communicating with the UI thread via
channels plus an atomic cancel flag; it never touches State, the
terminal, or the arena. The pure fileops primitives are reused.

- Dialog::Progress shows a gauge, N/M files, bytes, and Esc=Cancel.
- Copy/move pre-scan the source tree for accurate totals and copy
  per-file so a large copy stays cancellable.
- The per-file overwrite prompt is preserved via a worker<->UI
  handshake (NeedOverwrite -> Decision), reusing the overwrite dialog.
- Commands spawn with piped stdout/stderr drained on reader threads to
  avoid pipe-buffer deadlock; cancel kills the child.
- main.rs polls the job and uses a 50ms read timeout while one runs;
  no tui/sys changes needed.

Tests: worker-level (delete/copy/move/overwrite skip+replace/command)
and State-level (delete flow spawns a job, completes, refreshes panel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scan_dir(path, show_hidden) -> Vec<FileEntry> performs only filesystem
and platform calls, so it can run on a worker thread. refresh() now
calls it then sorts and clamps the cursor. Behaviour is unchanged; this
is the reusable enumeration primitive for off-thread directory scans.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five hand-synced match tables collapse to single sources of truth:
- KEY_NAMES drives both key_display_name and parse_key_name.
- action_meta is one exhaustive match for an action's settings string
  and its label; action_str/action_label read from it.
- parse_action is derived from action_str over ALL_ACTIONS, so the
  string<->action directions cannot drift.

Removes the "added a variant, forgot to update table N" bug class.
Round-trip tests (tests/test_action.rs) lock every action and key name,
and check that ALL_ACTIONS covers the default bindings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy/move/overwrite now run entirely through the background job, so the
synchronous resume machinery is unreachable. Drop continue_copy_move and
execute_file_op, collapse handle_overwrite_dialog to the job handshake,
and trim the now-vestigial pending/errors fields from
Dialog::ConfirmOverwrite.

Panel::navigate_to(path) replaces the repeated
"path = ...; cursor = 0; scroll_offset = 0; refresh()" sequence at the
three navigation sites (choose_root, dir history, cd).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both the command line and the text-input dialogs duplicated the same
char-index cursor arithmetic (insert / backspace / delete / left / right
/ home / end). Extract a TextField { text, cursor } helper that owns that
logic; both handlers now select the target field and delegate. Behaviour
is unchanged (command-line backspace still falls through at the start of
the line). New tests cover mid-string insert/delete and cursor motion for
both the command line and a dialog field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preview already rendered a scrollbar and clamped preview_scroll, but
nothing drove it. Route a wheel event over the quick view (the inactive
side while quick view is on) to preview_scroll; the file list keeps the
wheel on its own side. Reset preview_scroll to 0 when the previewed file
changes so the offset does not leak between files.

Tests cover wheel-over-preview scrolling, wheel-over-file-list leaving the
preview untouched, and the reset on file change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backgrounding commands with piped stdout/stderr left the TUI holding the
terminal in raw mode, so a REPL or full-screen program (python, vim, less,
ssh) could never receive keystrokes. Capturing output and interactivity are
mutually exclusive without a pty, so switch to the foreground model used by
mc/FAR: suspend the TUI, run the command with the real terminal inherited,
then resume.

- sys (unix + windows): add suspend()/resume() to toggle cooked/raw
  terminal modes; factor the unix raw-termios computation out of
  switch_modes so resume reuses it.
- platform::run_interactive: leave the alternate screen, suspend, run
  `sh -c`/`cmd /C` with inherited stdio in cwd, wait for Enter so output
  stays readable, resume, re-enter the alternate screen, and inject a
  resize to force a full repaint. execute_command now calls it.
- Remove the now-obsolete captured-output machinery: JobKind::Command and
  its worker, Dialog::ShellOutput, last_output, and the F12/LastOutput
  action. Output scrolls the real terminal instead.

Verified via a pty harness: a command reading stdin receives interactive
input (`read line; echo GOT-$line` echoes the typed text) and the return
prompt appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running a command leaves and re-enters the alternate screen. The
framebuffer's incremental diff then compared the new frame against the
identical pre-command frame and emitted nothing, so the panels stayed
blank until the next content change (e.g. F5) forced a redraw.

flip() only forced a full redraw on a size change. Add
Framebuffer::request_full_redraw()/Tui::request_full_redraw() to force the
next frame to re-emit unconditionally. execute_command sets a repaint
request after run_interactive; the main loop consumes it before render.

Tested: framebuffer unit test (unchanged frame is quiet, is re-emitted
after request_full_redraw, flag is one-shot) and a pty check that the
panels repaint after a command with no further keypress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kromych kromych merged commit 9c0dbc1 into main Jul 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant