use std::collections::HashMap;
use std::process::Command;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
pub fn triptych_detail_area(total: Rect, has_pending_approvals: bool) -> Option<Rect> {
if total.width == 0 || total.height == 0 {
return None;
}
let body = if has_pending_approvals {
let v = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(total);
v[1]
} else {
total
};
let outer = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(28), Constraint::Min(0), ])
.split(body);
let right_stack = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(3, 5), Constraint::Ratio(2, 5)])
.split(outer[1]);
let detail = right_stack[0];
if detail.width == 0 || detail.height == 0 {
return None;
}
Some(detail)
}
pub fn should_sync(
cache: &HashMap<String, (u16, u16)>,
session: &str,
current: (u16, u16),
) -> bool {
cache.get(session) != Some(¤t)
}
pub trait PaneResizer: Send + Sync {
fn resize(&self, session: &str, cols: u16, rows: u16);
}
#[derive(Debug, Default, Clone, Copy)]
pub struct TmuxPaneResizer;
impl PaneResizer for TmuxPaneResizer {
fn resize(&self, session: &str, cols: u16, rows: u16) {
let _ = Command::new("tmux")
.args([
"resize-pane",
"-t",
session,
"-x",
&cols.to_string(),
"-y",
&rows.to_string(),
])
.status();
}
}
pub mod test_support {
use std::sync::Mutex;
use super::PaneResizer;
#[derive(Debug, Default)]
pub struct MockPaneResizer {
pub calls: Mutex<Vec<(String, u16, u16)>>,
}
impl PaneResizer for MockPaneResizer {
fn resize(&self, session: &str, cols: u16, rows: u16) {
self.calls
.lock()
.unwrap()
.push((session.to_string(), cols, rows));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detail_area_for_typical_terminal_without_approvals() {
let total = Rect::new(0, 0, 120, 40);
let detail = triptych_detail_area(total, false).unwrap();
assert_eq!(detail.x, 28);
assert_eq!(detail.y, 0);
assert_eq!(detail.width, 92);
assert_eq!(detail.height, 24);
}
#[test]
fn detail_area_with_approvals_stripe_loses_one_row() {
let total = Rect::new(0, 0, 120, 40);
let detail = triptych_detail_area(total, true).unwrap();
assert_eq!(detail.x, 28);
assert_eq!(detail.y, 1);
assert_eq!(detail.width, 92);
assert_eq!(detail.height, 23);
}
#[test]
fn detail_area_returns_none_on_zero_dimension() {
assert!(triptych_detail_area(Rect::new(0, 0, 0, 40), false).is_none());
assert!(triptych_detail_area(Rect::new(0, 0, 120, 0), false).is_none());
}
#[test]
fn detail_area_returns_none_when_sidebar_consumes_everything() {
let total = Rect::new(0, 0, 28, 40);
assert!(triptych_detail_area(total, false).is_none());
}
#[test]
fn should_sync_returns_true_on_first_call() {
let cache = HashMap::new();
assert!(should_sync(&cache, "t-hello-mgr", (92, 24)));
}
#[test]
fn should_sync_returns_false_when_size_unchanged() {
let mut cache = HashMap::new();
cache.insert("t-hello-mgr".to_string(), (92, 24));
assert!(!should_sync(&cache, "t-hello-mgr", (92, 24)));
}
#[test]
fn should_sync_returns_true_when_size_differs() {
let mut cache = HashMap::new();
cache.insert("t-hello-mgr".to_string(), (92, 24));
assert!(should_sync(&cache, "t-hello-mgr", (100, 24)));
assert!(should_sync(&cache, "t-hello-mgr", (92, 25)));
}
#[test]
fn should_sync_treats_different_sessions_independently() {
let mut cache = HashMap::new();
cache.insert("t-hello-mgr".to_string(), (92, 24));
assert!(should_sync(&cache, "t-hello-dev", (92, 24)));
}
use super::test_support::MockPaneResizer;
#[test]
fn mock_resizer_records_calls() {
let m = MockPaneResizer::default();
m.resize("t-a", 100, 30);
m.resize("t-b", 80, 20);
let calls = m.calls.lock().unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0], ("t-a".to_string(), 100, 30));
assert_eq!(calls[1], ("t-b".to_string(), 80, 20));
}
}