use crate::input::RuntimeAction;
use crate::status::AttachStatusLine;
use bmux_attach_layout_protocol::{
AttachInputModeState, AttachMouseProtocolState, AttachScene, AttachSurface,
};
pub use bmux_attach_pipeline::{
AttachCursorState, AttachScrollbackCursor, AttachScrollbackPosition, PaneRect, PaneRenderBuffer,
};
use bmux_attach_pipeline::{FrameDamage, RetainedCompositor, TerminalGraphicsCache};
use bmux_client::AttachLayoutState;
use bmux_config::{MouseBehaviorConfig, StatusPosition};
use bmux_control_catalog_plugin_api::control_catalog_state::{
ContextRow, ContextSessionBinding, SessionRow,
};
use bmux_plugin::{AttachInputHook, AttachVisualProjectionUpdate};
use bmux_windows_plugin_api::windows_commands::PaneResizeDirection;
use bmux_windows_plugin_api::windows_list::WindowListSnapshot;
use crossterm::event::MouseEvent;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use uuid::Uuid;
use super::prompt_ui::AttachPromptState;
pub enum AttachEventAction {
Send(Vec<u8>),
PluginCommand {
plugin_id: String,
command_name: String,
args: Vec<String>,
},
Mouse(MouseEvent),
Ui(RuntimeAction),
Redraw,
Detach,
Ignore,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttachUiMode {
Normal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttachExitReason {
Detached,
StreamClosed,
Quit,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttachDirtySource {
PaneOutput,
LayoutChanged,
FocusChanged,
SceneChanged,
StatusChanged,
PromptOverlay,
HelpOverlay,
AppearanceChanged,
ManualRedraw,
SnapshotHydration,
AlternateScreenTransition,
PluginCommand,
UserAction,
ActionDispatch,
Mouse,
Scrollback,
Selection,
ProfileChanged,
ControlCatalogChanged,
PaneLifecycle,
FollowTargetChanged,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttachDirtyKind {
Pane,
Status,
Overlay,
FullFrame,
Extension,
PreciseDamage,
LayoutRefresh,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AttachDirtyEvent {
pub source: AttachDirtySource,
pub kind: AttachDirtyKind,
pub pane_id: Option<Uuid>,
}
#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)]
pub struct AttachDirtyFlags {
pub status_needs_redraw: bool,
pub layout_needs_refresh: bool,
pub overlay_needs_redraw: bool,
pub pane_dirty_ids: BTreeSet<Uuid>,
pub full_pane_redraw: bool,
pub extension_needs_redraw: bool,
pub precise_frame_damage: FrameDamage,
pub dirty_events: Vec<AttachDirtyEvent>,
}
impl Default for AttachDirtyFlags {
fn default() -> Self {
Self {
status_needs_redraw: true,
layout_needs_refresh: true,
overlay_needs_redraw: false,
pane_dirty_ids: BTreeSet::new(),
full_pane_redraw: true,
extension_needs_redraw: true,
precise_frame_damage: FrameDamage::default(),
dirty_events: Vec::new(),
}
}
}
impl AttachDirtyFlags {
fn push_event(
&mut self,
source: AttachDirtySource,
kind: AttachDirtyKind,
pane_id: Option<Uuid>,
) {
self.dirty_events.push(AttachDirtyEvent {
source,
kind,
pane_id,
});
}
pub fn mark_pane_dirty(&mut self, pane_id: Uuid, source: AttachDirtySource) {
self.pane_dirty_ids.insert(pane_id);
self.push_event(source, AttachDirtyKind::Pane, Some(pane_id));
}
pub fn mark_status_dirty(&mut self, source: AttachDirtySource) {
self.status_needs_redraw = true;
self.push_event(source, AttachDirtyKind::Status, None);
}
pub fn mark_overlay_dirty(&mut self, source: AttachDirtySource) {
self.overlay_needs_redraw = true;
self.push_event(source, AttachDirtyKind::Overlay, None);
}
pub fn mark_full_frame(&mut self, source: AttachDirtySource) {
self.full_pane_redraw = true;
self.push_event(source, AttachDirtyKind::FullFrame, None);
}
pub fn mark_extension_dirty(&mut self, source: AttachDirtySource) {
self.extension_needs_redraw = true;
self.push_event(source, AttachDirtyKind::Extension, None);
}
pub fn mark_surface_changed(&mut self, surface_id: Uuid, source: AttachDirtySource) {
self.precise_frame_damage
.mark_extension_surface_query(surface_id);
self.push_event(source, AttachDirtyKind::PreciseDamage, None);
}
pub fn mark_layout_refresh(&mut self, source: AttachDirtySource) {
self.layout_needs_refresh = true;
self.push_event(source, AttachDirtyKind::LayoutRefresh, None);
}
pub fn mark_layout_frame_dirty(&mut self, source: AttachDirtySource) {
self.mark_layout_refresh(source);
self.mark_full_frame(source);
}
pub fn mark_layout_frame_and_status_dirty(&mut self, source: AttachDirtySource) {
self.mark_layout_frame_dirty(source);
self.mark_status_dirty(source);
}
pub fn mark_layout_refresh_and_status_dirty(&mut self, source: AttachDirtySource) {
self.mark_layout_refresh(source);
self.mark_status_dirty(source);
}
pub fn merge_precise_damage(&mut self, damage: &FrameDamage, source: AttachDirtySource) {
if damage.is_empty() {
return;
}
self.precise_frame_damage.merge_from(damage);
self.push_event(source, AttachDirtyKind::PreciseDamage, None);
}
#[must_use]
pub fn dirty_events(&self) -> &[AttachDirtyEvent] {
&self.dirty_events
}
#[must_use]
pub fn frame_damage(&self, scene: &AttachScene) -> FrameDamage {
let mut damage = if self.full_pane_redraw {
FrameDamage::full_frame()
} else {
FrameDamage::default()
};
damage.merge_from(&self.precise_frame_damage);
for pane_id in &self.pane_dirty_ids {
damage.mark_content_surface(*pane_id);
}
if self.extension_needs_redraw {
let _ = scene;
damage.mark_extension_query();
}
if self.status_needs_redraw {
damage.mark_status();
}
if self.overlay_needs_redraw {
damage.mark_overlay();
}
damage
}
#[must_use]
pub fn needs_render(&self) -> bool {
self.status_needs_redraw
|| self.full_pane_redraw
|| self.extension_needs_redraw
|| self.overlay_needs_redraw
|| !self.precise_frame_damage.is_empty()
|| !self.pane_dirty_ids.is_empty()
}
pub fn clear_frame_damage(&mut self) {
self.full_pane_redraw = false;
self.extension_needs_redraw = false;
self.overlay_needs_redraw = false;
self.precise_frame_damage = FrameDamage::default();
self.pane_dirty_ids.clear();
self.dirty_events.clear();
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct AttachViewState {
pub self_client_id: Option<Uuid>,
pub attached_id: Uuid,
pub attached_context_id: Option<Uuid>,
pub can_write: bool,
pub ui_mode: AttachUiMode,
pub active_mode_id: String,
pub active_mode_label: String,
pub scrollback_active: bool,
pub scrollback_offset: usize,
pub scrollback_cursor: Option<AttachScrollbackCursor>,
pub selection_anchor: Option<AttachScrollbackPosition>,
pub help_overlay_open: bool,
pub help_overlay_scroll: usize,
pub prompt: AttachPromptState,
pub transient_status: Option<String>,
pub transient_status_until: Option<Instant>,
pub control_catalog_revision: u64,
pub cached_window_list: Option<Arc<WindowListSnapshot>>,
pub cached_contexts: Vec<ContextRow>,
pub cached_sessions: Vec<SessionRow>,
pub cached_context_session_bindings: Vec<ContextSessionBinding>,
pub pane_buffers: BTreeMap<Uuid, PaneRenderBuffer>,
pub terminal_graphics_cache: TerminalGraphicsCache,
pub pane_mouse_protocol_hints: BTreeMap<Uuid, AttachMouseProtocolState>,
pub pane_input_mode_hints: BTreeMap<Uuid, AttachInputModeState>,
pub status_position: StatusPosition,
pub cached_status_line: Option<AttachStatusLine>,
pub cached_layout_state: Option<AttachLayoutState>,
pub retained_compositor: RetainedCompositor,
pub last_help_overlay_surface: Option<AttachSurface>,
pub last_prompt_overlay_surface: Option<AttachSurface>,
pub last_cursor_state: Option<AttachCursorState>,
pub force_cursor_move_next_frame: bool,
pub mouse: AttachMouseState,
pub visual_projection_updates: Vec<AttachVisualProjectionUpdate>,
pub dirty: AttachDirtyFlags,
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pub pane_images: BTreeMap<Uuid, Vec<bmux_attach_image_protocol::AttachPaneImage>>,
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pub image_sequences: BTreeMap<Uuid, u64>,
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pub host_image_caps: bmux_image::HostImageCapabilities,
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pub kitty_host_state: bmux_image::compositor::KittyHostState,
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pub image_decode_mode: bmux_image::config::ImageDecodeMode,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct AttachMouseState {
pub config: MouseBehaviorConfig,
pub tab_drag_enabled: bool,
pub last_position: Option<(u16, u16)>,
pub last_event_at: Option<Instant>,
pub hover_started_at: Option<Instant>,
pub hovered_pane_id: Option<Uuid>,
pub last_focused_pane_id: Option<Uuid>,
pub resize_drag: Option<AttachMouseResizeDrag>,
pub floating_drag: Option<AttachMouseFloatingDrag>,
pub selection_drag: Option<AttachMouseSelectionDrag>,
pub tab_drag: Option<AttachMouseTabDrag>,
pub input_capture: Option<AttachInputHookCapture>,
pub input_hook_last_dispatched_at: BTreeMap<String, Instant>,
}
impl Default for AttachMouseState {
fn default() -> Self {
Self {
config: MouseBehaviorConfig::default(),
tab_drag_enabled: true,
last_position: None,
last_event_at: None,
hover_started_at: None,
hovered_pane_id: None,
last_focused_pane_id: None,
resize_drag: None,
floating_drag: None,
selection_drag: None,
tab_drag: None,
input_capture: None,
input_hook_last_dispatched_at: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachInputHookCapture {
pub hook: AttachInputHook,
pub pointer: bool,
pub keyboard_keys: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachMouseTabDrag {
pub source_context_id: Uuid,
pub started_col: u16,
pub started_row: u16,
pub active: bool,
pub drop_target: Option<AttachTabDropTarget>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachTabDropTarget {
pub context_id: Uuid,
pub placement: AttachTabDropPlacement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttachTabDropPlacement {
Before,
After,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachUiReduction {
pub consumed: bool,
pub effects: Vec<AttachUiEffect>,
}
impl AttachUiReduction {
pub const fn ignored() -> Self {
Self {
consumed: false,
effects: Vec::new(),
}
}
pub const fn consumed() -> Self {
Self {
consumed: true,
effects: Vec::new(),
}
}
pub fn with_effect(effect: AttachUiEffect) -> Self {
Self {
consumed: true,
effects: vec![effect],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttachUiEffect {
SwitchWindow {
target_context_id: Uuid,
},
MoveWindow {
source_context_id: Uuid,
target_context_id: Uuid,
placement: AttachTabDropPlacement,
},
ResizePane {
pane_id: Uuid,
direction: PaneResizeDirection,
cells: u16,
},
FocusPane {
pane_id: Uuid,
},
MoveFloatingPane {
pane_id: Uuid,
x: u16,
y: u16,
},
ShowTransientStatus {
message: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachMouseFloatingDrag {
pub pane_id: Uuid,
pub start_x: u16,
pub start_y: u16,
pub width: u16,
pub height: u16,
pub scene_max_x: u16,
pub scene_max_y: u16,
pub last_x: u16,
pub last_y: u16,
pub start_column: u16,
pub start_row: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachMouseSelectionDrag {
pub pane_id: Uuid,
pub anchor: AttachScrollbackPosition,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachMouseResizeDrag {
pub horizontal: Option<AttachMouseResizeAxisDrag>,
pub vertical: Option<AttachMouseResizeAxisDrag>,
pub last_column: u16,
pub last_row: u16,
pub latest_column: u16,
pub latest_row: u16,
pub last_applied_at: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachMouseResizeAxisDrag {
pub positive_target_pane_id: Uuid,
pub positive_direction: PaneResizeDirection,
pub negative_target_pane_id: Uuid,
pub negative_direction: PaneResizeDirection,
}
impl AttachViewState {
pub fn new(attach_info: bmux_client::AttachOpenInfo) -> Self {
Self {
self_client_id: None,
attached_id: attach_info.session_id,
attached_context_id: attach_info.context_id,
can_write: attach_info.can_write,
ui_mode: AttachUiMode::Normal,
active_mode_id: "normal".to_string(),
active_mode_label: "NORMAL".to_string(),
scrollback_active: false,
scrollback_offset: 0,
scrollback_cursor: None,
selection_anchor: None,
help_overlay_open: false,
help_overlay_scroll: 0,
prompt: AttachPromptState::default(),
transient_status: None,
transient_status_until: None,
control_catalog_revision: 0,
cached_window_list: None,
cached_contexts: Vec::new(),
cached_sessions: Vec::new(),
cached_context_session_bindings: Vec::new(),
pane_buffers: BTreeMap::new(),
terminal_graphics_cache: TerminalGraphicsCache::new(),
pane_mouse_protocol_hints: BTreeMap::new(),
pane_input_mode_hints: BTreeMap::new(),
status_position: StatusPosition::Bottom,
cached_status_line: None,
cached_layout_state: None,
retained_compositor: RetainedCompositor::new(),
last_help_overlay_surface: None,
last_prompt_overlay_surface: None,
last_cursor_state: None,
force_cursor_move_next_frame: false,
mouse: AttachMouseState {
config: MouseBehaviorConfig::default(),
..AttachMouseState::default()
},
visual_projection_updates: Vec::new(),
dirty: AttachDirtyFlags::default(),
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
pane_images: BTreeMap::new(),
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
image_sequences: BTreeMap::new(),
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
host_image_caps: bmux_image::HostImageCapabilities::default(),
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
kitty_host_state: bmux_image::compositor::KittyHostState::default(),
#[cfg(any(
feature = "image-sixel",
feature = "image-kitty",
feature = "image-iterm2"
))]
image_decode_mode: bmux_image::config::ImageDecodeMode::Passthrough,
}
}
pub fn set_transient_status(
&mut self,
message: impl Into<String>,
now: Instant,
ttl: Duration,
) {
self.transient_status = Some(message.into());
self.transient_status_until = Some(now + ttl);
self.dirty
.mark_status_dirty(AttachDirtySource::StatusChanged);
}
pub fn clear_expired_transient_status(&mut self, now: Instant) -> bool {
let Some(until) = self.transient_status_until else {
return false;
};
if now < until {
return false;
}
self.transient_status = None;
self.transient_status_until = None;
self.dirty
.mark_status_dirty(AttachDirtySource::StatusChanged);
true
}
pub fn transient_status_text(&self, now: Instant) -> Option<&str> {
if self
.transient_status_until
.is_some_and(|until| now >= until)
{
return None;
}
self.transient_status.as_deref()
}
pub const fn exit_scrollback(&mut self) {
self.scrollback_active = false;
self.scrollback_offset = 0;
self.scrollback_cursor = None;
self.selection_anchor = None;
}
pub const fn selection_active(&self) -> bool {
self.selection_anchor.is_some()
}
}