tenseleyflow/fackr / 4d05e34

Browse files

feat: add multi-session support to terminal panel

Refactor TerminalPanel to support multiple terminal sessions:
- Add TerminalSession struct holding PTY + screen buffer per session
- Change from single PTY to Vec<TerminalSession> with active index
- Add session management methods: new_session(), close_active_session(),
switch_session(), next_session(), prev_session()
- Poll all sessions to keep background shells responsive
- Auto-remove dead sessions and hide terminal when last closes
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4d05e3457a9ae3435bd4f67643dd9bf16b324522
Parents
04a8281
Tree
7371944

1 changed file

StatusFile+-
M src/terminal/panel.rs 206 59
src/terminal/panel.rsmodified
@@ -1,6 +1,6 @@
1
 //! Terminal panel
1
 //! Terminal panel
2
 //!
2
 //!
3
-//! The main interface for the integrated terminal.
3
+//! The main interface for the integrated terminal with multi-session support.
4
 
4
 
5
 use anyhow::Result;
5
 use anyhow::Result;
6
 
6
 
@@ -14,12 +14,90 @@ const MAX_HEIGHT_PERCENT: u16 = 80;
14
 /// Minimum terminal height in rows
14
 /// Minimum terminal height in rows
15
 const MIN_HEIGHT_ROWS: u16 = 3;
15
 const MIN_HEIGHT_ROWS: u16 = 3;
16
 
16
 
