Skip to main content

lineread/
chars.rs

1//! Provides utilities for manipulating character values
2
3// This is technically configurable on Unix, but exposing that information
4// from the low-level terminal interface and storing it in Reader is a pain.
5// Does anyone even care?
6/// Character value indicating end-of-file
7pub const EOF: char = '\x04';
8
9/// Character value generated by the Escape key
10pub const ESCAPE: char = '\x1b';
11
12/// Character value generated by the Backspace key
13///
14/// On Unix systems, this is equivalent to `RUBOUT`
15#[cfg(unix)]
16pub const DELETE: char = RUBOUT;
17
18/// Character value generated by the Backspace key
19///
20/// On Windows systems, this character is Ctrl-H
21#[cfg(windows)]
22pub const DELETE: char = '\x08';
23
24/// Character value generated by the Backspace key on some systems
25pub const RUBOUT: char = '\x7f';
26
27/// Returns a character name as a key sequence, e.g. `Control-x` or `Meta-x`.
28///
29/// Returns `None` if the name is invalid.
30pub fn parse_char_name(name: &str) -> Option<String> {
31    let name_lc = name.to_lowercase();
32
33    let is_ctrl = contains_any(&name_lc, &["c-", "ctrl-", "control-"]);
34    let is_meta = contains_any(&name_lc, &["m-", "meta-"]);
35
36    let name = match name_lc.rfind('-') {
37        Some(pos) => &name_lc[pos + 1..],
38        None => &name_lc[..],
39    };
40
41    let ch = match name {
42        "del" | "rubout" => DELETE,
43        "esc" | "escape" => ESCAPE,
44        "lfd" | "newline" => '\n',
45        "ret" | "return" => '\r',
46        "spc" | "space" => ' ',
47        "tab" => '\t',
48        s if !s.is_empty() => s.chars().next().unwrap(),
49        _ => return None,
50    };
51
52    let ch = match (is_ctrl, is_meta) {
53        (true, true) => meta(ctrl(ch)),
54        (true, false) => ctrl(ch).to_string(),
55        (false, true) => meta(ch),
56        (false, false) => ch.to_string(),
57    };
58
59    Some(ch)
60}
61
62/// Returns a character sequence escaped for user-facing display.
63///
64/// Escape is formatted as `\e`.
65/// Control key combinations are prefixed with `\C-`.
66pub fn escape_sequence(s: &str) -> String {
67    let mut res = String::with_capacity(s.len());
68
69    for ch in s.chars() {
70        match ch {
71            ESCAPE => res.push_str(r"\e"),
72            RUBOUT => res.push_str(r"\C-?"),
73            '\\' => res.push_str(r"\\"),
74            '\'' => res.push_str(r"\'"),
75            '"' => res.push_str(r#"\""#),
76            ch if is_ctrl(ch) => {
77                res.push_str(r"\C-");
78                res.push(unctrl_lower(ch));
79            }
80            ch => res.push(ch),
81        }
82    }
83
84    res
85}
86
87/// Returns a meta sequence for the given character.
88pub fn meta(ch: char) -> String {
89    let mut s = String::with_capacity(ch.len_utf8() + 1);
90    s.push(ESCAPE);
91    s.push(ch);
92    s
93}
94
95fn contains_any(s: &str, strs: &[&str]) -> bool {
96    strs.iter().any(|a| s.contains(a))
97}
98
99/// Returns whether the character is printable.
100///
101/// That is, not NUL or a control character (other than Tab or Newline).
102pub fn is_printable(c: char) -> bool {
103    c == '\t' || c == '\n' || !(c == '\0' || is_ctrl(c))
104}
105
106const CTRL_BIT: u8 = 0x40;
107const CTRL_MASK: u8 = 0x1f;
108
109/// Returns whether the given character is a control character.
110pub fn is_ctrl(c: char) -> bool {
111    const CTRL_MAX: u32 = 0x1f;
112
113    c != '\0' && c as u32 <= CTRL_MAX
114}
115
116/// Returns a control character for the given character.
117pub fn ctrl(c: char) -> char {
118    ((c as u8) & CTRL_MASK) as char
119}
120
121/// Returns the printable character corresponding to the given control character.
122pub fn unctrl(c: char) -> char {
123    ((c as u8) | CTRL_BIT) as char
124}
125
126/// Returns the lowercase character corresponding to the given control character.
127pub fn unctrl_lower(c: char) -> char {
128    unctrl(c).to_ascii_lowercase()
129}
130
131#[cfg(test)]
132mod test {
133    use super::{ctrl, escape_sequence, parse_char_name, unctrl, unctrl_lower};
134
135    #[test]
136    fn test_ctrl() {
137        assert_eq!(ctrl('A'), '\x01');
138        assert_eq!(ctrl('I'), '\t');
139        assert_eq!(ctrl('J'), '\n');
140        assert_eq!(ctrl('M'), '\r');
141
142        assert_eq!(unctrl('\x01'), 'A');
143        assert_eq!(unctrl('\t'), 'I');
144        assert_eq!(unctrl('\n'), 'J');
145        assert_eq!(unctrl('\r'), 'M');
146    }
147
148    #[test]
149    fn test_unctrl() {
150        assert_eq!(unctrl('\x1d'), ']');
151        assert_eq!(unctrl_lower('\x1d'), ']');
152    }
153
154    #[test]
155    fn test_escape() {
156        assert_eq!(escape_sequence("\x1b\x7f"), r"\e\C-?");
157    }
158
159    #[test]
160    fn test_parse_char() {
161        assert_eq!(parse_char_name("Escape"), Some("\x1b".to_owned()));
162        assert_eq!(parse_char_name("Control-u"), Some("\x15".to_owned()));
163        assert_eq!(parse_char_name("Meta-tab"), Some("\x1b\t".to_owned()));
164    }
165}