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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user