mod creation;
mod focus;
mod layout;
mod session;
mod tmux_convert;
mod tmux_layout;
mod tmux_update;
use crate::config::{Config, PaneBackgroundConfig};
use crate::pane::types::{Pane, PaneBounds, PaneId, PaneNode};
use anyhow::Result;
use par_term_terminal::TerminalManager;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
pub enum ExtractResult {
Extracted {
pane: Pane,
remaining: Option<PaneNode>,
},
OnlyPane(Pane),
NotFound,
}
pub type PaneIdRemap = HashMap<PaneId, PaneId>;
enum ExtractInternal {
Extracted { pane: Pane, remaining: PaneNode },
OnlyPane(Pane),
NotFound(PaneNode),
}
pub struct PaneManager {
pub(super) root: Option<PaneNode>,
pub(super) focused_pane_id: Option<PaneId>,
pub(super) next_pane_id: PaneId,
pub(super) divider_width: f32,
pub(super) divider_hit_width: f32,
pub(super) total_bounds: PaneBounds,
}
impl PaneManager {
pub fn new() -> Self {
Self {
root: None,
focused_pane_id: None,
next_pane_id: 1,
divider_width: 1.0, divider_hit_width: 8.0, total_bounds: PaneBounds::default(),
}
}
pub fn new_with_existing_terminal(
terminal: Arc<RwLock<TerminalManager>>,
working_directory: Option<String>,
is_active: Arc<AtomicBool>,
) -> Self {
let mut manager = Self::new();
let primary_pane_id = manager.next_pane_id;
manager.next_pane_id += 1;
let pane =
Pane::new_wrapping_terminal(primary_pane_id, terminal, working_directory, is_active);
manager.root = Some(PaneNode::leaf(pane));
manager.focused_pane_id = Some(primary_pane_id);
manager
}
pub fn new_with_pane(pane: Pane) -> Self {
let pane_id = pane.id;
let mut manager = Self::new();
manager.next_pane_id = pane_id + 1;
manager.root = Some(PaneNode::leaf(pane));
manager.focused_pane_id = Some(pane_id);
manager
}
pub fn with_initial_pane(
config: &Config,
runtime: Arc<Runtime>,
working_directory: Option<String>,
) -> Result<Self> {
let mut manager = Self::new();
manager.divider_width = config.panes.pane_divider_width.unwrap_or(1.0);
manager.divider_hit_width = config.panes.pane_divider_hit_width;
manager.create_initial_pane(config, runtime, working_directory)?;
Ok(manager)
}
pub fn next_pane_id(&self) -> PaneId {
self.next_pane_id
}
pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
self.root.as_ref()?.find_pane(id)
}
pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
self.root.as_mut()?.find_pane_mut(id)
}
pub fn all_panes(&self) -> Vec<&Pane> {
self.root
.as_ref()
.map(|r| r.all_panes())
.unwrap_or_default()
}
pub fn all_panes_mut(&mut self) -> Vec<&mut Pane> {
self.root
.as_mut()
.map(|r| r.all_panes_mut())
.unwrap_or_default()
}
pub fn collect_pane_backgrounds(&self) -> Vec<PaneBackgroundConfig> {
self.all_panes()
.iter()
.enumerate()
.filter_map(|(index, pane)| {
pane.background
.image_path
.as_ref()
.map(|path| PaneBackgroundConfig {
index,
image: path.clone(),
mode: pane.background.mode,
opacity: pane.background.opacity,
darken: pane.background.darken,
})
})
.collect()
}
pub fn pane_count(&self) -> usize {
self.root.as_ref().map(|r| r.pane_count()).unwrap_or(0)
}
pub fn has_multiple_panes(&self) -> bool {
self.pane_count() > 1
}
pub fn root(&self) -> Option<&PaneNode> {
self.root.as_ref()
}
pub fn root_mut(&mut self) -> Option<&mut PaneNode> {
self.root.as_mut()
}
#[must_use = "on success a renumbered pane is only reachable through the returned \
remap; on failure the returned subtree owns live terminals and dropping \
it kills them"]
pub fn insert_subtree_at(
&mut self,
target_pane_id: PaneId,
mut subtree: PaneNode,
direction: crate::pane::types::SplitDirection,
ratio: f32,
) -> Result<PaneIdRemap, PaneNode> {
let Some(root) = self.root.take() else {
return Err(subtree);
};
let remap = self.reconcile_adopted_ids(&root, &mut subtree);
match Self::insert_subtree_at_node(root, target_pane_id, subtree, direction, ratio) {
Ok(new_root) => {
self.root = Some(new_root);
self.recalculate_bounds();
debug_assert!(
self.pane_ids_are_unique(),
"pane ids are not unique after adopting a subtree: {:?}",
self.root.as_ref().map(PaneNode::all_pane_ids)
);
Ok(remap)
}
Err((original_root, mut subtree)) => {
self.root = Some(original_root);
Self::undo_reconcile(&mut subtree, &remap);
Err(subtree)
}
}
}
fn undo_reconcile(subtree: &mut PaneNode, remap: &PaneIdRemap) {
let arrived_as: HashMap<PaneId, PaneId> = remap
.iter()
.map(|(&arrived, &now)| (now, arrived))
.collect();
for pane in subtree.all_panes_mut() {
if let Some(&arrived) = arrived_as.get(&pane.id) {
pane.id = arrived;
}
}
}
fn reconcile_adopted_ids(
&mut self,
existing_root: &PaneNode,
subtree: &mut PaneNode,
) -> PaneIdRemap {
let existing: HashSet<PaneId> = existing_root.all_pane_ids().into_iter().collect();
let mut next = self.next_pane_id;
for id in existing.iter().copied().chain(subtree.all_pane_ids()) {
next = next.max(id.saturating_add(1));
}
let mut remap = PaneIdRemap::new();
for pane in subtree.all_panes_mut() {
let arrived_as = pane.id;
if existing.contains(&arrived_as) {
log::info!(
"Adopted pane id {} is already taken in this tab; renumbering it to {}",
arrived_as,
next
);
pane.id = next;
next = next.saturating_add(1);
}
remap.insert(arrived_as, pane.id);
}
self.next_pane_id = next;
remap
}
fn pane_ids_are_unique(&self) -> bool {
let ids = self
.root
.as_ref()
.map(PaneNode::all_pane_ids)
.unwrap_or_default();
ids.iter().collect::<HashSet<_>>().len() == ids.len()
}
pub fn extract_pane(&mut self, target_id: PaneId) -> ExtractResult {
if let Some(root) = self.root.take() {
match Self::extract_pane_from_node(root, target_id) {
ExtractInternal::Extracted { pane, remaining } => {
self.root = Some(remaining);
if let Some(id) = self.focused_pane_id
&& id == target_id
{
self.focused_pane_id = self.root.as_ref().and_then(|r| r.first_pane_id());
}
ExtractResult::Extracted {
pane,
remaining: self.root.take(),
}
}
ExtractInternal::OnlyPane(pane) => {
self.root = None;
self.focused_pane_id = None;
ExtractResult::OnlyPane(pane)
}
ExtractInternal::NotFound(node) => {
self.root = Some(node);
ExtractResult::NotFound
}
}
} else {
ExtractResult::NotFound
}
}
pub fn take_root(&mut self) -> Option<PaneNode> {
self.focused_pane_id = None;
self.root.take()
}
pub fn set_root(&mut self, node: PaneNode) {
self.root = Some(node);
self.recalculate_bounds();
}
}
impl Default for PaneManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pane::types::SplitDirection;
#[test]
fn test_pane_manager_new() {
let manager = PaneManager::new();
assert!(manager.root.is_none());
assert_eq!(manager.pane_count(), 0);
assert!(!manager.has_multiple_panes());
}
fn stub_pane(id: PaneId, marker: &str) -> Pane {
let terminal = TerminalManager::new_with_scrollback(80, 24, 100)
.expect("stub terminal creation without a shell");
Pane::new_wrapping_terminal(
id,
Arc::new(RwLock::new(terminal)),
Some(marker.to_string()),
Arc::new(AtomicBool::new(false)),
)
}
fn marker_of(manager: &PaneManager, id: PaneId) -> Option<String> {
manager.get_pane(id)?.working_directory.clone()
}
fn manager_with_two_panes(first: &str, second: &str) -> PaneManager {
let mut manager = PaneManager::new();
manager.root = Some(PaneNode::split(
SplitDirection::Vertical,
0.5,
PaneNode::leaf(stub_pane(1, first)),
PaneNode::leaf(stub_pane(2, second)),
));
manager.focused_pane_id = Some(1);
manager.next_pane_id = 3;
manager
}
#[test]
fn an_adopted_subtree_does_not_shadow_panes_that_already_hold_its_ids() {
const TARGET_1: &str = "/target/one";
const TARGET_2: &str = "/target/two";
const MOVED_1: &str = "/moved/one";
const MOVED_2: &str = "/moved/two";
let mut target = manager_with_two_panes(TARGET_1, TARGET_2);
let subtree = PaneNode::split(
SplitDirection::Horizontal,
0.5,
PaneNode::leaf(stub_pane(1, MOVED_1)),
PaneNode::leaf(stub_pane(2, MOVED_2)),
);
let Ok(remap) = target.insert_subtree_at(1, subtree, SplitDirection::Vertical, 0.5) else {
panic!("target pane 1 exists, so the insert succeeds");
};
assert_eq!(target.pane_count(), 4, "all four panes must survive");
for (arrived_as, marker) in [(1, MOVED_1), (2, MOVED_2)] {
let now = remap[&arrived_as];
assert_eq!(
marker_of(&target, now).as_deref(),
Some(marker),
"get_pane(remap[{arrived_as}]) must resolve to the pane that moved in, \
not to the pane that was already holding that id"
);
assert_ne!(now, arrived_as, "a colliding id must not be kept");
}
for (id, marker) in [(1, TARGET_1), (2, TARGET_2)] {
assert_eq!(
marker_of(&target, id).as_deref(),
Some(marker),
"the pane that already held id {id} must keep it"
);
}
}
#[test]
fn an_adopted_pane_keeps_an_id_that_is_free() {
let mut target = manager_with_two_panes("/target/one", "/target/two");
let Ok(remap) = target.insert_subtree_at(
1,
PaneNode::leaf(stub_pane(9, "/moved")),
SplitDirection::Vertical,
0.5,
) else {
panic!("target pane 1 exists, so the insert succeeds");
};
assert_eq!(remap[&9], 9, "an id that is free must be kept as-is");
assert_eq!(marker_of(&target, 9).as_deref(), Some("/moved"));
}
#[test]
fn an_adopted_pane_id_is_never_handed_out_again() {
let mut target = manager_with_two_panes("/target/one", "/target/two");
let inserted = target.insert_subtree_at(
1,
PaneNode::leaf(stub_pane(9, "/moved")),
SplitDirection::Vertical,
0.5,
);
assert!(
inserted.is_ok(),
"target pane 1 exists, so the insert succeeds"
);
let fresh = target.next_pane_id();
let live = target
.root
.as_ref()
.map(PaneNode::all_pane_ids)
.unwrap_or_default();
assert!(
live.iter().all(|id| fresh > *id),
"next id {fresh} must be past every adopted id {live:?}"
);
assert!(
!live.contains(&fresh),
"the next id {fresh} must be free, but the tree holds {live:?}"
);
}
#[test]
fn a_replacement_id_dodges_the_ids_the_subtree_keeps() {
let mut target = PaneManager::new();
target.root = Some(PaneNode::leaf(stub_pane(1, "/target")));
target.focused_pane_id = Some(1);
target.next_pane_id = 2;
let subtree = PaneNode::split(
SplitDirection::Horizontal,
0.5,
PaneNode::leaf(stub_pane(2, "/moved/two")),
PaneNode::leaf(stub_pane(1, "/moved/one")),
);
let Ok(remap) = target.insert_subtree_at(1, subtree, SplitDirection::Vertical, 0.5) else {
panic!("target pane 1 exists, so the insert succeeds");
};
assert_eq!(remap[&2], 2, "the non-colliding incoming id is kept");
assert_ne!(remap[&1], 2, "the replacement must not land on the kept id");
assert_eq!(marker_of(&target, remap[&1]).as_deref(), Some("/moved/one"));
assert_eq!(marker_of(&target, 2).as_deref(), Some("/moved/two"));
assert_eq!(marker_of(&target, 1).as_deref(), Some("/target"));
}
#[test]
fn a_failed_insert_leaves_the_tree_untouched() {
let mut target = manager_with_two_panes("/target/one", "/target/two");
let result = target.insert_subtree_at(
99,
PaneNode::leaf(stub_pane(1, "/moved")),
SplitDirection::Vertical,
0.5,
);
assert!(result.is_err(), "no such target pane, so no insertion");
assert_eq!(target.pane_count(), 2, "the original tree is restored");
assert_eq!(marker_of(&target, 1).as_deref(), Some("/target/one"));
assert_eq!(marker_of(&target, 2).as_deref(), Some("/target/two"));
}
#[test]
fn a_failed_insert_hands_the_subtree_back_alive() {
let mut target = manager_with_two_panes("/target/one", "/target/two");
let subtree = PaneNode::split(
SplitDirection::Horizontal,
0.5,
PaneNode::leaf(stub_pane(1, "/moved/one")),
PaneNode::leaf(stub_pane(2, "/moved/two")),
);
let kept_terminal = Arc::clone(&subtree.all_panes()[0].terminal);
let refs_before = Arc::strong_count(&kept_terminal);
let counter_before = target.next_pane_id();
let Err(returned) = target.insert_subtree_at(99, subtree, SplitDirection::Vertical, 0.5)
else {
panic!("no pane holds id 99, so the insert must fail");
};
assert_eq!(
Arc::strong_count(&kept_terminal),
refs_before,
"the moved terminal must still be referenced by its pane, not dropped"
);
assert_eq!(returned.pane_count(), 2, "both moved panes must come back");
let mut returned_ids = returned.all_pane_ids();
returned_ids.sort_unstable();
assert_eq!(
returned_ids,
vec![1, 2],
"the subtree must carry the ids it arrived with"
);
for (id, marker) in [(1, "/moved/one"), (2, "/moved/two")] {
assert_eq!(
returned
.find_pane(id)
.and_then(|p| p.working_directory.clone())
.as_deref(),
Some(marker),
"restored id {id} must name the pane that originally held it"
);
}
assert_eq!(marker_of(&target, 1).as_deref(), Some("/target/one"));
assert_eq!(marker_of(&target, 2).as_deref(), Some("/target/two"));
assert!(
target.next_pane_id() >= counter_before,
"the target's counter must not rewind onto an id its own panes hold"
);
}
}