use super::layout::{SplitDirection, SplitNode};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PaneId(pub u32);
impl PaneId {
pub const ROOT: Self = Self(0);
}
#[derive(Debug, Clone)]
pub struct Pane {
pub id: PaneId,
pub session_id: Option<Uuid>,
pub scroll_offset: usize,
pub auto_scroll: bool,
}
impl Pane {
pub fn new(id: PaneId, session_id: Option<Uuid>) -> Self {
Self {
id,
session_id,
scroll_offset: 0,
auto_scroll: true,
}
}
}
#[derive(Serialize, Deserialize)]
struct LayoutSnapshot {
root: Option<SplitNode>,
panes: Vec<PaneSnapshot>,
focused: PaneId,
next_id: u32,
}
#[derive(Serialize, Deserialize)]
struct PaneSnapshot {
id: PaneId,
session_id: Option<Uuid>,
}
#[derive(Debug, Clone)]
pub struct PaneManager {
pub root: Option<SplitNode>,
pub panes: Vec<Pane>,
pub focused: PaneId,
next_id: u32,
}
impl Default for PaneManager {
fn default() -> Self {
Self::new()
}
}
impl PaneManager {
pub fn new() -> Self {
let root_pane = Pane::new(PaneId::ROOT, None);
Self {
root: None,
panes: vec![root_pane],
focused: PaneId::ROOT,
next_id: 1,
}
}
fn alloc_id(&mut self) -> PaneId {
let id = PaneId(self.next_id);
self.next_id += 1;
id
}
pub fn focused_pane(&self) -> Option<&Pane> {
self.panes.iter().find(|p| p.id == self.focused)
}
pub fn focused_pane_mut(&mut self) -> Option<&mut Pane> {
let focused = self.focused;
self.panes.iter_mut().find(|p| p.id == focused)
}
pub fn get(&self, id: PaneId) -> Option<&Pane> {
self.panes.iter().find(|p| p.id == id)
}
pub fn get_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
self.panes.iter_mut().find(|p| p.id == id)
}
pub fn is_split(&self) -> bool {
self.panes.len() > 1
}
pub fn pane_count(&self) -> usize {
self.panes.len()
}
pub fn split(&mut self, direction: SplitDirection) -> PaneId {
let new_id = self.alloc_id();
let new_pane = Pane::new(new_id, None);
self.panes.push(new_pane);
let current = self.focused;
match self.root.take() {
None => {
self.root = Some(SplitNode::Split {
direction,
ratio: 0.5,
first: Box::new(SplitNode::Leaf(current)),
second: Box::new(SplitNode::Leaf(new_id)),
});
}
Some(tree) => {
self.root = Some(tree.replace_leaf(current, direction, new_id));
}
}
self.focused = new_id;
new_id
}
pub fn close_focused(&mut self) -> bool {
if self.panes.len() <= 1 {
return false;
}
let closing = self.focused;
self.panes.retain(|p| p.id != closing);
if let Some(tree) = self.root.take() {
let simplified = tree.remove_leaf(closing);
match simplified {
SplitNode::Leaf(id) => {
self.root = None;
self.focused = id;
}
other => {
let new_focus = other.first_leaf();
self.root = Some(other);
self.focused = new_focus;
}
}
} else {
self.focused = self.panes.first().map(|p| p.id).unwrap_or(PaneId::ROOT);
}
true
}
pub fn focus_next(&mut self) {
if self.panes.len() <= 1 {
return;
}
let idx = self
.panes
.iter()
.position(|p| p.id == self.focused)
.unwrap_or(0);
let next = (idx + 1) % self.panes.len();
self.focused = self.panes[next].id;
}
pub fn focus_prev(&mut self) {
if self.panes.len() <= 1 {
return;
}
let idx = self
.panes
.iter()
.position(|p| p.id == self.focused)
.unwrap_or(0);
let prev = if idx == 0 {
self.panes.len() - 1
} else {
idx - 1
};
self.focused = self.panes[prev].id;
}
pub fn pane_ids_in_order(&self) -> Vec<PaneId> {
match &self.root {
None => vec![self.panes.first().map(|p| p.id).unwrap_or(PaneId::ROOT)],
Some(tree) => tree.leaves(),
}
}
fn layout_path() -> std::path::PathBuf {
crate::config::opencrabs_home().join("layout.json")
}
pub fn save_layout(&self) {
if !self.is_split() {
if let Err(e) = std::fs::remove_file(Self::layout_path())
&& e.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!("Failed to remove layout.json: {}", e);
}
return;
}
let snapshot = LayoutSnapshot {
root: self.root.clone(),
panes: self
.panes
.iter()
.map(|p| PaneSnapshot {
id: p.id,
session_id: p.session_id,
})
.collect(),
focused: self.focused,
next_id: self.next_id,
};
if let Ok(json) = serde_json::to_string_pretty(&snapshot)
&& let Err(e) = std::fs::write(Self::layout_path(), json)
{
tracing::warn!("Failed to save layout.json: {}", e);
}
}
pub fn load_layout() -> Self {
let path = Self::layout_path();
let Ok(data) = std::fs::read_to_string(&path) else {
return Self::new();
};
let Ok(snapshot) = serde_json::from_str::<LayoutSnapshot>(&data) else {
tracing::warn!("Corrupt layout.json — starting with single pane");
if let Err(e) = std::fs::remove_file(&path) {
tracing::warn!("Failed to remove corrupt layout.json: {}", e);
}
return Self::new();
};
let panes: Vec<Pane> = snapshot
.panes
.into_iter()
.map(|s| Pane::new(s.id, s.session_id))
.collect();
if panes.is_empty() {
return Self::new();
}
Self {
root: snapshot.root,
panes,
focused: snapshot.focused,
next_id: snapshot.next_id,
}
}
}