17
-/// Integrated terminal panel
17
+/// A single terminal session (PTY + screen buffer)
18
-pub struct TerminalPanel {
18
+pub struct TerminalSession {
19
     /// PTY connection to shell
19
     /// PTY connection to shell
20
     pty: Option<Pty>,
20
     pty: Option<Pty>,
21
     /// Terminal screen buffer
21
     /// Terminal screen buffer
22
     screen: TerminalScreen,
22
     screen: TerminalScreen,
23
+}
24
+
25
+impl TerminalSession {
26
+    /// Create a new terminal session
27
+    fn new(width: u16, height: u16) -> Self {
28
+        Self {
29
+            pty: None,
30
+            screen: TerminalScreen::new(width, height),
31
+        }
32
+    }
33
+
34
+    /// Spawn the PTY for this session
35
+    fn spawn(&mut self, width: u16, height: u16) -> Result<()> {
36
+        let pty = Pty::spawn(width, height)?;
37
+        self.pty = Some(pty);
38
+        Ok(())
39
+    }
40
+
41
+    /// Check if the session's shell is still alive
42
+    fn is_alive(&self) -> bool {
43
+        self.pty.as_ref().map(|p| p.is_alive()).unwrap_or(false)
44
+    }
45
+
46
+    /// Poll for output from this session
47
+    fn poll(&mut self) -> bool {
48
+        let mut had_data = false;
49
+
50
+        if let Some(ref mut pty) = self.pty {
51
+            if let Some(data) = pty.read() {
52
+                self.screen.process(&data);
53
+                had_data = true;
54
+            }
55
+        }
56
+
57
+        // Send any queued responses back to PTY
58
+        let responses = self.screen.drain_responses();
59
+        for response in responses {
60
+            if let Some(ref mut pty) = self.pty {
61
+                let _ = pty.write(&response);
62
+            }
63
+        }
64
+
65
+        had_data
66
+    }
67
+
68
+    /// Send input to this session
69
+    fn send_input(&mut self, data: &[u8]) -> Result<()> {
70
+        if let Some(ref mut pty) = self.pty {
71
+            pty.write(data)?;
72
+        }
73
+        Ok(())
74
+    }
75
+
76
+    /// Resize this session
77
+    fn resize(&mut self, width: u16, height: u16) {
78
+        self.screen.resize(width, height);
79
+        if let Some(ref pty) = self.pty {
80
+            let _ = pty.resize(width, height);
81
+        }
82
+    }
83
+
84
+    /// Get the current working directory (from OSC 7)
85
+    pub fn cwd(&self) -> Option<&str> {
86
+        self.screen.cwd.as_deref()
87
+    }
88
+
89
+    /// Get the screen buffer
90
+    pub fn screen(&self) -> &TerminalScreen {
91
+        &self.screen
92
+    }
93
+}
94
+
95
+/// Integrated terminal panel with multi-session support
96
+pub struct TerminalPanel {
97
+    /// All terminal sessions
98
+    sessions: Vec<TerminalSession>,
99
+    /// Active session index
100
+    active_session: usize,
23
     /// Whether the terminal is visible
101
     /// Whether the terminal is visible
24
     pub visible: bool,
102
     pub visible: bool,
25
     /// Terminal height in rows
103
     /// Terminal height in rows
@@ -34,11 +112,9 @@ impl TerminalPanel {
34
     /// Create a new terminal panel (not yet spawned)
112
     /// Create a new terminal panel (not yet spawned)
35
     pub fn new(screen_width: u16, screen_height: u16) -> Self {
113
     pub fn new(screen_width: u16, screen_height: u16) -> Self {
36
         let height = (screen_height * DEFAULT_HEIGHT_PERCENT / 100).max(MIN_HEIGHT_ROWS);
114
         let height = (screen_height * DEFAULT_HEIGHT_PERCENT / 100).max(MIN_HEIGHT_ROWS);
37
-        // Content area is height - 1 (title bar takes one row)
38
-        let content_height = height.saturating_sub(1).max(1);
39
         Self {
115
         Self {
40
-            pty: None,
116
+            sessions: Vec::new(),
41
-            screen: TerminalScreen::new(screen_width, content_height),
117
+            active_session: 0,
42
             visible: false,
118
             visible: false,
43
             height,
119
             height,
44
             screen_height,
120
             screen_height,
@@ -46,41 +122,112 @@ impl TerminalPanel {
46
         }
122
         }
47
     }
123
     }
48
 
124
 
125
+    /// Get the content height (excluding title bar)
126
+    fn content_height(&self) -> u16 {
127
+        self.height.saturating_sub(1).max(1)
128
+    }
129
+
49
     /// Toggle terminal visibility
130
     /// Toggle terminal visibility
50
     pub fn toggle(&mut self) -> Result<()> {
131
     pub fn toggle(&mut self) -> Result<()> {
51
         self.visible = !self.visible;
132
         self.visible = !self.visible;
52
 
133
 
53
-        // Spawn PTY on first show
134
+        // Spawn first session on first show
54
-        if self.visible && self.pty.is_none() {
135
+        if self.visible && self.sessions.is_empty() {
55
-            self.spawn()?;
136
+            self.new_session()?;
56
         }
137
         }
57
 
138
 
58
         Ok(())
139
         Ok(())
59
     }
140
     }
60
 
141
 
61
-    /// Spawn the PTY process
142
+    /// Create a new terminal session
62
-    fn spawn(&mut self) -> Result<()> {
143
+    pub fn new_session(&mut self) -> Result<()> {
63
-        // PTY gets content height (excluding title bar)
144
+        let content_height = self.content_height();
64
-        let content_height = self.height.saturating_sub(1).max(1);
145
+        let mut session = TerminalSession::new(self.screen_width, content_height);
65
-        let pty = Pty::spawn(self.screen_width, content_height)?;
146
+        session.spawn(self.screen_width, content_height)?;
66
-        self.pty = Some(pty);
147
+        self.sessions.push(session);
148
+        self.active_session = self.sessions.len() - 1;
67
         Ok(())
149
         Ok(())
68
     }
150
     }
69
 
151
 
152
+    /// Close the active session. Returns true if the terminal should be hidden.
153
+    pub fn close_active_session(&mut self) -> bool {
154
+        if self.sessions.is_empty() {
155
+            return true;
156
+        }
157
+
158
+        self.sessions.remove(self.active_session);
159
+
160
+        if self.sessions.is_empty() {
161
+            return true;
162
+        }
163
+
164
+        // Adjust active_session if needed
165
+        if self.active_session >= self.sessions.len() {
166
+            self.active_session = self.sessions.len() - 1;
167
+        }
168
+
169
+        false
170
+    }
171
+
172
+    /// Switch to a specific session by index
173
+    pub fn switch_session(&mut self, index: usize) {
174
+        if index < self.sessions.len() {
175
+            self.active_session = index;
176
+        }
177
+    }
178
+
179
+    /// Switch to the next session
180
+    pub fn next_session(&mut self) {
181
+        if !self.sessions.is_empty() {
182
+            self.active_session = (self.active_session + 1) % self.sessions.len();
183
+        }
184
+    }
185
+
186
+    /// Switch to the previous session
187
+    pub fn prev_session(&mut self) {
188
+        if !self.sessions.is_empty() {
189
+            self.active_session = if self.active_session == 0 {
190
+                self.sessions.len() - 1
191
+            } else {
192
+                self.active_session - 1
193
+            };
194
+        }
195
+    }
196
+
197
+    /// Get the number of sessions
198
+    pub fn session_count(&self) -> usize {
199
+        self.sessions.len()
200
+    }
201
+
202
+    /// Get the active session index
203
+    pub fn active_session_index(&self) -> usize {
204
+        self.active_session
205
+    }
206
+
207
+    /// Get a reference to all sessions (for rendering tabs)
208
+    pub fn sessions(&self) -> &[TerminalSession] {
209
+        &self.sessions
210
+    }
211
+
212
+    /// Get the CWD of the active session
213
+    pub fn active_cwd(&self) -> Option<&str> {
214
+        self.sessions.get(self.active_session).and_then(|s| s.cwd())
215
+    }
216
+
70
     /// Hide the terminal (ESC pressed)
217
     /// Hide the terminal (ESC pressed)
71
     pub fn hide(&mut self) {
218
     pub fn hide(&mut self) {
72
         self.visible = false;
219
         self.visible = false;
73
     }
220
     }
74
 
221
 
75
-    /// Send input to the terminal
222
+    /// Send input to the active terminal
76
     pub fn send_input(&mut self, data: &[u8]) -> Result<()> {
223
     pub fn send_input(&mut self, data: &[u8]) -> Result<()> {
77
-        if let Some(ref mut pty) = self.pty {
224
+        if let Some(session) = self.sessions.get_mut(self.active_session) {
78
-            pty.write(data)?;
225
+            session.send_input(data)?;
79
         }
226
         }
80
         Ok(())
227
         Ok(())
81
     }
228
     }
82
 
229
 
83
-    /// Send a key to the terminal
230
+    /// Send a key to the active terminal
84
     pub fn send_key(&mut self, key: &crossterm::event::KeyEvent) -> Result<()> {
231
     pub fn send_key(&mut self, key: &crossterm::event::KeyEvent) -> Result<()> {
85
         use crossterm::event::{KeyCode, KeyModifiers};
232
         use crossterm::event::{KeyCode, KeyModifiers};
86
 
233
 
@@ -137,47 +284,53 @@ impl TerminalPanel {
137
         Ok(())
284
         Ok(())
138
     }
285
     }
139
 
286
 
140
-    /// Poll for and process PTY output. Returns true if data was received or terminal closed.
287
+    /// Poll for and process PTY output. Returns true if data was received or terminal state changed.
141
     pub fn poll(&mut self) -> bool {
288
     pub fn poll(&mut self) -> bool {
142
-        let mut had_data = false;
289
+        let mut had_activity = false;
143
 
290
 
144
-        if let Some(ref mut pty) = self.pty {
291
+        // Poll all sessions (to keep them responsive)
145
-            // Check if shell has exited
292
+        for session in &mut self.sessions {
146
-            if !pty.is_alive() {
293
+            if session.poll() {
147
-                // Shell exited - close terminal and clean up
294
+                had_activity = true;
148
-                self.visible = false;
149
-                self.pty = None;
150
-                return true; // Trigger render to hide terminal
151
             }
295
             }
296
+        }
152
 
297
 
153
-            if let Some(data) = pty.read() {
298
+        // Remove dead sessions
154
-                self.screen.process(&data);
299
+        let active_before = self.active_session;
155
-                had_data = true;
300
+        self.sessions.retain(|s| s.is_alive());
156
-            }
301
+
302
+        if self.sessions.is_empty() {
303
+            self.visible = false;
304
+            return true;
157
         }
305
         }
158
 
306
 
159
-        // Send any queued responses (e.g., device status reports) back to PTY
307
+        // Adjust active_session if sessions were removed
160
-        let responses = self.screen.drain_responses();
308
+        if self.active_session >= self.sessions.len() {
161
-        for response in responses {
309
+            self.active_session = self.sessions.len() - 1;
162
-            let _ = self.send_input(&response);
310
+            had_activity = true;
311
+        } else if active_before != self.active_session {
312
+            had_activity = true;
163
         }
313
         }
164
 
314
 
165
-        had_data
315
+        had_activity
166
     }
316
     }
167
 
317
 
168
-    /// Get the terminal screen for rendering
318
+    /// Get the active terminal screen for rendering
169
-    pub fn screen(&self) -> &TerminalScreen {
319
+    pub fn screen(&self) -> Option<&TerminalScreen> {
170
-        &self.screen
320
+        self.sessions.get(self.active_session).map(|s| s.screen())
171
     }
321
     }
172
 
322
 
173
-    /// Get a cell from the terminal screen
323
+    /// Get a cell from the active terminal screen
174
     pub fn get_cell(&self, row: usize, col: usize) -> Option<&Cell> {
324
     pub fn get_cell(&self, row: usize, col: usize) -> Option<&Cell> {
175
-        self.screen.cells().get(row).and_then(|r| r.get(col))
325
+        self.screen()?.cells().get(row).and_then(|r| r.get(col))
176
     }
326
     }
177
 
327
 
178
-    /// Get cursor position
328
+    /// Get cursor position from the active session
179
     pub fn cursor_pos(&self) -> (u16, u16) {
329
     pub fn cursor_pos(&self) -> (u16, u16) {
180
-        (self.screen.cursor_row, self.screen.cursor_col)
330
+        self.sessions
331
+            .get(self.active_session)
332
+            .map(|s| (s.screen.cursor_row, s.screen.cursor_col))
333
+            .unwrap_or((0, 0))
181
     }
334
     }
182
 
335
 
183
     /// Update screen dimensions
336
     /// Update screen dimensions
@@ -189,15 +342,11 @@ impl TerminalPanel {
189
         let max_height = height * MAX_HEIGHT_PERCENT / 100;
342
         let max_height = height * MAX_HEIGHT_PERCENT / 100;
190
         self.height = self.height.min(max_height).max(MIN_HEIGHT_ROWS);
343
         self.height = self.height.min(max_height).max(MIN_HEIGHT_ROWS);
191
 
344
 
192
-        // Content height excludes title bar
345
+        let content_height = self.content_height();
193
-        let content_height = self.height.saturating_sub(1).max(1);
194
-
195
-        // Resize terminal screen
196
-        self.screen.resize(width, content_height);
197
 
346
 
198
-        // Resize PTY
347
+        // Resize all sessions
199
-        if let Some(ref pty) = self.pty {
348
+        for session in &mut self.sessions {
200
-            let _ = pty.resize(width, content_height);
349
+            session.resize(width, content_height);
201
         }
350
         }
202
     }
351
     }
203
 
352
 
@@ -206,13 +355,11 @@ impl TerminalPanel {
206
         let max_height = self.screen_height * MAX_HEIGHT_PERCENT / 100;
355
         let max_height = self.screen_height * MAX_HEIGHT_PERCENT / 100;
207
         self.height = new_height.min(max_height).max(MIN_HEIGHT_ROWS);
356
         self.height = new_height.min(max_height).max(MIN_HEIGHT_ROWS);
208
 
357
 
209
-        // Content height excludes title bar
358
+        let content_height = self.content_height();
210
-        let content_height = self.height.saturating_sub(1).max(1);
211
-
212
-        self.screen.resize(self.screen_width, content_height);
213
 
359
 
214
-        if let Some(ref pty) = self.pty {
360
+        // Resize all sessions
215
-            let _ = pty.resize(self.screen_width, content_height);
361
+        for session in &mut self.sessions {
362
+            session.resize(self.screen_width, content_height);
216
         }
363
         }
217
     }
364
     }
218
 
365