Commit Graph
35 Commits
Author SHA1 Message Date
dailzandSisyphus 58ac4a65c7 refactor(tui): split app query and viewport cache helpers
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-30 16:55:34 +08:00
dailz e69b7af32a fix(tui): decouple cursor from viewport + correct width calc in no-VHI paths
Three intertwined fixes for the no-VHI scroll path that together eliminate
the 'switching to a new entry scrolls it to viewport top' behavior observed
during Loading state, Loading->Ready transition window, and Tab-toggle
VHI rebuild window.

Cursor/viewport decoupling (the user-visible bug):
* scroll_down_line / scroll_up_line no-VHI branches now advance only the
  cursor (cursor_line, cursor_sub_offset), then call ensure_cursor_visible_no_vhi
  to adjust the viewport minimally if needed. Previously they locked
  v_offset = cursor_line, forcing the cursor's entry to the top of the
  viewport on every keypress.
* New helpers: advance_visual_pos, retreat_visual_pos (bounded visual-row
  walks using compute_visual_height), ensure_cursor_visible_no_vhi (matches
  the VHI-path semantics - top-align when cursor is above viewport,
  bottom-align with minimal scroll when below, no movement when within).
* Same pattern applied to scroll_down/up_half_page and scroll_down/up_page.
* scroll_to_bottom sets cursor_sub_offset to the last visual row of the
  last line in no-VHI state (was 0).
* ensure_viewport_cache hidden anchors removed: the params-change recenter
  and the Loading post-check that did v_offset = cursor_line are replaced
  with the visibility helper.
* 4 transition sites (Tab handler, ensure_visual_height_index, reload,
  poll VHI complete) now clamp cursor_sub_offset against the current
  line height instead of blindly zeroing.

Width calculation correctness (root cause of the residual j/k bug):
* gutter_width() pub(crate) method extracted from ui.rs's inline formula,
  shared between renderer and App to prevent drift.
* get_content_width() now returns effective text-wrap width (area minus
  gutter), matching the renderer's actual_content_width. Previously it
  returned raw area width, causing compute_visual_height to under-count
  wrap rows for lines that wrapped only after gutter subtraction.
* Free function gutter_width_for(total_lines, is_loading) added so
  handle_file_appended and reload_ready_reader can recompute width
  after line-count changes without conflicting borrows on self.
* handle_file_appended: width now captured AFTER update_for_append
  (was before), so digit-boundary crossings (9->10, 99->100) don't
  leave stale gutter widths.

ui.rs render_content now calls app.gutter_width() instead of inlining
the formula (DRY).

