Skip to main content

frankenterm_core/
cursor.rs

1//! Terminal cursor: position, visibility, movement, and saved state.
2//!
3//! The cursor tracks the current writing position in the grid and manages
4//! saved/restored state for DECSC/DECRC sequences. It also tracks the
5//! scroll region (top/bottom margins) and tab stops.
6
7use crate::cell::SgrAttrs;
8
9/// Default tab stop interval (every 8 columns).
10const DEFAULT_TAB_INTERVAL: u16 = 8;
11
12/// Terminal cursor state.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Cursor {
15    /// Current row (0-indexed from top of viewport).
16    pub row: u16,
17    /// Current column (0-indexed from left).
18    pub col: u16,
19    /// Whether the cursor is visible (DECTCEM).
20    pub visible: bool,
21    /// Pending wrap: the cursor is at the right margin and the next printable
22    /// character should trigger a line wrap. This avoids the xterm off-by-one
23    /// behavior where the cursor sits *past* the last column.
24    pub pending_wrap: bool,
25    /// Current SGR attributes applied to newly written characters.
26    pub attrs: SgrAttrs,
27    /// Top of scroll region (inclusive, 0-indexed).
28    scroll_top: u16,
29    /// Bottom of scroll region (exclusive, 0-indexed).
30    scroll_bottom: u16,
31    /// Tab stop positions (sorted). `true` at column index means tab stop set.
32    tab_stops: Vec<bool>,
33    /// Character set slots G0–G3. Each stores a charset designator byte:
34    /// `b'B'` = ASCII (default), `b'0'` = DEC Special Graphics, `b'A'` = UK.
35    pub charset_slots: [u8; 4],
36    /// Active character set slot index (0 = G0, default).
37    pub active_charset: u8,
38    /// Single-shift override: if `Some(2)` or `Some(3)`, the next printed char
39    /// uses G2 or G3 respectively, then clears back to `None`.
40    pub single_shift: Option<u8>,
41}
42
43impl Cursor {
44    /// Create a cursor for a grid of the given dimensions.
45    pub fn new(cols: u16, rows: u16) -> Self {
46        let mut tab_stops = vec![false; cols as usize];
47        for i in (0..cols).step_by(DEFAULT_TAB_INTERVAL as usize) {
48            tab_stops[i as usize] = true;
49        }
50        Self {
51            row: 0,
52            col: 0,
53            visible: true,
54            pending_wrap: false,
55            attrs: SgrAttrs::default(),
56            scroll_top: 0,
57            scroll_bottom: rows,
58            tab_stops,
59            charset_slots: [b'B'; 4],
60            active_charset: 0,
61            single_shift: None,
62        }
63    }
64
65    /// Create a cursor at the given position with default attributes.
66    pub fn at(row: u16, col: u16) -> Self {
67        Self {
68            row,
69            col,
70            ..Self::default()
71        }
72    }
73
74    // ── Scroll region ───────────────────────────────────────────────
75
76    /// Top of scroll region (inclusive).
77    pub fn scroll_top(&self) -> u16 {
78        self.scroll_top
79    }
80
81    /// Bottom of scroll region (exclusive).
82    pub fn scroll_bottom(&self) -> u16 {
83        self.scroll_bottom
84    }
85
86    /// Set scroll region margins (DECSTBM). Both are 0-indexed.
87    /// `top` is inclusive, `bottom` is exclusive.
88    ///
89    /// If `top >= bottom` or `bottom > rows`, the request is ignored.
90    pub fn set_scroll_region(&mut self, top: u16, bottom: u16, rows: u16) {
91        if top < bottom && bottom <= rows {
92            self.scroll_top = top;
93            self.scroll_bottom = bottom;
94        }
95    }
96
97    /// Reset scroll region to full screen.
98    pub fn reset_scroll_region(&mut self, rows: u16) {
99        self.scroll_top = 0;
100        self.scroll_bottom = rows;
101    }
102
103    /// Whether the cursor is inside the scroll region.
104    pub fn in_scroll_region(&self) -> bool {
105        self.row >= self.scroll_top && self.row < self.scroll_bottom
106    }
107
108    // ── Movement ────────────────────────────────────────────────────
109
110    /// Clamp the cursor position to the given grid bounds.
111    pub fn clamp(&mut self, rows: u16, cols: u16) {
112        if rows > 0 {
113            self.row = self.row.min(rows - 1);
114        }
115        if cols > 0 {
116            self.col = self.col.min(cols - 1);
117        }
118        self.pending_wrap = false;
119    }
120
121    /// CUP: Move cursor to absolute position (0-indexed).
122    /// Coordinates are clamped to grid bounds.
123    pub fn move_to(&mut self, row: u16, col: u16, rows: u16, cols: u16) {
124        self.row = row.min(rows.saturating_sub(1));
125        self.col = col.min(cols.saturating_sub(1));
126        self.pending_wrap = false;
127    }
128
129    /// CUU: Move cursor up by `count` rows, stopping at the top margin.
130    pub fn move_up(&mut self, count: u16) {
131        let limit = self.scroll_top;
132        self.row = self.row.saturating_sub(count).max(limit);
133        self.pending_wrap = false;
134    }
135
136    /// CUD: Move cursor down by `count` rows, stopping at the bottom margin.
137    pub fn move_down(&mut self, count: u16, rows: u16) {
138        let limit = self.scroll_bottom.min(rows).saturating_sub(1);
139        self.row = self.row.saturating_add(count).min(limit);
140        self.pending_wrap = false;
141    }
142
143    /// CUF: Move cursor right by `count` columns, stopping at the right margin.
144    pub fn move_right(&mut self, count: u16, cols: u16) {
145        self.col = self.col.saturating_add(count).min(cols.saturating_sub(1));
146        self.pending_wrap = false;
147    }
148
149    /// CUB: Move cursor left by `count` columns, stopping at column 0.
150    pub fn move_left(&mut self, count: u16) {
151        self.col = self.col.saturating_sub(count);
152        self.pending_wrap = false;
153    }
154
155    /// CR: Carriage return — move cursor to column 0.
156    pub fn carriage_return(&mut self) {
157        self.col = 0;
158        self.pending_wrap = false;
159    }
160
161    // ── Tab stops ───────────────────────────────────────────────────
162
163    /// Advance to the next tab stop. Returns the new column.
164    pub fn next_tab_stop(&self, cols: u16) -> u16 {
165        let start = (self.col as usize).saturating_add(1);
166        for i in start..self.tab_stops.len().min(cols as usize) {
167            if self.tab_stops[i] {
168                return i as u16;
169            }
170        }
171        // No tab stop found — go to last column.
172        cols.saturating_sub(1)
173    }
174
175    /// Move back to the previous tab stop. Returns the new column.
176    pub fn prev_tab_stop(&self) -> u16 {
177        if self.col == 0 {
178            return 0;
179        }
180        for i in (0..self.col as usize).rev() {
181            if self.tab_stops[i] {
182                return i as u16;
183            }
184        }
185        0
186    }
187
188    /// Set a tab stop at the current column.
189    pub fn set_tab_stop(&mut self) {
190        if (self.col as usize) < self.tab_stops.len() {
191            self.tab_stops[self.col as usize] = true;
192        }
193    }
194
195    /// Clear the tab stop at the current column.
196    pub fn clear_tab_stop(&mut self) {
197        if (self.col as usize) < self.tab_stops.len() {
198            self.tab_stops[self.col as usize] = false;
199        }
200    }
201
202    /// Clear all tab stops.
203    pub fn clear_all_tab_stops(&mut self) {
204        for stop in &mut self.tab_stops {
205            *stop = false;
206        }
207    }
208
209    /// Reset tab stops to the default interval (every 8 columns).
210    pub fn reset_tab_stops(&mut self, cols: u16) {
211        self.tab_stops = vec![false; cols as usize];
212        for i in (0..cols).step_by(DEFAULT_TAB_INTERVAL as usize) {
213            self.tab_stops[i as usize] = true;
214        }
215    }
216
217    // ── Resize ──────────────────────────────────────────────────────
218
219    /// Adjust cursor state after a grid resize.
220    pub fn resize(&mut self, new_cols: u16, new_rows: u16) {
221        let old_cols = self.tab_stops.len();
222        self.scroll_top = 0;
223        self.scroll_bottom = new_rows;
224        self.clamp(new_rows, new_cols);
225        // Resize tab stops, preserving existing stops in the original range.
226        self.tab_stops.resize(new_cols as usize, false);
227        // Set default tab stops only on newly added columns.
228        for i in (0..new_cols).step_by(DEFAULT_TAB_INTERVAL as usize) {
229            let idx = i as usize;
230            if idx >= old_cols {
231                self.tab_stops[idx] = true;
232            }
233        }
234    }
235}
236
237impl Default for Cursor {
238    fn default() -> Self {
239        Self {
240            row: 0,
241            col: 0,
242            visible: true,
243            pending_wrap: false,
244            attrs: SgrAttrs::default(),
245            scroll_top: 0,
246            scroll_bottom: 24, // reasonable default
247            tab_stops: {
248                let mut stops = vec![false; 80];
249                for i in (0..80).step_by(DEFAULT_TAB_INTERVAL as usize) {
250                    stops[i] = true;
251                }
252                stops
253            },
254            charset_slots: [b'B'; 4],
255            active_charset: 0,
256            single_shift: None,
257        }
258    }
259}
260
261/// Saved cursor state for DECSC / DECRC.
262///
263/// Captures the full cursor state so it can be restored exactly.
264#[derive(Debug, Clone, PartialEq, Eq, Default)]
265pub struct SavedCursor {
266    pub row: u16,
267    pub col: u16,
268    pub attrs: SgrAttrs,
269    pub origin_mode: bool,
270    pub pending_wrap: bool,
271    pub charset_slots: [u8; 4],
272    pub active_charset: u8,
273}
274
275impl SavedCursor {
276    /// Capture the current cursor state.
277    pub fn save(cursor: &Cursor, origin_mode: bool) -> Self {
278        Self {
279            row: cursor.row,
280            col: cursor.col,
281            attrs: cursor.attrs,
282            origin_mode,
283            pending_wrap: cursor.pending_wrap,
284            charset_slots: cursor.charset_slots,
285            active_charset: cursor.active_charset,
286        }
287    }
288
289    /// Restore the saved state into the cursor.
290    pub fn restore(&self, cursor: &mut Cursor) {
291        cursor.row = self.row;
292        cursor.col = self.col;
293        cursor.attrs = self.attrs;
294        cursor.pending_wrap = self.pending_wrap;
295        cursor.charset_slots = self.charset_slots;
296        cursor.active_charset = self.active_charset;
297        cursor.single_shift = None;
298    }
299}
300
301// ── Character set translation ─────────────────────────────────────────
302
303/// Translate a character through the DEC Special Graphics charset (`ESC ( 0`).
304///
305/// Maps ASCII 0x60–0x7E to Unicode line-drawing and symbol characters.
306/// Characters outside this range pass through unchanged.
307fn dec_graphics_char(ch: char) -> char {
308    match ch {
309        '`' => '\u{25C6}', // ◆ diamond
310        'a' => '\u{2592}', // ▒ checker board
311        'b' => '\u{2409}', // ␉ HT symbol
312        'c' => '\u{240C}', // ␌ FF symbol
313        'd' => '\u{240D}', // ␍ CR symbol
314        'e' => '\u{240A}', // ␊ LF symbol
315        'f' => '\u{00B0}', // ° degree sign
316        'g' => '\u{00B1}', // ± plus-minus
317        'h' => '\u{2424}', // ␤ NL symbol
318        'i' => '\u{240B}', // ␋ VT symbol
319        'j' => '\u{2518}', // ┘ lower-right corner
320        'k' => '\u{2510}', // ┐ upper-right corner
321        'l' => '\u{250C}', // ┌ upper-left corner
322        'm' => '\u{2514}', // └ lower-left corner
323        'n' => '\u{253C}', // ┼ crossing lines
324        'o' => '\u{23BA}', // ⎺ scan line 1
325        'p' => '\u{23BB}', // ⎻ scan line 3
326        'q' => '\u{2500}', // ─ horizontal line
327        'r' => '\u{23BC}', // ⎼ scan line 7
328        's' => '\u{23BD}', // ⎽ scan line 9
329        't' => '\u{251C}', // ├ left tee
330        'u' => '\u{2524}', // ┤ right tee
331        'v' => '\u{2534}', // ┴ bottom tee
332        'w' => '\u{252C}', // ┬ top tee
333        'x' => '\u{2502}', // │ vertical line
334        'y' => '\u{2264}', // ≤ less-than-or-equal
335        'z' => '\u{2265}', // ≥ greater-than-or-equal
336        '{' => '\u{03C0}', // π pi
337        '|' => '\u{2260}', // ≠ not-equal
338        '}' => '\u{00A3}', // £ pound sign
339        '~' => '\u{00B7}', // · centered dot
340        _ => ch,
341    }
342}
343
344/// Translate a character through the given charset designator.
345///
346/// - `b'B'` (ASCII): pass-through
347/// - `b'0'` (DEC Special Graphics): line-drawing substitution
348/// - All others: pass-through (UK charset differences are negligible)
349pub fn translate_charset(ch: char, charset_designator: u8) -> char {
350    match charset_designator {
351        b'0' => dec_graphics_char(ch),
352        _ => ch,
353    }
354}
355
356impl Cursor {
357    /// Get the effective charset designator for the next printed character,
358    /// accounting for single-shift overrides.
359    pub fn effective_charset(&self) -> u8 {
360        if let Some(shift) = self.single_shift {
361            let slot = (shift as usize).min(3);
362            self.charset_slots[slot]
363        } else {
364            self.charset_slots[self.active_charset as usize & 3]
365        }
366    }
367
368    /// Consume the single-shift state (call after printing a character).
369    pub fn consume_single_shift(&mut self) {
370        self.single_shift = None;
371    }
372
373    /// Designate a charset for a slot.
374    pub fn designate_charset(&mut self, slot: u8, charset: u8) {
375        let idx = (slot as usize).min(3);
376        self.charset_slots[idx] = charset;
377    }
378
379    /// Reset charset state to defaults (all ASCII, G0 active, no single-shift).
380    pub fn reset_charset(&mut self) {
381        self.charset_slots = [b'B'; 4];
382        self.active_charset = 0;
383        self.single_shift = None;
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::cell::SgrFlags;
391
392    #[test]
393    fn default_cursor_at_origin() {
394        let c = Cursor::default();
395        assert_eq!(c.row, 0);
396        assert_eq!(c.col, 0);
397        assert!(c.visible);
398        assert!(!c.pending_wrap);
399    }
400
401    #[test]
402    fn cursor_new_with_dimensions() {
403        let c = Cursor::new(80, 24);
404        assert_eq!(c.scroll_top(), 0);
405        assert_eq!(c.scroll_bottom(), 24);
406    }
407
408    #[test]
409    fn cursor_at_position() {
410        let c = Cursor::at(5, 10);
411        assert_eq!(c.row, 5);
412        assert_eq!(c.col, 10);
413    }
414
415    #[test]
416    fn cursor_clamp_to_bounds() {
417        let mut c = Cursor::at(100, 200);
418        c.clamp(24, 80);
419        assert_eq!(c.row, 23);
420        assert_eq!(c.col, 79);
421        assert!(!c.pending_wrap);
422    }
423
424    #[test]
425    fn move_to_clamps() {
426        let mut c = Cursor::new(80, 24);
427        c.move_to(999, 999, 24, 80);
428        assert_eq!(c.row, 23);
429        assert_eq!(c.col, 79);
430    }
431
432    #[test]
433    fn move_up_stops_at_scroll_top() {
434        let mut c = Cursor::new(80, 24);
435        c.set_scroll_region(5, 20, 24);
436        c.row = 7;
437        c.move_up(10);
438        assert_eq!(c.row, 5);
439    }
440
441    #[test]
442    fn move_down_stops_at_scroll_bottom() {
443        let mut c = Cursor::new(80, 24);
444        c.set_scroll_region(0, 10, 24);
445        c.row = 8;
446        c.move_down(10, 24);
447        assert_eq!(c.row, 9); // bottom - 1
448    }
449
450    #[test]
451    fn move_left_stops_at_zero() {
452        let mut c = Cursor::new(80, 24);
453        c.col = 3;
454        c.move_left(100);
455        assert_eq!(c.col, 0);
456    }
457
458    #[test]
459    fn move_right_stops_at_margin() {
460        let mut c = Cursor::new(80, 24);
461        c.col = 70;
462        c.move_right(100, 80);
463        assert_eq!(c.col, 79);
464    }
465
466    #[test]
467    fn carriage_return() {
468        let mut c = Cursor::new(80, 24);
469        c.col = 42;
470        c.pending_wrap = true;
471        c.carriage_return();
472        assert_eq!(c.col, 0);
473        assert!(!c.pending_wrap);
474    }
475
476    // ── Scroll region ───────────────────────────────────────────────
477
478    #[test]
479    fn set_scroll_region() {
480        let mut c = Cursor::new(80, 24);
481        c.set_scroll_region(5, 20, 24);
482        assert_eq!(c.scroll_top(), 5);
483        assert_eq!(c.scroll_bottom(), 20);
484    }
485
486    #[test]
487    fn invalid_scroll_region_is_ignored() {
488        let mut c = Cursor::new(80, 24);
489        c.set_scroll_region(20, 5, 24); // top >= bottom
490        assert_eq!(c.scroll_top(), 0);
491        assert_eq!(c.scroll_bottom(), 24);
492    }
493
494    #[test]
495    fn reset_scroll_region() {
496        let mut c = Cursor::new(80, 24);
497        c.set_scroll_region(5, 20, 24);
498        c.reset_scroll_region(24);
499        assert_eq!(c.scroll_top(), 0);
500        assert_eq!(c.scroll_bottom(), 24);
501    }
502
503    #[test]
504    fn in_scroll_region() {
505        let mut c = Cursor::new(80, 24);
506        c.set_scroll_region(5, 15, 24);
507        c.row = 10;
508        assert!(c.in_scroll_region());
509        c.row = 3;
510        assert!(!c.in_scroll_region());
511        c.row = 15; // exclusive boundary
512        assert!(!c.in_scroll_region());
513    }
514
515    // ── Tab stops ───────────────────────────────────────────────────
516
517    #[test]
518    fn default_tab_stops_every_8() {
519        let c = Cursor::new(80, 24);
520        // Tab stops at 0, 8, 16, 24, ...
521        assert!(c.tab_stops[0]);
522        assert!(c.tab_stops[8]);
523        assert!(!c.tab_stops[7]);
524        assert!(c.tab_stops[16]);
525    }
526
527    #[test]
528    fn next_tab_stop() {
529        let c = Cursor::new(80, 24);
530        // From col 0, next tab is col 8.
531        let mut c2 = c.clone();
532        c2.col = 0;
533        assert_eq!(c2.next_tab_stop(80), 8);
534
535        // From col 7, next tab is col 8.
536        c2.col = 7;
537        assert_eq!(c2.next_tab_stop(80), 8);
538
539        // From col 8, next tab is col 16.
540        c2.col = 8;
541        assert_eq!(c2.next_tab_stop(80), 16);
542    }
543
544    #[test]
545    fn prev_tab_stop() {
546        let c = Cursor::new(80, 24);
547        let mut c2 = c.clone();
548        c2.col = 10;
549        assert_eq!(c2.prev_tab_stop(), 8);
550
551        c2.col = 8;
552        assert_eq!(c2.prev_tab_stop(), 0);
553
554        c2.col = 0;
555        assert_eq!(c2.prev_tab_stop(), 0);
556    }
557
558    #[test]
559    fn set_and_clear_tab_stop() {
560        let mut c = Cursor::new(80, 24);
561        c.col = 5;
562        c.set_tab_stop();
563        assert!(c.tab_stops[5]);
564
565        c.col = 5;
566        c.clear_tab_stop();
567        assert!(!c.tab_stops[5]);
568    }
569
570    #[test]
571    fn clear_all_tab_stops() {
572        let mut c = Cursor::new(80, 24);
573        c.clear_all_tab_stops();
574        assert!(c.tab_stops.iter().all(|&s| !s));
575    }
576
577    // ── Save/restore ────────────────────────────────────────────────
578
579    #[test]
580    fn save_restore_roundtrip() {
581        let mut cursor = Cursor::at(5, 10);
582        cursor.attrs.flags = SgrFlags::BOLD;
583        cursor.pending_wrap = true;
584
585        let saved = SavedCursor::save(&cursor, true);
586        assert_eq!(saved.row, 5);
587        assert_eq!(saved.col, 10);
588        assert!(saved.origin_mode);
589
590        let mut new_cursor = Cursor::default();
591        saved.restore(&mut new_cursor);
592        assert_eq!(new_cursor.row, 5);
593        assert_eq!(new_cursor.col, 10);
594        assert!(new_cursor.pending_wrap);
595        assert_eq!(new_cursor.attrs.flags, SgrFlags::BOLD);
596    }
597
598    // ── Edge cases ──────────────────────────────────────────────────
599
600    #[test]
601    fn move_to_zero_size_grid() {
602        let mut c = Cursor::default();
603        c.move_to(5, 5, 0, 0);
604        // saturating_sub(1) on 0 gives 0, so row and col stay 0.
605        assert_eq!(c.row, 0);
606        assert_eq!(c.col, 0);
607    }
608
609    #[test]
610    fn tab_stop_past_end_returns_last_col() {
611        let mut c = Cursor::new(10, 1);
612        c.clear_all_tab_stops();
613        c.col = 5;
614        assert_eq!(c.next_tab_stop(10), 9);
615    }
616
617    #[test]
618    fn resize_wider_sets_tab_stops_on_new_columns() {
619        let mut c = Cursor::new(80, 24);
620        // Clear a user-set tab stop to verify it's preserved after resize.
621        c.tab_stops[8] = false;
622
623        c.resize(120, 24);
624
625        // Original stops preserved: col 0 still set, col 8 still cleared.
626        assert!(c.tab_stops[0], "original tab stop at 0 must be preserved");
627        assert!(
628            !c.tab_stops[8],
629            "user-cleared tab stop at 8 must remain cleared"
630        );
631        // New columns get default tab stops at 8-column intervals.
632        assert!(c.tab_stops[80], "new column 80 must get a default tab stop");
633        assert!(c.tab_stops[88], "new column 88 must get a default tab stop");
634        assert!(c.tab_stops[96], "new column 96 must get a default tab stop");
635        assert!(!c.tab_stops[81], "new column 81 must not have a tab stop");
636    }
637
638    #[test]
639    fn resize_narrower_preserves_existing_tab_stops() {
640        let mut c = Cursor::new(80, 24);
641        c.resize(40, 24);
642
643        // Tab stops within the new width are preserved.
644        assert!(c.tab_stops[0]);
645        assert!(c.tab_stops[8]);
646        assert!(c.tab_stops[16]);
647        assert!(c.tab_stops[24]);
648        assert!(c.tab_stops[32]);
649        assert_eq!(c.tab_stops.len(), 40);
650    }
651
652    // ── Charset ──────────────────────────────────────────────────────
653
654    #[test]
655    fn default_charset_is_ascii() {
656        let c = Cursor::new(80, 24);
657        assert_eq!(c.charset_slots, [b'B'; 4]);
658        assert_eq!(c.active_charset, 0);
659        assert!(c.single_shift.is_none());
660        assert_eq!(c.effective_charset(), b'B');
661    }
662
663    #[test]
664    fn designate_charset_sets_slot() {
665        let mut c = Cursor::new(80, 24);
666        c.designate_charset(0, b'0'); // G0 = DEC Graphics
667        assert_eq!(c.charset_slots[0], b'0');
668        assert_eq!(c.effective_charset(), b'0');
669        // G1 still ASCII
670        assert_eq!(c.charset_slots[1], b'B');
671    }
672
673    #[test]
674    fn single_shift_overrides_effective_charset() {
675        let mut c = Cursor::new(80, 24);
676        c.charset_slots[2] = b'0'; // G2 = DEC Graphics
677        c.single_shift = Some(2);
678        assert_eq!(c.effective_charset(), b'0');
679        c.consume_single_shift();
680        assert!(c.single_shift.is_none());
681        // Back to G0 ASCII
682        assert_eq!(c.effective_charset(), b'B');
683    }
684
685    #[test]
686    fn reset_charset_restores_defaults() {
687        let mut c = Cursor::new(80, 24);
688        c.charset_slots = [b'0'; 4];
689        c.active_charset = 2;
690        c.single_shift = Some(3);
691        c.reset_charset();
692        assert_eq!(c.charset_slots, [b'B'; 4]);
693        assert_eq!(c.active_charset, 0);
694        assert!(c.single_shift.is_none());
695    }
696
697    #[test]
698    fn dec_graphics_translation() {
699        use super::translate_charset;
700        // Line-drawing corners
701        assert_eq!(translate_charset('j', b'0'), '┘');
702        assert_eq!(translate_charset('k', b'0'), '┐');
703        assert_eq!(translate_charset('l', b'0'), '┌');
704        assert_eq!(translate_charset('m', b'0'), '└');
705        assert_eq!(translate_charset('q', b'0'), '─');
706        assert_eq!(translate_charset('x', b'0'), '│');
707        assert_eq!(translate_charset('n', b'0'), '┼');
708        // Tees
709        assert_eq!(translate_charset('t', b'0'), '├');
710        assert_eq!(translate_charset('u', b'0'), '┤');
711        assert_eq!(translate_charset('v', b'0'), '┴');
712        assert_eq!(translate_charset('w', b'0'), '┬');
713        // Symbols
714        assert_eq!(translate_charset('`', b'0'), '◆');
715        assert_eq!(translate_charset('a', b'0'), '▒');
716        assert_eq!(translate_charset('~', b'0'), '·');
717        assert_eq!(translate_charset('{', b'0'), 'π');
718        // Non-Graphics range passes through
719        assert_eq!(translate_charset('A', b'0'), 'A');
720        // ASCII charset is pass-through
721        assert_eq!(translate_charset('q', b'B'), 'q');
722    }
723
724    #[test]
725    fn save_restore_preserves_charset() {
726        let mut cursor = Cursor::new(80, 24);
727        cursor.designate_charset(0, b'0');
728        cursor.designate_charset(1, b'A');
729        cursor.active_charset = 1;
730
731        let saved = SavedCursor::save(&cursor, false);
732        assert_eq!(saved.charset_slots[0], b'0');
733        assert_eq!(saved.charset_slots[1], b'A');
734        assert_eq!(saved.active_charset, 1);
735
736        let mut new_cursor = Cursor::new(80, 24);
737        saved.restore(&mut new_cursor);
738        assert_eq!(new_cursor.charset_slots[0], b'0');
739        assert_eq!(new_cursor.charset_slots[1], b'A');
740        assert_eq!(new_cursor.active_charset, 1);
741        assert!(new_cursor.single_shift.is_none());
742    }
743}