1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! Provides utilities for manipulating character values

// This is technically configurable on Unix, but exposing that information
// from the low-level terminal interface and storing it in Reader is a pain.
// Does anyone even care?
/// Character value indicating end-of-file
pub const EOF: char = '\x04';

/// Character value generated by the Escape key
pub const ESCAPE: char = '\x1b';

/// Character value generated by the Backspace key
///
/// On Unix systems, this is equivalent to `RUBOUT`
#[cfg(unix)]
pub const DELETE: char = RUBOUT;

/// Character value generated by the Backspace key
///
/// On Windows systems, this character is Ctrl-H
#[cfg(windows)]
pub const DELETE: char = '\x08';

/// Character value generated by the Backspace key on some systems
pub const RUBOUT: char = '\x7f';

/// Returns a character name as a key sequence, e.g. `Control-x` or `Meta-x`.
///
/// Returns `None` if the name is invalid.
pub fn parse_char_name(name: &str) -> Option<String> {
    let name_lc = name.to_lowercase();

    let is_ctrl = contains_any(&name_lc, &["c-", "ctrl-", "control-"]);
    let is_meta = contains_any(&name_lc, &["m-", "meta-"]);

    let name = match name_lc.rfind('-') {
        Some(pos) => &name_lc[pos + 1..],
        None => &name_lc[..]
    };

    let ch = match name {
        "del" | "rubout"  => DELETE,
        "esc" | "escape"  => ESCAPE,
        "lfd" | "newline" => '\n',
        "ret" | "return"  => '\r',
        "spc" | "space"   => ' ',
        "tab"             => '\t',
        s if !s.is_empty() => s.chars().next().unwrap(),
        _ => return None
    };

    let ch = match (is_ctrl, is_meta) {
        (true,  true)  => meta(ctrl(ch)),
        (true,  false) => ctrl(ch).to_string(),
        (false, true)  => meta(ch),
        (false, false) => ch.to_string(),
    };

    Some(ch)
}

/// Returns a character sequence escaped for user-facing display.
///
/// Escape is formatted as `\e`.
/// Control key combinations are prefixed with `\C-`.
pub fn escape_sequence(s: &str) -> String {
    let mut res = String::with_capacity(s.len());

    for ch in s.chars() {
        match ch {
            ESCAPE => res.push_str(r"\e"),
            RUBOUT => res.push_str(r"\C-?"),
            '\\' => res.push_str(r"\\"),
            '\'' => res.push_str(r"\'"),
            '"' => res.push_str(r#"\""#),
            ch if is_ctrl(ch) => {
                res.push_str(r"\C-");
                res.push(unctrl_lower(ch));
            }
            ch => res.push(ch)
        }
    }

    res
}

/// Returns a meta sequence for the given character.
pub fn meta(ch: char) -> String {
    let mut s = String::with_capacity(ch.len_utf8() + 1);
    s.push(ESCAPE);
    s.push(ch);
    s
}

fn contains_any(s: &str, strs: &[&str]) -> bool {
    strs.iter().any(|a| s.contains(a))
}

/// Returns whether the character is printable.
///
/// That is, not NUL or a control character (other than Tab or Newline).
pub fn is_printable(c: char) -> bool {
    c == '\t' || c == '\n' || !(c == '\0' || is_ctrl(c))
}

const CTRL_BIT: u8 = 0x40;
const CTRL_MASK: u8 = 0x1f;

/// Returns whether the given character is a control character.
pub fn is_ctrl(c: char) -> bool {
    const CTRL_MAX: u32 = 0x1f;

    c != '\0' && c as u32 <= CTRL_MAX
}

/// Returns a control character for the given character.
pub fn ctrl(c: char) -> char {
    ((c as u8) & CTRL_MASK) as char
}

/// Returns the printable character corresponding to the given control character.
pub fn unctrl(c: char) -> char {
    ((c as u8) | CTRL_BIT) as char
}

/// Returns the lowercase character corresponding to the given control character.
pub fn unctrl_lower(c: char) -> char {
    unctrl(c).to_ascii_lowercase()
}

#[cfg(test)]
mod test {
    use super::{ctrl, unctrl, unctrl_lower, escape_sequence, parse_char_name};

    #[test]
    fn test_ctrl() {
        assert_eq!(ctrl('A'), '\x01');
        assert_eq!(ctrl('I'), '\t');
        assert_eq!(ctrl('J'), '\n');
        assert_eq!(ctrl('M'), '\r');

        assert_eq!(unctrl('\x01'), 'A');
        assert_eq!(unctrl('\t'), 'I');
        assert_eq!(unctrl('\n'), 'J');
        assert_eq!(unctrl('\r'), 'M');
    }

    #[test]
    fn test_unctrl() {
        assert_eq!(unctrl('\x1d'), ']');
        assert_eq!(unctrl_lower('\x1d'), ']');
    }

    #[test]
    fn test_escape() {
        assert_eq!(escape_sequence("\x1b\x7f"), r"\e\C-?");
    }

    #[test]
    fn test_parse_char() {
        assert_eq!(parse_char_name("Escape"), Some("\x1b".to_owned()));
        assert_eq!(parse_char_name("Control-u"), Some("\x15".to_owned()));
        assert_eq!(parse_char_name("Meta-tab"), Some("\x1b\t".to_owned()));
    }
}