Tests: 7 existing tests that locked in the old buggy v_offset = cursor_line
coupling have been rewritten to assert the new correct behavior. New
property tests verify wrap_line_count matches wrap_line_chars across
CJK/emoji/tab/combining-mark/control-char inputs.
2026-06-22 14:52:32 +08:00
dailz 967c11fea9 fix(tui): preserve old state on load_file failure (issue #32)
load_file previously cleared file_watcher before attempting to open the
new file. If open failed, the watcher was lost while loading_state and
file_path still pointed to the old file, breaking change detection.

Refactor to build-then-swap pattern: all fallible work runs on local
variables first, then self is updated only on success. On failure, the
old file's watcher, loading_state and file_path remain intact.

Update test to verify all three fields are preserved on failure.

Closes #32
2026-06-11 17:23:16 +08:00
dailz 10323ce814 fix(tui): add area offset to settings popup positioning (closes #31) 2026-06-11 16:49:37 +08:00
dailz c1a931551b fix(tui): handle settings save errors and rewrite truncate_to_columns (closes #30)
- Report save failures in settings instead of silently discarding errors
- Save draft first, commit color_config only on success (prevents split-brain)
- Show error message in red on status bar, stay in Settings mode on failure
- Clear error on Esc, entering settings, or any settings edit
- Rewrite truncate_to_columns as dedicated O(prefix) truncator
- Tab: stop before tab if expansion would exceed width
- Fixes pre-existing test_truncate_to_columns_tab failure
2026-06-11 16:08:57 +08:00
dailz dfc016c348 fix(tui): wrap color backward cycle at index 0 instead of clamping (closes #29) 2026-06-11 15:05:42 +08:00
dailz e99861c76d fix(tui): add MAX_WRAP_INPUT_LEN guard to prevent UI freeze on oversized lines (closes #26)
- compute_line_entry/compute_visual_height: double guard (raw + post-format)
- Skip detect_level on oversized raw input to avoid O(n) JSON parsing
- Add post-format guard for JSON lines that expand beyond 10MB
- progressive_reader: add post-format guard to compute_line_visual_height
- Add truncate_to_columns helper using existing wrap_line_chars
- Fix misleading docstring on MAX_WRAP_INPUT_LEN constant
- Add 6 regression tests covering all guard paths
2026-06-11 13:15:11 +08:00
dailz a43ef673b0 fix(tui): rebase v_offset before VHI invalidation to prevent viewport jump (closes #25)
Ready state can have VHI=None during async rebuild (Tab toggle, resize,
file append, Loading→Ready transition). Without rebase, v_offset retains
a visual-row value that gets treated as a logical-line offset, causing
viewport jumps and cursor drift.

Changes:
- Add rebase_offset_for_invalidate() to convert v_offset from visual-row
  to logical-line + sub-row before VHI invalidation
- Call it at all 6 invalidation sites in Ready state
- Recalibrate v_offset from viewport top when VHI rebuild completes
- Clamp preserved sub_row to new line height after JSON/width changes

Regression tests: 6 new tests covering rebase conversion, Tab toggle,
VHI rebuild recalibration, sub-row clamping, and Loading→Ready transition.
2026-06-11 09:49:10 +08:00
dailz 70f930eef7 fix(tui): use updated v_offset after params_changed in ensure_viewport_cache (#24)
The loading branch of ensure_viewport_cache captured v_offset before the
params_changed block, which could reassign self.v_offset. This caused the
viewport to use a stale offset when loading + width/format changed together.

Remove the stale local variable and read self.v_offset directly, consistent
with the non-loading branch. Add regression test.
2026-06-11 08:58:10 +08:00
dailz 463c53148b fix(tui): filter KeyEventKind to prevent Release/Repeat from triggering commands (closes #23) 2026-06-10 17:34:17 +08:00
dailz 7852e92ecc fix(watcher): forward notify backend errors instead of silently discarding
Previously Err(_) => return in the notify callback silently dropped all
backend errors (inotify exhaustion, fs unmount, permission loss), leaving
the application unaware that file monitoring had stopped working.

Add FileEvent::WatcherError { message: String } variant to propagate
backend errors through the existing bounded channel. The TUI consumer
receives the event without disrupting the UI for transient errors.

Closes #14
2026-06-09 11:26:54 +08:00
dailz d4679a7543 fix(io): update visual height of last line on append without trailing newline (closes #11)
When a file does not end with a newline, appending content extends the
last logical line's text and thus its visual height. The incremental
extend path in handle_file_appended only computed heights for newly
created logical lines, missing the old last line whose content changed.

Add VisualHeightIndex::replace_last_line_height() — an O(1) method that
rewrites the final prefix sum entry and total. Called before
extend_from_heights so the correct line is targeted.

Changes:
- progressive_reader.rs: add replace_last_line_height, pub with_params,
  7 VHI unit tests
- app.rs: save old_reader_line_count before update, recompute last old
  line height in extend path, 2 integration regression tests
2026-06-07 09:46:24 +08:00
dailz 534a089b58 fix(tui): defer file-change events during Loading state to prevent stale reader (closes #10)
Loading state silently dropped FileEvent::Appended/Truncated via _ => {}.
After Loading→Ready transition the FileReader was based on a stale snapshot.

- Add reload_after_loading flag to defer reload until Ready state
- Extract reload_ready_reader() from handle_file_truncated
- Explicit 3-branch match: Ready handles, Loading sets flag, rest ignores
- Clear flag on IndexerMessage::Error to prevent stale dirty bit
- 4 regression tests covering append/truncate/collapse/error paths
2026-06-04 17:32:58 +08:00
dailz b7938e069d fix(tui): RAII TerminalGuard prevents terminal corruption on error exit (closes #8)
The previous code called std::process::exit(1) on file load failure,
bypassing all terminal restoration (disable_raw_mode, LeaveAlternateScreen,
show_cursor). This left the user's shell in a broken state with no echo
and no visible cursor.

Introduce TerminalGuard with Drop-based cleanup that fires on every exit
path: normal return, ? error propagation, and panic unwind. The guard
also handles partial initialization rollback (e.g. raw mode enabled but
alternate screen fails). Remove all process::exit calls from the guarded
scope.
2026-06-04 16:11:16 +08:00
dailz 1bb6b2e9f3 fix: eliminate TOCTOU race in IndexCache::save (closes #5)
spawn_indexer builds LineIndex from mmap snapshot but IndexCache::save()
re-opened the file to compute the hash. If the file changed between those
two steps, the cached index would be stored under the wrong hash.

- Add IndexCache::save_with_hash() that computes hash from in-memory data
- Add compute_data_hash() public function (same algorithm as compute_file_hash)
- Update spawn_indexer, FileReader::save_cache, and test callers
2026-06-04 13:30:16 +08:00
dailz b6e655bff6 fix(io): handle file shrink in update_for_append to prevent SIGBUS
File truncation/rotation during mmap lifetime caused SIGBUS crash
because update_for_append() ignored new_size < old_size, leaving
a stale mapping that would fault on access.

Introduce AppendStatus enum (Unchanged/Appended/Reloaded) so the
caller can distinguish shrink events from normal appends. On shrink,
reload() rebuilds the mmap and line index. The TUI layer clamps
cursor and invalidates viewport cache on Reloaded, matching the
existing handle_file_truncated() behavior.

Fixes #1
2026-06-03 14:47:23 +08:00
dailz b3256b2917 fix: audit fixes for 4 medium-severity bugs: index_cache tail hash, read_cache doc, mutex poison recovery, Remove event handling 2026-05-10 17:03:07 +08:00
dailz fb23e4c7cb fix(tui): smooth visual-row scrolling during Loading state
During Loading state (before VHI is built), j/k used to jump by logical
line, visually skipping multiple wrapped rows. Now uses v_sub_offset to
track position within a wrapped line, enabling smooth 1-visual-row scroll.

- Add v_sub_offset field to App for sub-line visual position tracking
- scroll_down/up_line else branch: advance v_sub_offset, wrap to next line
- ensure_viewport_cache Loading path: pass v_sub_offset as offset_in_line
- ensure_cursor_visible: skip during Loading (scroll functions manage it)
- Reset v_sub_offset on Loading→Ready, scroll_to_top, scroll_to_bottom
- Add 3 tests for Loading-state sub-offset scrolling behavior
2026-04-24 19:04:30 +08:00
dailz 8c5a838db0 fix(tui): j/k scroll by visual row instead of logical line
When a JSON line wraps to many visual rows (e.g. 73 rows), pressing j
would skip the entire logical line, making wrapped content unreadable.
Now j/k scroll by 1 visual row when a VisualHeightIndex is available,
with cursor tracking the viewport center. Falls back to logical-line
scroll in Loading/no-index modes or when all content fits the viewport.

Adds 4 tests: visual scroll down, visual scroll up, small-file fallback,
j/k roundtrip.
2026-04-24 07:41:30 +08:00
dailz 81cc72bd84 test(tui): add Loading + JSON expansion unit tests 2026-04-14 17:38:35 +08:00
dailz 06b2a39816 fix(tui): post-check cursor visibility in viewport during Loading 2026-04-14 17:09:50 +08:00
dailz 0941092b07 feat(tui): enable JSON expansion during Loading state 2026-04-14 16:52:17 +08:00
dailzandSisyphus 05491304bf fix(tui): allow free scrolling during progressive loading
total_lines() was returning sampled_line_count() (only ~300 lines from the initial 64KB scan) during the Loading state, capping the scroll range. Use estimated_lines instead so the user can scroll to any position while indexing runs in the background. get_line() already supports incremental forward scanning on demand.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:36:03 +08:00
dailzandSisyphus bab3d9078a feat(tui): replace O(N) scanning with progressive loading and VisualHeightIndex
Replace synchronous file loading with AppLoadingState state machine (Empty/Loading/Ready/Error) for instant interactivity. Add ViewportCache for on-demand viewport computation, replacing global wrap/level caches. Integrate background indexer polling and file watcher events into the TUI event loop. Add loading UI with progress percentage, estimated line numbers with ~ prefix, and error state display. Eliminate all O(N) linear scans using VisualHeightIndex binary search.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:07:18 +08:00
dailzandSisyphus 2ac3eb99c7 feat(tui): add Span-based rendering with level colors and settings panel
Replace Line::styled with Span-based rendering: gutter (DarkGray fg) + content (level-based fg color). Add centered settings popup (S key) with j/k navigation, arrow key color cycling, number key shortcuts, Enter to save, Esc to cancel.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:52:45 +08:00
dailzandSisyphus c6e83cb8c2 feat(tui): add AppMode, level_cache and config integration
Add AppMode enum (Normal/Settings), level_cache field populated in recompute_wrap_cache via detect_level(), color_config loaded from TOML in main.rs. Refactor handle_key for mode dispatch with S key to enter settings.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:52:03 +08:00
dailzandSisyphus ef4c2e7383 feat(tui): add color mapping module
Add color.rs with parse_color (string to ratatui::Color), level_fg (LogLevel to color via ColorConfig), and AVAILABLE_COLORS constant for the settings panel.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:51:26 +08:00
dailzandSisyphus 1c9367f1f9 feat(tui): update status bar with Tab format hint
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 11:38:42 +08:00
dailzandSisyphus 280c3f5014 feat(tui): add Tab key to toggle JSON pretty-print
Press Tab to switch JSON log lines between raw single-line and indented
multi-line format (2-space indent, like jq output). Only JSON Object lines
are affected; plain text lines stay unchanged.

- Add json_format bool field to App, toggled by Tab
- Add format_json_line() standalone function with serde_json::to_string_pretty
- Integrate into recompute_wrap_cache with \n-split sub-line wrapping
- Center viewport on cursor after cache rebuild to prevent jump on toggle
- Add 12 unit tests covering format, toggle, and edge cases

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 11:38:30 +08:00
dailz 8a664c02c8 fix(tui): use char-level wrapping instead of word-level for log lines 2026-04-11 09:02:01 +08:00
dailz 3c042e20a1 fix(tui): show [0/0] instead of [1/0] for empty files 2026-04-11 08:52:22 +08:00
dailz 6820246997 feat(tui): rewrite UI with line numbers, soft wrap, and scrolling 2026-04-10 23:38:06 +08:00
dailz 555ffc0836 feat(tui): expand App state with file loading and scroll 2026-04-10 23:28:23 +08:00
dailzandSisyphus 73322138c1 feat(tui): ratatui skeleton with layout
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-10 21:20:14 +08:00
dailzandSisyphus 0ee82cbccd feat(init): scaffold cargo workspace and CI
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-10 21:17:22 +08:00