tenseleyflow/fackr / 5f8e29e

Browse files

feat: complete VTE terminal emulation

- Fix flashing: poll() now returns bool, only render when data received
- Add DEC private modes: ?1/?7/?25/?1049/?2004 (cursor keys, autowrap,
cursor visibility, alternate screen, bracketed paste)
- Implement alternate screen buffer for vim/less compatibility
- Add missing CSI commands: E/F/G/L/M/P/S/T/X/@/d/n/r/s/u
- Complete J=1 and K=1 (clear from start to cursor)
- Add scroll region support (DECSTBM)
- Handle escape sequences: ESC 7/8/M/D/E/c (cursor save/restore,
index, reverse index, next line, reset)
- Add device status report responses (CSI 5n, CSI 6n)

This should fix fish shell's two-line prompt and vim rendering.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5f8e29e778200533b50271e66f223d9ddfc6a6d3
Parents
32e4ed7
Tree
dfca9d9

3 changed files

StatusFile+-
M src/editor/state.rs 3 4
M src/terminal/panel.rs 13 2
M src/terminal/screen.rs 390 4
src/editor/state.rsmodified
@@ -763,10 +763,9 @@ impl Editor {
763763
                 }
764764
             }
765765
 
766
-            // Poll terminal for output
767
-            if self.terminal.visible {
768
-                self.terminal.poll();
769
-                needs_render = true; // Terminal might have new output
766
+            // Poll terminal for output (only render if data received)
767
+            if self.terminal.visible && self.terminal.poll() {
768
+                needs_render = true;
770769
             }
771770
 
772771
             // Process LSP messages from language servers
src/terminal/panel.rsmodified
@@ -133,13 +133,24 @@ impl TerminalPanel {
133133
         Ok(())
134134
     }
135135
 
136
-    /// Poll for and process PTY output
137
-    pub fn poll(&mut self) {
136
+    /// Poll for and process PTY output. Returns true if data was received.
137
+    pub fn poll(&mut self) -> bool {
138
+        let mut had_data = false;
139
+
138140
         if let Some(ref mut pty) = self.pty {
139141
             if let Some(data) = pty.read() {
140142
                 self.screen.process(&data);
143
+                had_data = true;
141144
             }
142145
         }
146
+
147
+        // Send any queued responses (e.g., device status reports) back to PTY
148
+        let responses = self.screen.drain_responses();
149
+        for response in responses {
150
+            let _ = self.send_input(&response);
151
+        }
152
+
153
+        had_data
143154
     }
144155
 
145156
     /// Get the terminal screen for rendering
src/terminal/screen.rsmodified
@@ -78,6 +78,24 @@ pub struct TerminalScreen {
7878
     max_scrollback: usize,
7979
     /// Scroll offset (0 = at bottom)
8080
     pub scroll_offset: usize,
81
+    /// DEC private modes
82
+    pub cursor_visible: bool,
83
+    autowrap: bool,
84
+    application_cursor_keys: bool,
85
+    bracketed_paste: bool,
86
+    /// Alternate screen buffer
87
+    alt_cells: Option<Vec<Vec<Cell>>>,
88
+    alt_cursor_row: u16,
89
+    alt_cursor_col: u16,
90
+    using_alt_screen: bool,
91
+    /// Saved cursor position (for ESC 7/8 and CSI s/u)
92
+    saved_cursor_row: u16,
93
+    saved_cursor_col: u16,
94
+    /// Scroll region (top, bottom) - 0-indexed, inclusive
95
+    scroll_top: u16,
96
+    scroll_bottom: u16,
97
+    /// Response queue for device status reports
98
+    response_queue: Vec<Vec<u8>>,
8199
 }
82100
 
83101
 impl TerminalScreen {
@@ -98,6 +116,24 @@ impl TerminalScreen {
98116
             scrollback: Vec::new(),
99117
             max_scrollback: 10000,
100118
             scroll_offset: 0,
119
+            // DEC private modes
120
+            cursor_visible: true,
121
+            autowrap: true,
122
+            application_cursor_keys: false,
123
+            bracketed_paste: false,
124
+            // Alternate screen buffer
125
+            alt_cells: None,
126
+            alt_cursor_row: 0,
127
+            alt_cursor_col: 0,
128
+            using_alt_screen: false,
129
+            // Saved cursor
130
+            saved_cursor_row: 0,
131
+            saved_cursor_col: 0,
132
+            // Scroll region (full screen by default)
133
+            scroll_top: 0,
134
+            scroll_bottom: rows.saturating_sub(1),
135
+            // Response queue
136
+            response_queue: Vec::new(),
101137
         }
102138
     }
103139
 
@@ -156,6 +192,216 @@ impl TerminalScreen {
156192
         // Ensure cursor is within bounds
157193
         self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
158194
         self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
195
+
196
+        // Update scroll region to new size
197
+        self.scroll_bottom = rows.saturating_sub(1);
198
+        if self.scroll_top > self.scroll_bottom {
199
+            self.scroll_top = 0;
200
+        }
201
+    }
202
+
203
+    /// Drain response queue (for device status reports)
204
+    pub fn drain_responses(&mut self) -> Vec<Vec<u8>> {
205
+        std::mem::take(&mut self.response_queue)
206
+    }
207
+
208
+    /// Enter alternate screen buffer
209
+    fn enter_alt_screen(&mut self) {
210
+        if !self.using_alt_screen {
211
+            // Save primary screen and cursor
212
+            self.alt_cells = Some(std::mem::take(&mut self.cells));
213
+            self.alt_cursor_row = self.cursor_row;
214
+            self.alt_cursor_col = self.cursor_col;
215
+            // Create fresh alt screen
216
+            self.cells = vec![vec![Cell::default(); self.cols as usize]; self.rows as usize];
217
+            self.cursor_row = 0;
218
+            self.cursor_col = 0;
219
+            self.using_alt_screen = true;
220
+        }
221
+    }
222
+
223
+    /// Leave alternate screen buffer
224
+    fn leave_alt_screen(&mut self) {
225
+        if self.using_alt_screen {
226
+            if let Some(primary) = self.alt_cells.take() {
227
+                self.cells = primary;
228
+                self.cursor_row = self.alt_cursor_row;
229
+                self.cursor_col = self.alt_cursor_col;
230
+            }
231
+            self.using_alt_screen = false;
232
+        }
233
+    }
234
+
235
+    /// Save cursor position
236
+    fn save_cursor(&mut self) {
237
+        self.saved_cursor_row = self.cursor_row;
238
+        self.saved_cursor_col = self.cursor_col;
239
+    }
240
+
241
+    /// Restore cursor position
242
+    fn restore_cursor(&mut self) {
243
+        self.cursor_row = self.saved_cursor_row.min(self.rows.saturating_sub(1));
244
+        self.cursor_col = self.saved_cursor_col.min(self.cols.saturating_sub(1));
245
+    }
246
+
247
+    /// Handle DEC private mode set/reset
248
+    fn handle_dec_private_mode(&mut self, params: &[u16], set: bool) {
249
+        for &param in params {
250
+            match param {
251
+                1 => self.application_cursor_keys = set,     // DECCKM
252
+                7 => self.autowrap = set,                     // DECAWM
253
+                25 => self.cursor_visible = set,              // DECTCEM
254
+                1049 => {
255
+                    // Alternate screen buffer
256
+                    if set {
257
+                        self.enter_alt_screen();
258
+                    } else {
259
+                        self.leave_alt_screen();
260
+                    }
261
+                }
262
+                2004 => self.bracketed_paste = set,           // Bracketed paste
263
+                _ => {} // Ignore unknown modes
264
+            }
265
+        }
266
+    }
267
+
268
+    /// Reverse index - move cursor up, scroll down if at top
269
+    fn reverse_index(&mut self) {
270
+        if self.cursor_row == self.scroll_top {
271
+            self.scroll_down_region(1);
272
+        } else {
273
+            self.cursor_row = self.cursor_row.saturating_sub(1);
274
+        }
275
+    }
276
+
277
+    /// Index - move cursor down, scroll up if at bottom
278
+    fn index(&mut self) {
279
+        if self.cursor_row == self.scroll_bottom {
280
+            self.scroll_up_region(1);
281
+        } else if self.cursor_row < self.rows - 1 {
282
+            self.cursor_row += 1;
283
+        }
284
+    }
285
+
286
+    /// Next line - move to start of next line, scroll if needed
287
+    fn next_line(&mut self) {
288
+        self.cursor_col = 0;
289
+        self.index();
290
+    }
291
+
292
+    /// Scroll up within scroll region
293
+    fn scroll_up_region(&mut self, n: u16) {
294
+        let top = self.scroll_top as usize;
295
+        let bottom = self.scroll_bottom as usize;
296
+
297
+        for _ in 0..n {
298
+            if top < self.cells.len() && bottom < self.cells.len() && top <= bottom {
299
+                // Move top row to scrollback (only if scroll region is full screen)
300
+                if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 {
301
+                    let top_row = self.cells.remove(top);
302
+                    self.scrollback.push(top_row);
303
+                    if self.scrollback.len() > self.max_scrollback {
304
+                        self.scrollback.remove(0);
305
+                    }
306
+                } else {
307
+                    self.cells.remove(top);
308
+                }
309
+                // Insert new row at bottom of scroll region
310
+                self.cells.insert(bottom, vec![Cell::default(); self.cols as usize]);
311
+            }
312
+        }
313
+    }
314
+
315
+    /// Scroll down within scroll region
316
+    fn scroll_down_region(&mut self, n: u16) {
317
+        let top = self.scroll_top as usize;
318
+        let bottom = self.scroll_bottom as usize;
319
+
320
+        for _ in 0..n {
321
+            if top < self.cells.len() && bottom < self.cells.len() && top <= bottom {
322
+                // Remove bottom row
323
+                self.cells.remove(bottom);
324
+                // Insert new row at top of scroll region
325
+                self.cells.insert(top, vec![Cell::default(); self.cols as usize]);
326
+            }
327
+        }
328
+    }
329
+
330
+    /// Insert n lines at cursor position
331
+    fn insert_lines(&mut self, n: u16) {
332
+        let row = self.cursor_row as usize;
333
+        let bottom = self.scroll_bottom as usize;
334
+
335
+        for _ in 0..n {
336
+            if row <= bottom && bottom < self.cells.len() {
337
+                self.cells.remove(bottom);
338
+                self.cells.insert(row, vec![Cell::default(); self.cols as usize]);
339
+            }
340
+        }
341
+    }
342
+
343
+    /// Delete n lines at cursor position
344
+    fn delete_lines(&mut self, n: u16) {
345
+        let row = self.cursor_row as usize;
346
+        let bottom = self.scroll_bottom as usize;
347
+
348
+        for _ in 0..n {
349
+            if row <= bottom && row < self.cells.len() {
350
+                self.cells.remove(row);
351
+                self.cells.insert(bottom, vec![Cell::default(); self.cols as usize]);
352
+            }
353
+        }
354
+    }
355
+
356
+    /// Insert n blank characters at cursor position
357
+    fn insert_chars(&mut self, n: u16) {
358
+        if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
359
+            let col = self.cursor_col as usize;
360
+            for _ in 0..n {
361
+                if col < row.len() {
362
+                    row.pop(); // Remove from end
363
+                    row.insert(col, Cell::default()); // Insert at cursor
364
+                }
365
+            }
366
+        }
367
+    }
368
+
369
+    /// Delete n characters at cursor position
370
+    fn delete_chars(&mut self, n: u16) {
371
+        if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
372
+            let col = self.cursor_col as usize;
373
+            for _ in 0..n {
374
+                if col < row.len() {
375
+                    row.remove(col);
376
+                    row.push(Cell::default()); // Add blank at end
377
+                }
378
+            }
379
+        }
380
+    }
381
+
382
+    /// Clear from start of screen to cursor
383
+    fn clear_from_start(&mut self) {
384
+        // Clear all rows before cursor row
385
+        for row in self.cells.iter_mut().take(self.cursor_row as usize) {
386
+            for cell in row.iter_mut() {
387
+                *cell = Cell::default();
388
+            }
389
+        }
390
+        // Clear current row from start to cursor
391
+        if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
392
+            for cell in row.iter_mut().take(self.cursor_col as usize + 1) {
393
+                *cell = Cell::default();
394
+            }
395
+        }
396
+    }
397
+
398
+    /// Clear from start of line to cursor
399
+    fn clear_line_from_start(&mut self) {
400
+        if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
401
+            for cell in row.iter_mut().take(self.cursor_col as usize + 1) {
402
+                *cell = Cell::default();
403
+            }
404
+        }
159405
     }
