use crossterm::terminal as crossterm_terminal;
use log::debug;
use std::path::{Path, PathBuf};
use crate::input_source::InputSource;
use crate::watch::FileWatcher;
use super::layout::{self, Layout};
use super::terminal;
pub(super) struct JumpEntry {
pub path: PathBuf,
pub y_offset: u32,
}
fn scale_scroll(scroll: u32, old_scale: f64, new_scale: f64) -> u32 {
if old_scale <= 0.0 || !new_scale.is_finite() {
return scroll;
}
let scaled = (scroll as f64 * (new_scale / old_scale)).round();
scaled.clamp(0.0, u32::MAX as f64) as u32
}
pub(super) struct Session {
pub layout: Layout,
pub input: InputSource,
pub filename: String,
pub watcher: Option<FileWatcher>,
pub jump_stack: Vec<JumpEntry>,
pub scroll_carry: u32,
pub pending_flash: Option<String>,
pub watch: bool,
pub log_buffer: crate::log::LogBuffer,
}
impl Session {
pub(super) fn current_file_path(&self) -> Option<&Path> {
match &self.input {
InputSource::File(p) => Some(p),
InputSource::Stdin(_) => None,
}
}
pub(super) fn update_layout_for_resize(
&mut self,
new_cols: u16,
new_rows: u16,
sidebar_cols: u16,
) -> anyhow::Result<()> {
let new_winsize = crossterm_terminal::window_size()?;
self.layout = layout::compute_layout(
new_cols,
new_rows,
new_winsize.width,
new_winsize.height,
sidebar_cols,
);
terminal::delete_all_images()?;
Ok(())
}
pub(super) fn handle_exit(
&mut self,
exit: super::effect::ExitReason,
scroll_position: u32,
sidebar_cols: u16,
) -> anyhow::Result<bool> {
use super::effect::ExitReason;
match exit {
ExitReason::Quit => return Ok(true),
ExitReason::Resize { new_cols, new_rows } => {
self.scroll_carry = scroll_position;
debug!("resize: rebuilding tiled document and sidebar");
self.update_layout_for_resize(new_cols, new_rows, sidebar_cols)?;
}
ExitReason::Reload => {
self.scroll_carry = scroll_position;
debug!("file changed: reloading document (double-buffer swap)");
}
ExitReason::SetScale { old, new, flash } => {
self.scroll_carry = scale_scroll(scroll_position, old, new);
if flash.is_some() {
self.pending_flash = flash;
}
debug!(
"scale changed {old} → {new}: scroll {scroll_position} → {} (rebuilding, double-buffer swap)",
self.scroll_carry
);
}
ExitReason::Navigate { path } => {
if !path.exists() {
self.pending_flash = Some(format!("File not found: {}", path.display()));
terminal::delete_all_images()?;
return Ok(false);
}
if let InputSource::File(cur) = &self.input {
self.jump_stack.push(JumpEntry {
path: cur.clone(),
y_offset: scroll_position,
});
}
let canonical = std::fs::canonicalize(&path).unwrap_or(path);
debug!("navigate: jumping to {}", canonical.display());
self.input = InputSource::File(canonical.clone());
self.filename = self.input.display_name().to_string();
self.scroll_carry = 0;
if self.watch {
self.watcher = Some(FileWatcher::new(&canonical)?);
}
terminal::delete_all_images()?;
}
ExitReason::GoBack => {
let entry = self.jump_stack.pop().expect("GoBack with empty stack");
debug!("go back: returning to {}", entry.path.display());
self.input = InputSource::File(entry.path.clone());
self.filename = self.input.display_name().to_string();
self.scroll_carry = entry.y_offset;
if self.watch {
self.watcher = Some(FileWatcher::new(&entry.path)?);
}
terminal::delete_all_images()?;
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::scale_scroll;
#[test]
fn scale_scroll_zoom_in_doubles_offset() {
assert_eq!(scale_scroll(1000, 1.0, 2.0), 2000);
}
#[test]
fn scale_scroll_zoom_out_halves_offset() {
assert_eq!(scale_scroll(1000, 2.0, 1.0), 500);
}
#[test]
fn scale_scroll_no_change_is_identity() {
assert_eq!(scale_scroll(1234, 1.25, 1.25), 1234);
}
#[test]
fn scale_scroll_zero_is_zero() {
assert_eq!(scale_scroll(0, 1.0, 2.0), 0);
}
#[test]
fn scale_scroll_rounds_to_nearest() {
assert_eq!(scale_scroll(100, 1.0, 0.85), 85);
assert_eq!(scale_scroll(100, 0.85, 1.5), 176);
}
#[test]
fn scale_scroll_handles_invalid_old_scale() {
assert_eq!(scale_scroll(500, 0.0, 2.0), 500);
assert_eq!(scale_scroll(500, -1.0, 2.0), 500);
}
#[test]
fn scale_scroll_clamps_overflow() {
assert_eq!(scale_scroll(u32::MAX, 1.0, 2.0), u32::MAX);
}
}