Skip to main content

kronos_tide/
pane.rs

1/// Pane — a PTY-backed terminal pane.
2///
3/// E0502 borrow fix (2026-04-16d):
4/// The original code called buf.write_char() directly in the PTY reader thread,
5/// bypassing the VTE parser.  Introducing VteParser::feed() caused a borrow
6/// conflict because feed() takes &mut self (parser) AND &mut buf, and both lived
7/// inside the thread closure which already held the Arc<Mutex<TerminalBuffer>>.
8///
9/// Fix: the VteParser is owned entirely by the PTY reader thread.  The main
10/// thread (user input path) does NOT need to share it — it just writes bytes
11/// directly to the PTY writer.  This keeps both borrows strictly separate and
12/// eliminates E0502 without requiring Cell::take/replace or double-locking.
13use crate::terminal::TerminalBuffer;
14use crate::vte::VteParser;
15use portable_pty::{native_pty_system, CommandBuilder, PtySize};
16use std::io::{Read, Write};
17use std::sync::{Arc, Mutex};
18use std::thread;
19
20pub struct Pane {
21    pub id: usize,
22    pub title: String,
23    pub buffer: Arc<Mutex<TerminalBuffer>>,
24    pub x: u16,
25    pub y: u16,
26    pub width: u16,
27    pub height: u16,
28    writer: Option<Box<dyn Write + Send>>,
29    active: bool,
30}
31
32impl Pane {
33    pub fn new(
34        id: usize,
35        shell: &str,
36        x: u16,
37        y: u16,
38        width: u16,
39        height: u16,
40        scrollback: usize,
41    ) -> Self {
42        let buffer = Arc::new(Mutex::new(TerminalBuffer::new(
43            height as usize,
44            width as usize,
45            scrollback,
46        )));
47
48        let pty_system = native_pty_system();
49        let pair = pty_system
50            .openpty(PtySize {
51                rows: height,
52                cols: width,
53                pixel_width: 0,
54                pixel_height: 0,
55            })
56            .expect("failed to open PTY");
57
58        let mut cmd = CommandBuilder::new(shell);
59        cmd.env("TERM", "xterm-256color");
60        cmd.env("KRONOS", "1");
61
62        let _child = pair
63            .slave
64            .spawn_command(cmd)
65            .expect("failed to spawn shell");
66        drop(pair.slave);
67
68        let mut reader = pair
69            .master
70            .try_clone_reader()
71            .expect("failed to clone PTY reader");
72        let writer = pair.master.take_writer().expect("failed to get PTY writer");
73
74        let buf_clone = Arc::clone(&buffer);
75
76        // The VteParser lives exclusively in the reader thread.
77        // It owns its own mutable state; the buffer is locked only while
78        // processing a chunk, then released before the next read().
79        // This resolves E0502: no shared mutable reference to VteParser exists
80        // outside this thread.
81        thread::spawn(move || {
82            let mut parser = VteParser::new();
83            let mut byte_buf = [0u8; 4096];
84            loop {
85                match reader.read(&mut byte_buf) {
86                    Ok(0) => break,
87                    Ok(n) => {
88                        // Phase 1: copy bytes out — no locks held.
89                        let chunk = byte_buf[..n].to_vec();
90
91                        // Phase 2: lock buffer and feed the VTE parser.
92                        // parser and buf are both exclusively owned/locked here —
93                        // no aliasing, no E0502.
94                        if let Ok(mut buf) = buf_clone.lock() {
95                            parser.feed_str(&mut buf, &chunk);
96                        }
97                    }
98                    Err(_) => break,
99                }
100            }
101        });
102
103        Self {
104            id,
105            title: format!("pane-{}", id),
106            buffer,
107            x,
108            y,
109            width,
110            height,
111            writer: Some(Box::new(writer)),
112            active: true,
113        }
114    }
115
116    /// Write raw bytes to the PTY stdin (user keystrokes, paste, etc.).
117    pub fn send_input(&mut self, data: &[u8]) {
118        if let Some(ref mut writer) = self.writer {
119            let _ = writer.write_all(data);
120            let _ = writer.flush();
121        }
122    }
123
124    pub fn resize(&mut self, width: u16, height: u16) {
125        self.width = width;
126        self.height = height;
127        if let Ok(mut buf) = self.buffer.lock() {
128            buf.resize(height as usize, width as usize);
129        }
130    }
131
132    pub fn is_active(&self) -> bool {
133        self.active
134    }
135}