swimmers 0.2.0

Axum server plus TUI for orchestrating Claude Code and Codex agents across tmux panes
Documentation
use super::*;

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct Rect {
    pub(crate) x: u16,
    pub(crate) y: u16,
    pub(crate) width: u16,
    pub(crate) height: u16,
}

impl Rect {
    pub(crate) fn right(self) -> u16 {
        self.x + self.width
    }

    pub(crate) fn bottom(self) -> u16 {
        self.y + self.height
    }

    pub(crate) fn contains(self, x: u16, y: u16) -> bool {
        x >= self.x && y >= self.y && x < self.right() && y < self.bottom()
    }

    pub(crate) fn inset(self, amount: u16) -> Self {
        if self.width <= amount * 2 || self.height <= amount * 2 {
            return Self {
                x: self.x,
                y: self.y,
                width: 0,
                height: 0,
            };
        }
        Self {
            x: self.x + amount,
            y: self.y + amount,
            width: self.width - amount * 2,
            height: self.height - amount * 2,
        }
    }
}

#[derive(Clone, Copy)]
pub(crate) struct WorkspaceLayout {
    pub(crate) workspace_box: Rect,
    pub(crate) overview_box: Rect,
    pub(crate) overview_field: Rect,
    pub(crate) thought_box: Option<Rect>,
    pub(crate) thought_content: Option<Rect>,
    pub(crate) split_divider: Option<Rect>,
    pub(crate) split_hitbox: Option<Rect>,
    pub(crate) footer_start_y: u16,
}

impl WorkspaceLayout {
    #[allow(dead_code)]
    pub(crate) fn for_terminal(width: u16, height: u16) -> Self {
        Self::for_terminal_with_ratio(width, height, THOUGHT_RAIL_DEFAULT_RATIO)
    }

    pub(crate) fn for_terminal_with_ratio(width: u16, height: u16, thought_ratio: f32) -> Self {
        let workspace_box = field_box(width, height);
        let footer_start_y = workspace_box.bottom() + 1;
        let inner = workspace_box.inset(1);

        let split_allowed = width >= THOUGHT_RAIL_MIN_WIDTH
            && inner.height >= 3
            && inner.width >= THOUGHT_RAIL_MIN_PANEL_WIDTH + THOUGHT_RAIL_GAP + ENTITY_WIDTH + 4;
        if !split_allowed {
            return Self {
                workspace_box,
                overview_box: workspace_box,
                overview_field: inner,
                thought_box: None,
                thought_content: None,
                split_divider: None,
                split_hitbox: None,
                footer_start_y,
            };
        }

        let min_overview_width = ENTITY_WIDTH + 4;
        let sanitized_ratio = if thought_ratio.is_finite() {
            thought_ratio.clamp(0.0, 1.0)
        } else {
            THOUGHT_RAIL_DEFAULT_RATIO
        };
        let ideal_thought_width = ((inner.width as f32) * sanitized_ratio).floor() as u16;
        let ideal_thought_width = ideal_thought_width.max(THOUGHT_RAIL_MIN_PANEL_WIDTH);
        let max_thought_width = inner
            .width
            .saturating_sub(THOUGHT_RAIL_GAP + min_overview_width);
        let thought_width = ideal_thought_width.min(max_thought_width);
        let overview_width = inner.width.saturating_sub(thought_width + THOUGHT_RAIL_GAP);
        if overview_width < min_overview_width {
            return Self {
                workspace_box,
                overview_box: workspace_box,
                overview_field: inner,
                thought_box: None,
                thought_content: None,
                split_divider: None,
                split_hitbox: None,
                footer_start_y,
            };
        }

        let thought_box = Rect {
            x: inner.x,
            y: inner.y,
            width: thought_width,
            height: inner.height,
        };
        let overview_box = Rect {
            x: thought_box.right() + THOUGHT_RAIL_GAP,
            y: inner.y,
            width: overview_width,
            height: inner.height,
        };
        let split_divider = Rect {
            x: thought_box.right(),
            y: inner.y,
            width: THOUGHT_RAIL_GAP,
            height: inner.height,
        };
        let split_hitbox = Rect {
            x: split_divider.x.saturating_sub(1),
            y: inner.y,
            width: THOUGHT_RAIL_DRAG_HITBOX_WIDTH,
            height: inner.height,
        };

        Self {
            workspace_box,
            overview_box,
            overview_field: overview_box.inset(1),
            thought_box: Some(thought_box),
            thought_content: Some(thought_box.inset(1)),
            split_divider: Some(split_divider),
            split_hitbox: Some(split_hitbox),
            footer_start_y,
        }
    }

    pub(crate) fn thought_entry_capacity(self) -> usize {
        self.thought_content
            .map(|content| content.height.saturating_sub(THOUGHT_RAIL_HEADER_ROWS) as usize)
            .unwrap_or(0)
    }

    pub(crate) fn thought_ratio_for_divider_x(self, x: u16) -> Option<f32> {
        let thought_box = self.thought_box?;
        let inner = self.workspace_box.inset(1);
        let min_overview_width = ENTITY_WIDTH + 4;
        let max_thought_width = inner
            .width
            .saturating_sub(THOUGHT_RAIL_GAP + min_overview_width);
        if max_thought_width < THOUGHT_RAIL_MIN_PANEL_WIDTH || inner.width == 0 {
            return None;
        }

        let requested_width = x.saturating_sub(thought_box.x);
        let thought_width = requested_width.clamp(THOUGHT_RAIL_MIN_PANEL_WIDTH, max_thought_width);
        Some(thought_width as f32 / inner.width as f32)
    }
}

pub(crate) fn frame_rect(width: u16, height: u16) -> Rect {
    Rect {
        x: 0,
        y: 0,
        width,
        height,
    }
}

pub(crate) fn field_box(width: u16, height: u16) -> Rect {
    let footer_height = 6;
    Rect {
        x: 1,
        y: 3,
        width: width.saturating_sub(2),
        height: height.saturating_sub(footer_height + 3),
    }
}