refactor(tui): split remaining app.rs into per-concern submodules

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-09 13:53:36 +08:00
co-authored by Sisyphus
parent 58ac4a65c7
commit 9854ef13b6
7 changed files with 1161 additions and 1125 deletions
+169
View File
@@ -0,0 +1,169 @@
use std::path::Path;
use log_viewer_core::io::progressive_reader::{
IndexerMessage, ProgressiveFileReader, spawn_indexer,
};
use log_viewer_core::watcher::file_watcher::FileWatcher;
use super::{App, AppLoadingState};
impl App {
pub fn load_file(&mut self, path: &str) -> anyhow::Result<()> {
// ── Phase 1: Pure computation, no mutation of self ──────────
// If any step fails and returns ?, self remains completely untouched,
// preserving the old file's watcher, loading_state, and file_path.
let mut pfr =
ProgressiveFileReader::open(Path::new(path)).map_err(|e| anyhow::anyhow!("{e}"))?;
let new_loading_state = if pfr.is_sampling() {
// Cache miss: spawn background indexer
let (cancel_tx, cancel_rx) = crossbeam_channel::bounded(1);
let generation = pfr.generation();
let indexer_rx =
spawn_indexer(pfr.path().to_path_buf(), generation, 80, false, cancel_rx);
pfr = ProgressiveFileReader::with_channels(
Path::new(path),
cancel_tx,
indexer_rx,
generation,
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let estimated = pfr.line_count() as u64;
AppLoadingState::Loading {
reader: pfr,
estimated_lines: estimated,
progress_percent: 0.0,
}
} else {
// Cache hit: Ready state
AppLoadingState::Ready { reader: pfr }
};
let new_watcher = FileWatcher::watch(Path::new(path)).ok();
// ── Phase 2: Commit — swap self to new state ───────────────
// SAFETY: Do NOT add any fallible operations (with ?) below this point.
// The old file_watcher and loading_state are dropped here, cancelling
// any background indexer for the previous file.
self.file_watcher = new_watcher;
self.loading_state = new_loading_state;
self.file_path = Some(path.to_string());
// Reset UI state for the new file
self.cursor_line = 0;
self.v_offset = 0;
self.v_sub_offset = 0;
self.cursor_sub_offset = 0;
self.viewport_cache.invalidate();
self.last_g_press = None;
self.json_format = false;
self.mode = super::AppMode::Normal;
self.reload_after_loading = false;
Ok(())
}
/// Poll the background indexer for progress/completion.
/// Transitions Loading → Ready when indexing completes.
/// Must be called every frame in the event loop.
pub fn poll_background_indexer(&mut self) {
// Poll visual height rebuild (Ready state only).
// Recalibrate v_offset only when VHI actually changed.
if let AppLoadingState::Ready { reader } = &mut self.loading_state
&& let Some(index) = reader.poll_visual_height_rebuild()
{
if let log_viewer_core::io::progressive_reader::ReaderState::Ready {
visual_height_index,
..
} = &mut reader.state
{
*visual_height_index = Some(index);
}
let logical_top = self.v_offset.min(self.total_lines().saturating_sub(1));
let sub = self.v_sub_offset;
let first_visual = self.cursor_to_first_visual_row(logical_top);
let line_height = self
.get_visual_height_index()
.map_or(1, |idx| idx.visual_height_of_line(logical_top));
self.v_offset = first_visual.saturating_add(sub.min(line_height.saturating_sub(1)));
self.v_sub_offset = 0;
self.clamp_cursor_sub_offset_no_vhi();
self.clamp_v_offset();
self.viewport_cache.invalidate();
}
// Poll main indexer (Loading state)
let old_state = std::mem::replace(&mut self.loading_state, AppLoadingState::Empty);
if let AppLoadingState::Loading {
mut reader,
estimated_lines,
mut progress_percent,
} = old_state
{
if let Some(msg) = reader.poll_indexer() {
match msg {
IndexerMessage::Progress { percent, .. } => {
progress_percent = percent;
self.loading_state = AppLoadingState::Loading {
reader,
estimated_lines,
progress_percent,
};
}
IndexerMessage::Complete {
reader: fr,
visual_height_index,
..
} => {
let saved_cursor = self.cursor_line;
reader.set_ready(fr, visual_height_index);
self.loading_state = AppLoadingState::Ready { reader };
self.viewport_cache.invalidate();
// Clamp cursor if exact count < estimated
self.cursor_line = saved_cursor.min(self.total_lines().saturating_sub(1));
// Loading uses 1:1 logical-line offsets; Ready uses visual-row
// offsets derived from the prefix-sum index. Recompute v_offset
// so the same logical line stays visible (falls back to 1:1 when
// the index is absent, which is the case right after invalidate).
self.v_offset = self.cursor_to_first_visual_row(self.cursor_line);
self.clamp_v_offset();
self.v_sub_offset = 0;
self.cursor_sub_offset = 0;
// Gutter width changes (~N → N) shift content_width, so any
// VisualHeightIndex built with the old width is stale.
let (new_offset, new_sub) = self.rebase_offset_for_invalidate();
if let AppLoadingState::Ready { reader } = &mut self.loading_state {
reader.invalidate_visual_height_index();
}
self.v_offset = new_offset;
self.v_sub_offset = new_sub;
self.cursor_sub_offset = 0;
if self.reload_after_loading {
self.reload_after_loading = false;
self.reload_ready_reader();
}
}
IndexerMessage::Error { message, .. } => {
self.loading_state = AppLoadingState::Error(message);
self.reload_after_loading = false;
}
}
} else {
self.loading_state = AppLoadingState::Loading {
reader,
estimated_lines,
progress_percent,
};
}
} else {
self.loading_state = old_state;
}
}
}