Skip to main content

scarab_protocol/
lib.rs

1#![no_std]
2// This crate defines the data layout shared between Daemon and Client.
3// It must be #[repr(C)] to ensure memory layout consistency across processes.
4
5use bytemuck::{Pod, Zeroable};
6
7// Safe abstraction layer for SharedState access
8pub mod terminal_state;
9pub use terminal_state::TerminalStateReader;
10
11// Semantic zones for deep shell integration
12pub mod zones;
13pub use zones::{CommandBlock, SemanticZone, ZoneTracker, ZoneType};
14
15/// Default shared memory path for terminal state.
16/// Can be overridden via SCARAB_SHMEM_PATH environment variable.
17pub const SHMEM_PATH: &str = "/scarab_shm_v1";
18
19/// Environment variable to override the shared memory path.
20/// Useful for sandboxed environments where /dev/shm is not writable.
21pub const SHMEM_PATH_ENV: &str = "SCARAB_SHMEM_PATH";
22
23/// Environment variable to override the image shared memory path.
24pub const IMAGE_SHMEM_PATH_ENV: &str = "SCARAB_IMAGE_SHMEM_PATH";
25pub const GRID_WIDTH: usize = 200;
26pub const GRID_HEIGHT: usize = 100;
27pub const BUFFER_SIZE: usize = GRID_WIDTH * GRID_HEIGHT;
28
29#[repr(C)]
30#[derive(Copy, Clone, Pod, Zeroable)]
31pub struct Cell {
32    pub char_codepoint: u32,
33    pub fg: u32,           // RGBA
34    pub bg: u32,           // RGBA
35    pub flags: u8,         // Bold, Italic, etc.
36    pub _padding: [u8; 3], // Align to 16 bytes
37}
38
39impl Default for Cell {
40    fn default() -> Self {
41        Self {
42            char_codepoint: b' ' as u32,
43            fg: 0xFFA8DF5A, // Slime green foreground (ARGB: #a8df5a)
44            bg: 0xFF0D1208, // Slime dark background (ARGB: #0d1208)
45            flags: 0,
46            _padding: [0; 3],
47        }
48    }
49}
50
51// A double-buffered grid state living in shared memory
52#[repr(C)]
53#[derive(Copy, Clone)]
54pub struct SharedState {
55    pub sequence_number: u64, // Atomic sequence for synchronization
56    pub dirty_flag: u8,
57    pub error_mode: u8, // 0 = normal mode, 1 = error mode (PTY/SHM unavailable)
58    pub cursor_x: u16,
59    pub cursor_y: u16,
60    pub _padding2: [u8; 2], // Align to u64 boundary for cells array
61    // Fixed size buffer for the "visible" screen.
62    // In production, use offset pointers to a larger ring buffer.
63    pub cells: [Cell; BUFFER_SIZE],
64}
65
66// Manual implementations needed for large arrays
67unsafe impl Pod for SharedState {}
68unsafe impl Zeroable for SharedState {}
69
70// Image buffer constants
71/// Maximum number of concurrent image placements
72pub const MAX_IMAGES: usize = 64;
73
74/// Maximum total image buffer size (16MB)
75pub const IMAGE_BUFFER_SIZE: usize = 16 * 1024 * 1024;
76
77/// Default shared memory path for image buffer (separate from terminal state).
78/// Can be overridden via SCARAB_IMAGE_SHMEM_PATH environment variable.
79pub const IMAGE_SHMEM_PATH: &str = "/scarab_img_shm_v1";
80
81/// Image placement metadata for shared memory
82#[repr(C)]
83#[derive(Copy, Clone)]
84pub struct SharedImagePlacement {
85    /// Unique identifier for this placement
86    pub image_id: u64,
87    /// Column position in terminal grid
88    pub x: u16,
89    /// Row position in terminal grid
90    pub y: u16,
91    /// Width in terminal cells
92    pub width_cells: u16,
93    /// Height in terminal cells
94    pub height_cells: u16,
95    /// Pixel width of decoded image
96    pub pixel_width: u32,
97    /// Pixel height of decoded image
98    pub pixel_height: u32,
99    /// Offset into blob_data buffer
100    pub blob_offset: u32,
101    /// Size of image data in bytes
102    pub blob_size: u32,
103    /// Image format (0=PNG, 1=JPEG, 2=GIF, 3=RGBA)
104    pub format: u8,
105    /// Flags (bit 0: valid/active)
106    pub flags: u8,
107    /// Padding for alignment
108    pub _padding: [u8; 6],
109}
110
111// Manual Pod/Zeroable implementations
112unsafe impl Pod for SharedImagePlacement {}
113unsafe impl Zeroable for SharedImagePlacement {}
114
115impl SharedImagePlacement {
116    /// Check if this placement is valid/active
117    pub const fn is_valid(&self) -> bool {
118        (self.flags & 0x01) != 0
119    }
120
121    /// Mark this placement as valid/active
122    pub fn set_valid(&mut self) {
123        self.flags |= 0x01;
124    }
125
126    /// Mark this placement as invalid/inactive
127    pub fn set_invalid(&mut self) {
128        self.flags &= !0x01;
129    }
130}
131
132/// Shared memory buffer for image data
133#[repr(C)]
134#[derive(Copy, Clone)]
135pub struct SharedImageBuffer {
136    /// Sequence number for synchronization (increment on any change)
137    pub sequence_number: u64,
138    /// Number of active placements
139    pub count: u32,
140    /// Next blob write offset (circular buffer pointer)
141    pub next_blob_offset: u32,
142    /// Image placement metadata array
143    pub placements: [SharedImagePlacement; MAX_IMAGES],
144    /// Raw image blob data (circular buffer)
145    pub blob_data: [u8; IMAGE_BUFFER_SIZE],
146}
147
148// Manual Pod/Zeroable implementations for large array
149unsafe impl Pod for SharedImageBuffer {}
150unsafe impl Zeroable for SharedImageBuffer {}
151
152// Log levels for plugin logging
153#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
154#[archive(check_bytes)]
155pub enum LogLevel {
156    Error,
157    Warn,
158    Info,
159    Debug,
160}
161
162// Notification severity levels
163#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
164#[archive(check_bytes)]
165pub enum NotifyLevel {
166    Error,
167    Warning,
168    Info,
169    Success,
170}
171
172// Tab/Pane split direction
173#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
174#[archive(check_bytes)]
175pub enum SplitDirection {
176    Horizontal,
177    Vertical,
178}
179
180// Menu action types from plugin API
181#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
182#[archive(check_bytes)]
183pub enum MenuActionType {
184    Command { command: alloc::string::String },
185    Remote { id: alloc::string::String },
186}
187
188// Navigation focusable action types
189#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
190#[archive(check_bytes)]
191pub enum NavFocusableAction {
192    /// Open a URL in the default browser
193    OpenUrl(alloc::string::String),
194    /// Open a file in the configured editor
195    OpenFile(alloc::string::String),
196    /// Custom plugin-defined action
197    Custom(alloc::string::String),
198}
199
200// Control messages (Sent via Socket/Pipe, not ShMem)
201// Using rkyv for zero-copy serialization
202#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
203#[archive(check_bytes)]
204pub enum ControlMessage {
205    Resize {
206        cols: u16,
207        rows: u16,
208    },
209    Input {
210        data: alloc::vec::Vec<u8>,
211    },
212    LoadPlugin {
213        path: alloc::string::String,
214    },
215    Ping {
216        timestamp: u64,
217    },
218    Disconnect {
219        client_id: u64,
220    },
221
222    // Session management commands
223    SessionCreate {
224        name: alloc::string::String,
225    },
226    SessionDelete {
227        id: alloc::string::String,
228    },
229    SessionList,
230    SessionAttach {
231        id: alloc::string::String,
232    },
233    SessionDetach {
234        id: alloc::string::String,
235    },
236    SessionRename {
237        id: alloc::string::String,
238        new_name: alloc::string::String,
239    },
240
241    // Tab management commands
242    TabCreate {
243        title: Option<alloc::string::String>,
244    },
245    TabClose {
246        tab_id: u64,
247    },
248    TabSwitch {
249        tab_id: u64,
250    },
251    TabRename {
252        tab_id: u64,
253        new_title: alloc::string::String,
254    },
255    TabList,
256
257    // Pane management commands
258    PaneSplit {
259        pane_id: u64,
260        direction: SplitDirection,
261    },
262    PaneClose {
263        pane_id: u64,
264    },
265    PaneFocus {
266        pane_id: u64,
267    },
268    PaneResize {
269        pane_id: u64,
270        width: u16,
271        height: u16,
272    },
273    /// Focus the next pane in the current tab (for navigation)
274    PaneFocusNext,
275    /// Focus the previous pane in the current tab (for navigation)
276    PaneFocusPrev,
277
278    // Tab navigation commands
279    /// Switch to the next tab
280    TabNext,
281    /// Switch to the previous tab
282    TabPrev,
283
284    // Mouse input commands
285    /// Send mouse click event to the terminal
286    MouseClick {
287        col: u16,
288        row: u16,
289        button: u8,
290    },
291
292    // Remote UI Responses
293    CommandSelected {
294        id: alloc::string::String,
295    },
296
297    // Plugin inspection commands
298    PluginListRequest,
299    PluginEnable {
300        name: alloc::string::String,
301    },
302    PluginDisable {
303        name: alloc::string::String,
304    },
305    PluginReload {
306        name: alloc::string::String,
307    },
308
309    // Plugin menu commands
310    PluginMenuRequest {
311        plugin_name: alloc::string::String,
312    },
313    PluginMenuExecute {
314        plugin_name: alloc::string::String,
315        action: MenuActionType,
316    },
317
318    // Plugin logging and notifications (sent from daemon to client)
319    PluginLog {
320        plugin_name: alloc::string::String,
321        level: LogLevel,
322        message: alloc::string::String,
323    },
324    PluginNotify {
325        title: alloc::string::String,
326        body: alloc::string::String,
327        level: NotifyLevel,
328    },
329
330    // Navigation API commands (sent from plugins via client to daemon/client)
331    NavEnterHintMode {
332        plugin_name: alloc::string::String,
333    },
334    NavExitMode {
335        plugin_name: alloc::string::String,
336    },
337    NavRegisterFocusable {
338        plugin_name: alloc::string::String,
339        x: u16,
340        y: u16,
341        width: u16,
342        height: u16,
343        label: alloc::string::String,
344        action: NavFocusableAction,
345    },
346    NavUnregisterFocusable {
347        plugin_name: alloc::string::String,
348        focusable_id: u64,
349    },
350
351    // Semantic zone commands (deep shell integration)
352    /// Request semantic zones update from daemon
353    ZonesRequest,
354
355    /// Copy the output from the last completed command
356    CopyLastOutput,
357
358    /// Select a specific zone by ID
359    SelectZone {
360        zone_id: u64,
361    },
362
363    /// Extract text from a zone
364    ExtractZoneText {
365        zone_id: u64,
366    },
367}
368
369// Session response messages
370#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
371#[archive(check_bytes)]
372pub enum SessionResponse {
373    Created {
374        id: alloc::string::String,
375        name: alloc::string::String,
376    },
377    Deleted {
378        id: alloc::string::String,
379    },
380    List {
381        sessions: alloc::vec::Vec<SessionInfo>,
382    },
383    Attached {
384        id: alloc::string::String,
385    },
386    Detached {
387        id: alloc::string::String,
388    },
389    Renamed {
390        id: alloc::string::String,
391        new_name: alloc::string::String,
392    },
393    Error {
394        message: alloc::string::String,
395    },
396}
397
398#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
399#[archive(check_bytes)]
400pub struct SessionInfo {
401    pub id: alloc::string::String,
402    pub name: alloc::string::String,
403    pub created_at: u64,
404    pub last_attached: u64,
405    pub attached_clients: u32,
406}
407
408// Tab information
409#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
410#[archive(check_bytes)]
411pub struct TabInfo {
412    pub id: u64,
413    pub title: alloc::string::String,
414    pub session_id: Option<alloc::string::String>,
415    pub is_active: bool,
416    pub pane_count: u32,
417}
418
419// Pane layout information
420#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
421#[archive(check_bytes)]
422pub struct PaneInfo {
423    pub id: u64,
424    pub x: u16,
425    pub y: u16,
426    pub width: u16,
427    pub height: u16,
428    pub is_focused: bool,
429}
430
431// Plugin information for inspector and dock display
432#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
433#[archive(check_bytes)]
434pub struct PluginInspectorInfo {
435    pub name: alloc::string::String,
436    pub version: alloc::string::String,
437    pub description: alloc::string::String,
438    pub author: alloc::string::String,
439    pub homepage: Option<alloc::string::String>,
440    pub api_version: alloc::string::String,
441    pub min_scarab_version: alloc::string::String,
442    pub enabled: bool,
443    pub failure_count: u32,
444    /// Plugin emoji for visual display (e.g., "🦠")
445    pub emoji: Option<alloc::string::String>,
446    /// Plugin color as hex code (e.g., "#FF5733")
447    pub color: Option<alloc::string::String>,
448    /// Verification status
449    pub verification: PluginVerificationStatus,
450}
451
452/// Verification status for plugins (zero-copy compatible)
453#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
454#[archive(check_bytes)]
455pub enum PluginVerificationStatus {
456    /// Plugin was verified with valid GPG signature
457    Verified {
458        key_fingerprint: alloc::string::String,
459        signature_timestamp: u64,
460    },
461    /// Plugin checksum was verified but no signature
462    ChecksumOnly { checksum: alloc::string::String },
463    /// Plugin was installed without verification
464    Unverified { warning: alloc::string::String },
465}
466
467// Status bar side specification
468#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
469#[archive(check_bytes)]
470pub enum StatusBarSide {
471    Left,
472    Right,
473}
474
475// Render item for status bar content
476// This is a simplified version for IPC - full version is in scarab-plugin-api
477#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
478#[archive(check_bytes)]
479pub enum StatusRenderItem {
480    Text(alloc::string::String),
481    Icon(alloc::string::String),
482    Foreground { r: u8, g: u8, b: u8 },
483    Background { r: u8, g: u8, b: u8 },
484    Bold,
485    Italic,
486    ResetAttributes,
487    Spacer,
488    Padding(u8),
489    Separator(alloc::string::String),
490}
491
492// Messages sent from Daemon to Client (Remote UI & Responses)
493#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
494#[archive(check_bytes)]
495pub enum DaemonMessage {
496    // Wrap existing session responses
497    Session(SessionResponse),
498
499    // Tab state updates
500    TabCreated {
501        tab: TabInfo,
502    },
503    TabClosed {
504        tab_id: u64,
505    },
506    TabSwitched {
507        tab_id: u64,
508    },
509    TabListResponse {
510        tabs: alloc::vec::Vec<TabInfo>,
511    },
512
513    // Pane state updates
514    PaneCreated {
515        pane: PaneInfo,
516    },
517    PaneClosed {
518        pane_id: u64,
519    },
520    PaneFocused {
521        pane_id: u64,
522    },
523    PaneLayoutUpdate {
524        panes: alloc::vec::Vec<PaneInfo>,
525    },
526
527    // Status bar updates
528    StatusBarUpdate {
529        window_id: u64,
530        side: StatusBarSide,
531        items: alloc::vec::Vec<StatusRenderItem>,
532    },
533
534    // Remote UI Commands
535    DrawOverlay {
536        id: u64, // UUID-like identifier
537        x: u16,
538        y: u16,
539        text: alloc::string::String,
540        style: OverlayStyle,
541    },
542    ClearOverlays {
543        id: Option<u64>, // None = Clear All
544    },
545    ShowModal {
546        title: alloc::string::String,
547        items: alloc::vec::Vec<ModalItem>,
548    },
549    HideModal,
550
551    // Plugin inspection responses
552    PluginList {
553        plugins: alloc::vec::Vec<PluginInspectorInfo>,
554    },
555    PluginStatusChanged {
556        name: alloc::string::String,
557        enabled: bool,
558    },
559    PluginError {
560        name: alloc::string::String,
561        error: alloc::string::String,
562    },
563
564    // Plugin logging and notifications
565    PluginLog {
566        plugin_name: alloc::string::String,
567        level: LogLevel,
568        message: alloc::string::String,
569    },
570    PluginNotification {
571        title: alloc::string::String,
572        body: alloc::string::String,
573        level: NotifyLevel,
574    },
575
576    // Plugin menu response
577    PluginMenuResponse {
578        plugin_name: alloc::string::String,
579        menu_json: alloc::string::String, // Serialized Vec<MenuItem>
580    },
581    PluginMenuError {
582        plugin_name: alloc::string::String,
583        error: alloc::string::String,
584    },
585
586    // Theme system updates
587    ThemeUpdate {
588        theme_json: alloc::string::String, // Serialized Theme
589    },
590
591    // Shell prompt markers update (OSC 133 shell integration)
592    PromptMarkersUpdate {
593        /// List of current prompt markers from daemon
594        markers: alloc::vec::Vec<PromptMarkerInfo>,
595    },
596
597    // Semantic zones update (deep shell integration)
598    SemanticZonesUpdate {
599        /// List of current semantic zones (prompt, input, output regions)
600        zones: alloc::vec::Vec<SemanticZone>,
601    },
602
603    // Command blocks update (grouped command sequences)
604    CommandBlocksUpdate {
605        /// List of completed command blocks
606        blocks: alloc::vec::Vec<CommandBlock>,
607    },
608
609    /// Response to ExtractZoneText with the zone's text content
610    ZoneTextExtracted {
611        zone_id: u64,
612        text: alloc::string::String,
613    },
614
615    // Event forwarding to clients
616    Event(EventMessage),
617
618    // Navigation API responses
619    NavFocusableRegistered {
620        plugin_name: alloc::string::String,
621        focusable_id: u64,
622    },
623    NavFocusableUnregistered {
624        plugin_name: alloc::string::String,
625        focusable_id: u64,
626    },
627    NavModeEntered {
628        plugin_name: alloc::string::String,
629    },
630    NavModeExited {
631        plugin_name: alloc::string::String,
632    },
633    /// Forward focusable registration from daemon to client
634    NavRegisterFocusable {
635        plugin_name: alloc::string::String,
636        x: u16,
637        y: u16,
638        width: u16,
639        height: u16,
640        label: alloc::string::String,
641        action: NavFocusableAction,
642    },
643    /// Forward focusable unregistration from daemon to client
644    NavUnregisterFocusable {
645        plugin_name: alloc::string::String,
646        focusable_id: u64,
647    },
648    /// Spawn an overlay at a given position
649    SpawnOverlay {
650        plugin_name: alloc::string::String,
651        overlay_id: u64,
652        x: u16,
653        y: u16,
654        content: alloc::string::String,
655        style: OverlayStyle,
656    },
657    /// Remove a previously spawned overlay
658    RemoveOverlay {
659        plugin_name: alloc::string::String,
660        overlay_id: u64,
661    },
662    /// Add a status bar item
663    AddStatusItem {
664        plugin_name: alloc::string::String,
665        item_id: u64,
666        label: alloc::string::String,
667        content: alloc::string::String,
668        priority: i32,
669    },
670    /// Remove a status bar item
671    RemoveStatusItem {
672        plugin_name: alloc::string::String,
673        item_id: u64,
674    },
675    /// Trigger prompt jump navigation
676    PromptJump {
677        plugin_name: alloc::string::String,
678        direction: PromptJumpDirection,
679    },
680    /// Apply a theme by name
681    ThemeApply {
682        theme_name: alloc::string::String,
683    },
684    /// Set a specific palette color
685    PaletteColorSet {
686        color_name: alloc::string::String,
687        value: alloc::string::String,
688    },
689    /// Response with current theme info
690    ThemeInfoResponse {
691        plugin_name: alloc::string::String,
692        theme_name: alloc::string::String,
693    },
694}
695
696/// Direction for prompt jump navigation
697#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
698#[archive(check_bytes)]
699pub enum PromptJumpDirection {
700    Up,
701    Down,
702    First,
703    Last,
704}
705
706/// Event message for IPC forwarding
707#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
708#[archive(check_bytes)]
709pub struct EventMessage {
710    /// Event type name
711    pub event_type: alloc::string::String,
712    /// Window ID if applicable
713    pub window_id: Option<u64>,
714    /// Pane ID if applicable
715    pub pane_id: Option<u64>,
716    /// Tab ID if applicable
717    pub tab_id: Option<u64>,
718    /// Serialized event data
719    pub data: alloc::vec::Vec<u8>,
720    /// Timestamp in microseconds since UNIX epoch
721    pub timestamp_micros: u64,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
725#[archive(check_bytes)]
726pub struct OverlayStyle {
727    pub fg: u32, // RGBA
728    pub bg: u32, // RGBA
729    pub z_index: f32,
730}
731
732impl Default for OverlayStyle {
733    fn default() -> Self {
734        Self {
735            fg: 0xFFFFFFFF, // White
736            bg: 0xFF0000FF, // Red background for high visibility by default
737            z_index: 100.0,
738        }
739    }
740}
741
742#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
743#[archive(check_bytes)]
744pub struct ModalItem {
745    pub id: alloc::string::String,
746    pub label: alloc::string::String,
747    pub description: Option<alloc::string::String>,
748}
749
750// IPC configuration constants
751pub const SOCKET_PATH: &str = "/tmp/scarab-daemon.sock";
752pub const MAX_MESSAGE_SIZE: usize = 8192;
753pub const MAX_CLIENTS: usize = 16;
754pub const RECONNECT_DELAY_MS: u64 = 100;
755pub const MAX_RECONNECT_ATTEMPTS: u32 = 10;
756
757/// Terminal display metrics shared between rendering and input systems
758///
759/// This provides the coordinate conversion information needed by:
760/// - Mouse input handlers (screen to grid coordinate conversion)
761/// - Text rendering systems (grid to screen coordinate conversion)
762/// - Selection and UI overlays (coordinate mapping)
763///
764/// This type can be used as a Bevy Resource when the bevy feature is enabled.
765#[derive(Debug, Clone, Copy)]
766#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Resource))]
767pub struct TerminalMetrics {
768    /// Width of a single character cell in pixels
769    pub cell_width: f32,
770    /// Height of a single character cell in pixels
771    pub cell_height: f32,
772    /// Number of columns in the terminal grid
773    pub columns: u16,
774    /// Number of rows in the terminal grid
775    pub rows: u16,
776}
777
778impl Default for TerminalMetrics {
779    fn default() -> Self {
780        Self {
781            cell_width: 9.0,   // Typical monospace width at 15px font
782            cell_height: 18.0, // Typical line height
783            columns: 80,
784            rows: 24,
785        }
786    }
787}
788
789impl TerminalMetrics {
790    /// Create metrics from font size and terminal dimensions
791    pub fn new(font_size: f32, line_height_multiplier: f32, columns: u16, rows: u16) -> Self {
792        Self {
793            cell_width: font_size * 0.6, // Typical monospace ratio
794            cell_height: font_size * line_height_multiplier,
795            columns,
796            rows,
797        }
798    }
799
800    /// Convert screen coordinates to grid coordinates
801    ///
802    /// # Arguments
803    /// * `screen_x` - X coordinate in pixels (from left edge)
804    /// * `screen_y` - Y coordinate in pixels (from top edge, Y-down)
805    ///
806    /// # Returns
807    /// Grid position clamped to valid bounds (col, row)
808    pub fn screen_to_grid(&self, screen_x: f32, screen_y: f32) -> (u16, u16) {
809        let col = (screen_x / self.cell_width).floor() as i32;
810        let row = (screen_y / self.cell_height).floor() as i32;
811
812        // Clamp to valid grid bounds
813        let col = col.max(0).min((self.columns - 1) as i32) as u16;
814        let row = row.max(0).min((self.rows - 1) as i32) as u16;
815
816        (col, row)
817    }
818
819    /// Convert grid coordinates to screen coordinates (top-left of cell)
820    ///
821    /// # Returns
822    /// Screen position in pixels (x, y)
823    pub fn grid_to_screen(&self, col: u16, row: u16) -> (f32, f32) {
824        let x = col as f32 * self.cell_width;
825        let y = row as f32 * self.cell_height;
826        (x, y)
827    }
828
829    /// Get total terminal size in pixels
830    pub fn screen_size(&self) -> (f32, f32) {
831        (
832            self.columns as f32 * self.cell_width,
833            self.rows as f32 * self.cell_height,
834        )
835    }
836}
837
838/// Image format specification for image protocol support
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
840#[repr(u8)]
841pub enum ImageFormat {
842    /// PNG image format
843    Png = 0,
844    /// JPEG image format
845    Jpeg = 1,
846    /// GIF image format
847    Gif = 2,
848    /// Raw RGBA pixel data
849    Rgba = 3,
850}
851
852/// Represents an image placement in the terminal grid
853///
854/// Images are transferred via shared memory to avoid protocol overhead.
855/// This struct contains the metadata and reference to the image data.
856#[derive(Debug, Clone)]
857pub struct ImagePlacement {
858    /// Unique identifier for this placement
859    pub id: u64,
860    /// Column position in terminal grid
861    pub x: u16,
862    /// Row position in terminal grid
863    pub y: u16,
864    /// Width in terminal cells
865    pub width_cells: u16,
866    /// Height in terminal cells
867    pub height_cells: u16,
868    /// Offset into shared memory image buffer
869    pub shm_offset: usize,
870    /// Size of image data in shared memory
871    pub shm_size: usize,
872    /// Image format
873    pub format: ImageFormat,
874}
875
876/// Shell prompt marker for IPC (OSC 133 shell integration)
877///
878/// This is a simplified, serializable version of the daemon's internal
879/// PromptMarker type. Used to communicate shell integration markers
880/// from daemon to client for features like:
881/// - Semantic prompt navigation (jump to previous/next prompt)
882/// - Command output extraction
883/// - Command duration tracking
884#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
885#[archive(check_bytes)]
886pub struct PromptMarkerInfo {
887    /// Marker type encoded as u8:
888    /// - 0 = PromptStart (OSC 133;A)
889    /// - 1 = CommandStart (OSC 133;B)
890    /// - 2 = CommandExecuted (OSC 133;C)
891    /// - 3 = CommandFinished (OSC 133;D)
892    pub marker_type: u8,
893    /// Absolute line number in scrollback
894    pub line: u32,
895    /// Exit code (only valid for CommandFinished markers)
896    pub exit_code: Option<i32>,
897    /// Timestamp in microseconds since UNIX epoch
898    pub timestamp_micros: u64,
899}
900
901impl PromptMarkerInfo {
902    /// Create a PromptStart marker
903    pub fn prompt_start(line: u32, timestamp_micros: u64) -> Self {
904        Self {
905            marker_type: 0,
906            line,
907            exit_code: None,
908            timestamp_micros,
909        }
910    }
911
912    /// Create a CommandStart marker
913    pub fn command_start(line: u32, timestamp_micros: u64) -> Self {
914        Self {
915            marker_type: 1,
916            line,
917            exit_code: None,
918            timestamp_micros,
919        }
920    }
921
922    /// Create a CommandExecuted marker
923    pub fn command_executed(line: u32, timestamp_micros: u64) -> Self {
924        Self {
925            marker_type: 2,
926            line,
927            exit_code: None,
928            timestamp_micros,
929        }
930    }
931
932    /// Create a CommandFinished marker with exit code
933    pub fn command_finished(line: u32, exit_code: i32, timestamp_micros: u64) -> Self {
934        Self {
935            marker_type: 3,
936            line,
937            exit_code: Some(exit_code),
938            timestamp_micros,
939        }
940    }
941
942    /// Check if this is a PromptStart marker
943    pub fn is_prompt_start(&self) -> bool {
944        self.marker_type == 0
945    }
946
947    /// Check if this is a CommandStart marker
948    pub fn is_command_start(&self) -> bool {
949        self.marker_type == 1
950    }
951
952    /// Check if this is a CommandExecuted marker
953    pub fn is_command_executed(&self) -> bool {
954        self.marker_type == 2
955    }
956
957    /// Check if this is a CommandFinished marker
958    pub fn is_command_finished(&self) -> bool {
959        self.marker_type == 3
960    }
961}
962
963extern crate alloc;