use std::collections::HashMap;
use ratatui::layout::Direction;
use tokio::sync::mpsc;
use tracing::info;
use crate::detect::{self, AgentState};
use crate::events::AppEvent;
use crate::layout::{PaneId, TileLayout};
use crate::pane::{PaneRuntime, PaneState};
pub struct Workspace {
pub name: String,
pub layout: TileLayout,
pub panes: HashMap<PaneId, PaneState>,
pub runtimes: HashMap<PaneId, PaneRuntime>,
pub zoomed: bool,
pub events: mpsc::Sender<AppEvent>,
}
impl Workspace {
pub fn new(
name: String,
rows: u16,
cols: u16,
events: mpsc::Sender<AppEvent>,
) -> std::io::Result<Self> {
let (layout, root_id) = TileLayout::new();
let cwd = std::env::current_dir().unwrap_or_else(|_| "/".into());
let runtime = PaneRuntime::spawn(root_id, rows, cols, cwd, events.clone())?;
let mut panes = HashMap::new();
panes.insert(root_id, PaneState::new());
let mut runtimes = HashMap::new();
runtimes.insert(root_id, runtime);
info!(workspace = %name, root_pane = root_id.raw(), "workspace created");
Ok(Self {
name,
layout,
panes,
runtimes,
zoomed: false,
events,
})
}
pub fn split_focused(
&mut self,
direction: Direction,
rows: u16,
cols: u16,
cwd: Option<std::path::PathBuf>,
) -> std::io::Result<PaneId> {
let new_id = self.layout.split_focused(direction);
let actual_cwd = cwd.unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| "/".into())
});
let runtime = PaneRuntime::spawn(new_id, rows, cols, actual_cwd, self.events.clone())?;
self.panes.insert(new_id, PaneState::new());
self.runtimes.insert(new_id, runtime);
self.zoomed = false;
Ok(new_id)
}
pub fn close_focused(&mut self) -> Option<PaneId> {
if self.layout.pane_count() <= 1 {
return None;
}
let closed = self.layout.focused();
if self.layout.close_focused() {
self.panes.remove(&closed);
self.runtimes.remove(&closed);
self.zoomed = false;
Some(closed)
} else {
None
}
}
pub fn focused_runtime(&self) -> Option<&PaneRuntime> {
self.runtimes.get(&self.layout.focused())
}
pub fn aggregate_state(&self) -> (AgentState, bool) {
let states: Vec<AgentState> = self.panes.values().map(|p| p.state).collect();
let state = detect::workspace_state(&states);
let seen = self.panes.values().all(|p| p.seen);
(state, seen)
}
pub fn pane_states(&self) -> Vec<(AgentState, bool)> {
self.layout
.pane_ids()
.iter()
.map(|id| {
self.panes
.get(id)
.map(|p| (p.state, p.seen))
.unwrap_or((AgentState::Unknown, true))
})
.collect()
}
}
#[cfg(test)]
impl Workspace {
pub fn test_new(name: &str) -> Self {
let (events, _) = mpsc::channel(64);
let (layout, root_id) = TileLayout::new();
let mut panes = HashMap::new();
panes.insert(root_id, PaneState::new());
Self {
name: name.to_string(),
layout,
panes,
runtimes: HashMap::new(),
zoomed: false,
events,
}
}
pub fn test_split(&mut self, direction: Direction) -> PaneId {
let new_id = self.layout.split_focused(direction);
self.panes.insert(new_id, PaneState::new());
new_id
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detect::AgentState;
#[test]
fn aggregate_state_all_unknown() {
let ws = Workspace::test_new("test");
let (state, seen) = ws.aggregate_state();
assert_eq!(state, AgentState::Unknown);
assert!(seen);
}
#[test]
fn aggregate_state_priority() {
let mut ws = Workspace::test_new("test");
let id2 = ws.test_split(Direction::Horizontal);
let root_id = *ws.panes.keys().find(|id| **id != id2).unwrap();
ws.panes.get_mut(&root_id).unwrap().state = AgentState::Idle;
ws.panes.get_mut(&id2).unwrap().state = AgentState::Busy;
let (state, _) = ws.aggregate_state();
assert_eq!(state, AgentState::Busy);
}
#[test]
fn aggregate_seen_any_unseen_means_unseen() {
let mut ws = Workspace::test_new("test");
let id2 = ws.test_split(Direction::Horizontal);
ws.panes.get_mut(&id2).unwrap().seen = false;
let (_, seen) = ws.aggregate_state();
assert!(!seen);
}
#[test]
fn close_focused_removes_pane() {
let mut ws = Workspace::test_new("test");
let _id2 = ws.test_split(Direction::Horizontal);
assert_eq!(ws.panes.len(), 2);
let closed = ws.close_focused();
assert!(closed.is_some());
assert_eq!(ws.panes.len(), 1);
}
#[test]
fn close_focused_last_pane_returns_none() {
let mut ws = Workspace::test_new("test");
assert_eq!(ws.panes.len(), 1);
let closed = ws.close_focused();
assert!(closed.is_none());
assert_eq!(ws.panes.len(), 1);
}
#[test]
fn pane_states_matches_layout_order() {
let mut ws = Workspace::test_new("test");
let id2 = ws.test_split(Direction::Horizontal);
ws.panes.get_mut(&id2).unwrap().state = AgentState::Waiting;
let states = ws.pane_states();
assert_eq!(states.len(), 2);
assert!(states.iter().any(|(s, _)| *s == AgentState::Waiting));
assert!(states.iter().any(|(s, _)| *s == AgentState::Unknown));
}
}