160406
 
161407
     /// Scroll the screen up by one line
@@ -274,9 +520,21 @@ impl Perform for TerminalScreen {
274520
 
275521
     fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
276522
 
277
-    fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
523
+    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {
278524
         let params: Vec<u16> = params.iter().map(|p| p.first().copied().unwrap_or(0) as u16).collect();
279525
 
526
+        // Check for DEC private mode sequences (CSI ? ...)
527
+        let is_private = intermediates.contains(&b'?');
528
+
529
+        if is_private {
530
+            match action {
531
+                'h' => self.handle_dec_private_mode(&params, true),  // Set mode
532
+                'l' => self.handle_dec_private_mode(&params, false), // Reset mode
533
+                _ => {}
534
+            }
535
+            return;
536
+        }
537
+
280538
         match action {
281539
             // Cursor Up
282540
             'A' => {
@@ -298,6 +556,23 @@ impl Perform for TerminalScreen {
298556
                 let n = params.first().copied().unwrap_or(1).max(1);
299557
                 self.cursor_col = self.cursor_col.saturating_sub(n);
300558
             }
559
+            // Cursor Next Line
560
+            'E' => {
561
+                let n = params.first().copied().unwrap_or(1).max(1);
562
+                self.cursor_col = 0;
563
+                self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
564
+            }
565
+            // Cursor Previous Line
566
+            'F' => {
567
+                let n = params.first().copied().unwrap_or(1).max(1);
568
+                self.cursor_col = 0;
569
+                self.cursor_row = self.cursor_row.saturating_sub(n);
570
+            }
571
+            // Cursor Horizontal Absolute
572
+            'G' => {
573
+                let col = params.first().copied().unwrap_or(1).max(1) - 1;
574
+                self.cursor_col = col.min(self.cols - 1);
575
+            }
301576
             // Cursor Position (CUP)
302577
             'H' | 'f' => {
303578
                 let row = params.first().copied().unwrap_or(1).max(1) - 1;
@@ -310,7 +585,7 @@ impl Perform for TerminalScreen {
310585
                 let mode = params.first().copied().unwrap_or(0);
311586
                 match mode {
312587
                     0 => self.clear_to_eos(),
313
-                    1 => {} // Clear from start to cursor (TODO)
588
+                    1 => self.clear_from_start(),
314589
                     2 | 3 => self.clear_screen(),
315590
                     _ => {}
316591
                 }
@@ -320,7 +595,7 @@ impl Perform for TerminalScreen {
320595
                 let mode = params.first().copied().unwrap_or(0);
321596
                 match mode {
322597
                     0 => self.clear_to_eol(),
323
-                    1 => {} // Clear from start of line to cursor (TODO)
598
+                    1 => self.clear_line_from_start(),
324599
                     2 => {
325600
                         // Clear entire line
326601
                         if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
@@ -332,6 +607,89 @@ impl Perform for TerminalScreen {
332607
                     _ => {}
333608
                 }
334609
             }
610
+            // Insert Lines
611
+            'L' => {
612
+                let n = params.first().copied().unwrap_or(1).max(1);
613
+                self.insert_lines(n);
614
+            }
615
+            // Delete Lines
616
+            'M' => {
617
+                let n = params.first().copied().unwrap_or(1).max(1);
618
+                self.delete_lines(n);
619
+            }
620
+            // Delete Characters
621
+            'P' => {
622
+                let n = params.first().copied().unwrap_or(1).max(1);
623
+                self.delete_chars(n);
624
+            }
625
+            // Scroll Up
626
+            'S' => {
627
+                let n = params.first().copied().unwrap_or(1).max(1);
628
+                self.scroll_up_region(n);
629
+            }
630
+            // Scroll Down
631
+            'T' => {
632
+                let n = params.first().copied().unwrap_or(1).max(1);
633
+                self.scroll_down_region(n);
634
+            }
635
+            // Erase Characters
636
+            'X' => {
637
+                let n = params.first().copied().unwrap_or(1).max(1) as usize;
638
+                if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
639
+                    for i in 0..n {
640
+                        let col = self.cursor_col as usize + i;
641
+                        if col < row.len() {
642
+                            row[col] = Cell::default();
643
+                        }
644
+                    }
645
+                }
646
+            }
647
+            // Insert Characters
648
+            '@' => {
649
+                let n = params.first().copied().unwrap_or(1).max(1);
650
+                self.insert_chars(n);
651
+            }
652
+            // Cursor Vertical Absolute
653
+            'd' => {
654
+                let row = params.first().copied().unwrap_or(1).max(1) - 1;
655
+                self.cursor_row = row.min(self.rows - 1);
656
+            }
657
+            // Device Status Report
658
+            'n' => {
659
+                let mode = params.first().copied().unwrap_or(0);
660
+                match mode {
661
+                    5 => {
662
+                        // Status report - respond "OK"
663
+                        self.response_queue.push(b"\x1b[0n".to_vec());
664
+                    }
665
+                    6 => {
666
+                        // Cursor position report
667
+                        let response = format!("\x1b[{};{}R", self.cursor_row + 1, self.cursor_col + 1);
668
+                        self.response_queue.push(response.into_bytes());
669
+                    }
670
+                    _ => {}
671
+                }
672
+            }
673
+            // Set Scroll Region (DECSTBM)
674
+            'r' => {
675
+                let top = params.first().copied().unwrap_or(1).max(1) - 1;
676
+                let bottom = params.get(1).copied().unwrap_or(self.rows).max(1) - 1;
677
+                if top < bottom && bottom < self.rows {
678
+                    self.scroll_top = top;
679
+                    self.scroll_bottom = bottom;
680
+                    // Move cursor to home position
681
+                    self.cursor_row = 0;
682
+                    self.cursor_col = 0;
683
+                }
684
+            }
685
+            // Save Cursor Position
686
+            's' => {
687
+                self.save_cursor();
688
+            }
689
+            // Restore Cursor Position
690
+            'u' => {
691
+                self.restore_cursor();
692
+            }
335693
             // Select Graphic Rendition (SGR) - colors and attributes
336694
             'm' => {
337695
                 if params.is_empty() {
@@ -446,5 +804,33 @@ impl Perform for TerminalScreen {
446804
         }
447805
     }
448806
 
449
-    fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}
807
+    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
808
+        match (intermediates, byte) {
809
+            // Save cursor position (DECSC)
810
+            ([], b'7') => self.save_cursor(),
811
+            // Restore cursor position (DECRC)
812
+            ([], b'8') => self.restore_cursor(),
813
+            // Reverse Index - move cursor up, scroll down if at top
814
+            ([], b'M') => self.reverse_index(),
815
+            // Index - move cursor down, scroll up if at bottom
816
+            ([], b'D') => self.index(),
817
+            // Next Line - move to start of next line
818
+            ([], b'E') => self.next_line(),
819
+            // Reset to Initial State (RIS)
820
+            ([], b'c') => {
821
+                // Full reset
822
+                self.clear_screen();
823
+                self.cursor_row = 0;
824
+                self.cursor_col = 0;
825
+                self.current_fg = Color::Default;
826
+                self.current_bg = Color::Default;
827
+                self.current_bold = false;
828
+                self.current_underline = false;
829
+                self.current_inverse = false;
830
+                self.scroll_top = 0;
831
+                self.scroll_bottom = self.rows.saturating_sub(1);
832
+            }
833
+            _ => {}
834
+        }
835
+    }
450836
 }