use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) enum PreviewMode {
WorkingTree = 1,
Log = 2,
BranchDiff = 3,
UpstreamDiff = 4,
Summary = 5,
}
impl PreviewMode {
pub(super) fn from_u8(n: u8) -> Self {
match n {
2 => Self::Log,
3 => Self::BranchDiff,
4 => Self::UpstreamDiff,
5 => Self::Summary,
_ => Self::WorkingTree,
}
}
}
const CHAR_ASPECT_RATIO: f64 = 0.5;
pub(super) const SKIM_HEIGHT_PERCENT: usize = 90;
pub(super) const MAX_VISIBLE_ITEMS: usize = 12;
pub(super) const LIST_CHROME_LINES: usize = 4;
pub(super) const MIN_PREVIEW_LINES: usize = 5;
const PREVIEW_WIDTH_PERCENT: usize = 50;
const MIN_COLS_FOR_RIGHT_LAYOUT: f64 = 80.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum PreviewLayout {
#[default]
Right,
Down,
}
impl PreviewLayout {
pub(super) fn auto_detect() -> Self {
let (cols, rows) = terminal_size::terminal_size()
.map(|(terminal_size::Width(w), terminal_size::Height(h))| (w as f64, h as f64))
.unwrap_or((80.0, 24.0));
Self::for_dimensions(cols, rows)
}
fn for_dimensions(cols: f64, rows: f64) -> Self {
if cols < MIN_COLS_FOR_RIGHT_LAYOUT {
return Self::Down;
}
let effective_ratio = (cols / rows) * CHAR_ASPECT_RATIO;
if effective_ratio < 1.0 {
Self::Down
} else {
Self::Right
}
}
pub(super) fn to_preview_window_spec(self, num_items: usize) -> String {
let (width, height) = self.preview_dimensions(num_items);
match self {
Self::Right => format!("right:{}", width),
Self::Down => format!("down:{}", height),
}
}
pub(super) fn preview_dimensions(self, num_items: usize) -> (usize, usize) {
let (term_width, term_height) = terminal_size::terminal_size()
.map(|(terminal_size::Width(w), terminal_size::Height(h))| (w as usize, h as usize))
.unwrap_or((80, 24));
match self {
Self::Right => {
let width = term_width * PREVIEW_WIDTH_PERCENT / 100;
let height = term_height * SKIM_HEIGHT_PERCENT / 100;
(width, height)
}
Self::Down => {
let width = term_width;
let available = term_height * SKIM_HEIGHT_PERCENT / 100;
let list_lines = LIST_CHROME_LINES + num_items.min(MAX_VISIBLE_ITEMS);
let remaining = available.saturating_sub(list_lines);
let height = remaining.max(MIN_PREVIEW_LINES).min(available);
(width, height)
}
}
}
}
pub(super) struct PreviewStateData;
impl PreviewStateData {
pub(super) fn state_path() -> PathBuf {
std::env::temp_dir().join(format!("wt-picker-state-{}", std::process::id()))
}
pub(super) fn read_mode() -> PreviewMode {
let state_path = Self::state_path();
fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree)
}
pub(super) fn write_mode(mode: PreviewMode) {
let state_path = Self::state_path();
let _ = fs::write(&state_path, format!("{}", mode as u8));
}
}
pub(super) struct PreviewState {
pub(super) path: PathBuf,
pub(super) initial_layout: PreviewLayout,
}
impl PreviewState {
pub(super) fn new() -> Self {
let path = PreviewStateData::state_path();
PreviewStateData::write_mode(PreviewMode::WorkingTree);
Self {
path,
initial_layout: PreviewLayout::auto_detect(),
}
}
}
impl Drop for PreviewState {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
let _ = fs::remove_file(self.path.with_extension("remove"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preview_mode_from_u8() {
assert_eq!(PreviewMode::from_u8(1), PreviewMode::WorkingTree);
assert_eq!(PreviewMode::from_u8(2), PreviewMode::Log);
assert_eq!(PreviewMode::from_u8(3), PreviewMode::BranchDiff);
assert_eq!(PreviewMode::from_u8(4), PreviewMode::UpstreamDiff);
assert_eq!(PreviewMode::from_u8(5), PreviewMode::Summary);
assert_eq!(PreviewMode::from_u8(0), PreviewMode::WorkingTree);
assert_eq!(PreviewMode::from_u8(99), PreviewMode::WorkingTree);
}
#[test]
fn test_preview_layout_to_preview_window_spec() {
let spec = PreviewLayout::Right.to_preview_window_spec(10);
assert!(spec.starts_with("right:"));
let spec = PreviewLayout::Down.to_preview_window_spec(5);
assert!(spec.starts_with("down:"));
}
#[test]
fn test_preview_state_data_read_default() {
let state_path = std::env::temp_dir().join("wt-test-read-default");
let _ = fs::remove_file(&state_path);
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::WorkingTree);
}
#[test]
fn test_preview_state_data_roundtrip() {
let state_path = std::env::temp_dir().join("wt-test-roundtrip");
let _ = fs::write(&state_path, "1");
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::WorkingTree);
let _ = fs::write(&state_path, "2");
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::Log);
let _ = fs::write(&state_path, "3");
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::BranchDiff);
let _ = fs::write(&state_path, "4");
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::UpstreamDiff);
let _ = fs::write(&state_path, "5");
let mode = fs::read_to_string(&state_path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(PreviewMode::from_u8)
.unwrap_or(PreviewMode::WorkingTree);
assert_eq!(mode, PreviewMode::Summary);
let _ = fs::remove_file(&state_path);
}
#[test]
fn test_layout_for_dimensions_wide_terminal() {
assert_eq!(
PreviewLayout::for_dimensions(120.0, 40.0),
PreviewLayout::Right
);
}
#[test]
fn test_layout_for_dimensions_portrait_terminal() {
assert_eq!(
PreviewLayout::for_dimensions(180.0, 136.0),
PreviewLayout::Down
);
}
#[test]
fn test_layout_for_dimensions_narrow_terminal_forces_down() {
assert_eq!(
PreviewLayout::for_dimensions(60.0, 24.0),
PreviewLayout::Down
);
assert_eq!(
PreviewLayout::for_dimensions(40.0, 20.0),
PreviewLayout::Down
);
}
#[test]
fn test_layout_for_dimensions_boundary() {
assert_eq!(
PreviewLayout::for_dimensions(80.0, 24.0),
PreviewLayout::Right
);
assert_eq!(
PreviewLayout::for_dimensions(79.0, 24.0),
PreviewLayout::Down
);
}
}