term_basics_linux/
lib.rs

1//! # Example
2//!
3//! ```
4//! use term_basics_linux as tbl;
5//! print!("typ your name: ");
6//! let name = tbl::input_field_simple(true);
7//! print!("Your name: {name}");
8//! ```
9
10use std::{
11    io::{ self, Write, Read },
12    cmp::{ max, min },
13    collections::VecDeque,
14};
15
16use termios::{ Termios, TCSANOW, ECHO, ICANON, tcsetattr };
17
18/// Reads a char from keyboard input.
19/// Returns the first byte, does not wait for the user to press enter.
20///
21/// # Example
22///
23/// ```
24/// use term_basics_linux as tbl;
25/// println!("Press any key...");
26/// let anykey = tbl::getch();
27/// ```
28pub fn getch() -> u8 {
29    // https://stackoverflow.com/questions/26321592/how-can-i-read-one-character-from-stdin-without-having-to-hit-enter
30    let stdin = 0;
31    let termios = Termios::from_fd(stdin).unwrap();
32    let mut new_termios = termios;
33    new_termios.c_lflag &= !(ICANON | ECHO);
34    tcsetattr(stdin, TCSANOW, &new_termios).unwrap();
35    let stdout = io::stdout();
36    let reader = io::stdin();
37    let mut buffer = [0; 1];
38    stdout.lock().flush().unwrap();
39    reader.lock().read_exact(&mut buffer).unwrap();
40    tcsetattr(stdin, TCSANOW, &termios).unwrap();
41    buffer[0]
42}
43
44/// Prints the result of getch as u8, infinite loop. Can be used for testing.
45///
46/// # Example
47///
48/// ```
49/// use term_basics_linux as tbl;
50/// let user_input = tbl::test_chars();
51/// ```
52pub fn test_chars() {
53    loop {
54        println!("{:?}", getch());
55    }
56}
57
58/// A struct that holds inputs available for the user to scroll through.
59pub struct InputList {
60    ilist: VecDeque<String>,
61    maxlen: usize,
62}
63
64impl InputList {
65    /// Make a new InputList with a certain maximum capacity.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use term_basics_linux as tbl;
71    /// let mut his = tbl::InputList::new(10);
72    /// let x = tbl::input_field_scrollable(&mut his, true);
73    /// ```
74    pub fn new(maxlen: usize) -> Self {
75        Self{
76            ilist: VecDeque::new(),
77            maxlen
78        }
79    }
80
81    fn trim(&mut self){
82        while self.ilist.len() > self.maxlen {
83            self.ilist.pop_front();
84        }
85    }
86
87    /// Adds an element to the list.
88    /// It will delete items if it's length would grow past the max length.
89    /// the oldest items will be removed.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use term_basics_linux as tbl;
95    /// let mut his = tbl::InputList::new(2);
96    /// his.add(&"0".to_string());
97    /// his.add(&"1".to_string());
98    /// his.add(&"2".to_string());
99    /// // only "1" and "2" will remain, as 0 is removed.
100    /// let _ = tbl::input_field_scrollable(&mut his, true);
101    /// ```
102    pub fn add(&mut self, string: &str) {
103        self.ilist.push_back(string.to_string());
104        self.trim();
105    }
106
107    /// Returns the an Option of String, the element at the given index.
108    /// The index wraps and you can query for negative indices as well as indices above the maximum length.
109    ///
110    /// # Example
111    ///
112    /// ```
113    /// use term_basics_linux as tbl;
114    /// let mut his = tbl::InputList::new(3);
115    /// his.add(&"0".to_string());
116    /// his.add(&"1".to_string());
117    /// his.add(&"2".to_string());
118    /// println!("at -2: {:?}", his.get_index(-2)); // "1"
119    /// println!("at -1: {:?}", his.get_index(-1)); // "2"
120    /// println!("at  0: {:?}", his.get_index(0));  // "0"
121    /// println!("at  1: {:?}", his.get_index(1));  // "1"
122    /// println!("at  2: {:?}", his.get_index(2));  // "2"
123    /// println!("at  3: {:?}", his.get_index(3));  // "0"
124    /// println!("at  4: {:?}", his.get_index(4));  // "1"
125    /// ```
126    pub fn get_index(&self, mut index: i32) -> Option<&String> {
127        if !self.ilist.is_empty(){
128            index %= self.ilist.len() as i32;
129        }
130        if index < 0{
131            index += self.maxlen as i32;
132        }
133        self.ilist.get(index as usize)
134    }
135}
136
137/// What kind of character the input field will print.
138/// ```Copy``` will just print what the user types in.
139/// ```Substitute(char)``` will print that character.
140/// ```None``` will not print anything at all.
141///
142/// # Example
143///
144/// ```
145/// use term_basics_linux as tbl;
146/// println!("{}", tbl::input_field(&mut tbl::InputList::new(0), tbl::PrintChar::Copy, true));
147/// println!("{}", tbl::input_field(&mut tbl::InputList::new(0), tbl::PrintChar::Substitute('#'), true));
148/// println!("{}", tbl::input_field(&mut tbl::InputList::new(0), tbl::PrintChar::None, true));
149/// ```
150#[derive(Debug, Copy, Clone, PartialEq)]
151pub enum PrintChar {
152    Copy,
153    Substitute(char),
154    None,
155}
156
157pub fn input_field(ilist: &mut InputList, pc: PrintChar, newline: bool) -> String {
158    fn charvec_to_string(vec: &[char]) -> String {
159        let mut string = String::new();
160        for &ch in vec {
161            string.push(ch);
162        }
163        string
164    }
165    fn typed_char(
166        ch: u8,
167        buff: &mut Vec<char>,
168        gstate: &mut u8,
169        hstate: &mut u8,
170        pos: &mut usize,
171        pc: PrintChar
172    ){
173        let ch = ch as char;
174        buff.insert(*pos, ch);
175        if pc != PrintChar::None {
176            if *pos != buff.len() - 1 {
177                for item in buff.iter().skip(*pos) {
178                    put_char(*item, pc);
179                }
180                go_back(*pos, buff.len() - 1, pc);
181            } else {
182                put_char(ch, pc);
183            }
184        }
185        *hstate = 0;
186        *gstate = 0;
187        *pos += 1;
188    }
189    fn delete_all(buff: &mut Vec<char>, pc: PrintChar) -> usize {
190        if pc == PrintChar::None {
191            buff.clear();
192            return 0;
193        }
194        let len = buff.len();
195        go_back(0, len, pc);
196        buff.clear();
197        len
198    }
199    fn feed_into_buffer(buff: &mut Vec<char>, string: &str) {
200        for ch in string.chars() {
201            buff.push(ch);
202        }
203    }
204    fn write_all(buff: &[char], pc: PrintChar) {
205        for item in buff.iter() {
206            put_char(*item, pc);
207        }
208    }
209    fn scroll_action(
210        res: &mut Vec<char>,
211        pos: &mut usize,
212        ilist: &InputList,
213        his_index: i32,
214        pc: PrintChar
215    ){
216        let val = ilist.get_index(his_index);
217        if let Some(valv) = val {
218            let old_len = delete_all(res, pc);
219            feed_into_buffer(res, valv);
220            *pos = res.len();
221            if pc == PrintChar::None { return; }
222            write_all(res, pc);
223            let diff = old_len as i32 - res.len() as i32;
224            if diff <= 0 { return; }
225            for _ in 0..diff {
226                print!(" ");
227            }
228            for _ in 0..diff {
229                print!("{}", 8 as char);
230            }
231        }
232    }
233    fn delete(res: &mut Vec<char>, pos: &mut usize, gstate: &mut u8, pc: PrintChar) {
234        if res.is_empty() { return; }
235        if *pos >= res.len() - 1 { return; }
236        res.remove(*pos);
237        for item in res.iter().skip(*pos) {
238            put_char(*item, pc);
239        }
240        if pc != PrintChar::None { print!(" "); }
241        go_back(*pos, res.len() + 1, pc);
242        *gstate = 0;
243    }
244    fn end(res: &mut [char], pos: &mut usize, hoen_state: &mut u8, pc: PrintChar) {
245        if pc != PrintChar::None {
246            for _ in *pos..res.len() {
247                print!("\x1B[1C");
248            }
249        }
250        *hoen_state = 0;
251        *pos = res.len();
252    }
253    fn put_char(ch: char, pc: PrintChar) {
254        match pc {
255            PrintChar::Copy => print!("{}", ch),
256            PrintChar::Substitute(sch) => print!("{}", sch),
257            PrintChar::None => {},
258        };
259    }
260    fn go_back(start: usize, end: usize, pc: PrintChar) {
261        if pc == PrintChar::None { return; }
262        for _ in  start..end {
263            print!("{}", 8 as char);
264        }
265    }
266
267    let mut res = Vec::new();
268    let mut gstate: u8 = 0;
269    let mut hoen_state: u8 = 0;
270    let mut pos = 0;
271    let mut his_index: i32 = 0;
272
273    flush().expect("term_basics_linux: Error: stdout flush failed.");
274    loop {
275        let mut x = getch();
276        if x == 8 { x = 127; } // shift + backspace
277        match x {
278            10 => { // enter
279                if newline {
280                    println!();
281                }
282                break;
283            }
284            127 => { // backspace
285                if res.is_empty() { continue; }
286                if pos == 0 { continue; }
287                res.remove(pos - 1);
288                if pc != PrintChar::None {
289                    print!("{}", 8 as char);
290                    //print!("\x1B[1D"); // also works
291                    for item in res.iter().skip(pos - 1){
292                        put_char(*item, pc);
293                    }
294                    print!(" ");
295                    go_back(pos - 1, res.len() + 1, pc);
296                }
297                pos -= 1;
298                gstate = 0;
299            }
300            27 => { // first char in arrow code and home/end code and other char combos
301                gstate = 1;
302                hoen_state = 1;
303            }
304            91 => { // 2nd char in arrow code and home/end code and other char combos
305                if gstate == 1 { gstate = 2; }
306                if hoen_state == 1 { hoen_state = 2; }
307                if gstate == 2 || hoen_state == 2 { continue; }
308                typed_char(91, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
309            }
310            65 => { // up arrow
311                if gstate == 2 {
312                    gstate = 0;
313                    his_index += 1;
314                    scroll_action(&mut res, &mut pos, ilist, his_index, pc);
315                }
316                else { typed_char(65, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
317            }
318            66 => { // down arrow
319                if gstate == 2 {
320                    gstate = 0;
321                    his_index -= 1;
322                    scroll_action(&mut res, &mut pos, ilist, his_index, pc);
323                }
324                else { typed_char(66, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
325            }
326            72 => { // home key
327                if hoen_state != 2 {
328                    typed_char(72, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
329                    continue;
330                }
331                go_back(0, pos, pc);
332                pos = 0;
333                hoen_state = 0;
334            }
335            51 => {
336                if gstate != 2 {
337                    typed_char(51, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
338                }
339                else {
340                    gstate = 3;
341                }
342            }
343            52 => { // end key 3e char
344                if hoen_state == 2 { hoen_state = 3; }
345                else { typed_char(52, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
346            }
347            70 =>{ // end(27-91-70)
348                if hoen_state == 2 {
349                    end(&mut res, &mut pos, &mut hoen_state, pc);
350                }
351                else{
352                    typed_char(70, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
353                }
354            }
355            126 => { // end(27-91-52-126) or delete(27-91-51-126)
356                if hoen_state == 3 { // end
357                    end(&mut res, &mut pos, &mut hoen_state, pc);
358                }
359                else if gstate >= 2{ // delete
360                    delete(&mut res, &mut pos, &mut gstate, pc);
361                }
362                else {
363                    typed_char(126, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
364                }
365            }
366            80 => { // delete key with code 27-91-80
367                if gstate != 2 {
368                    typed_char(80, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc);
369                    continue;
370                }
371                delete(&mut res, &mut pos, &mut gstate, pc);
372            }
373            67 => { // right arrow
374                if gstate == 2 {
375                    let old_pos = pos;
376                    pos = min(pos + 1, res.len());
377                    gstate = 0;
378                    if pc == PrintChar::None { continue; }
379                    // if pos < res.len() { print!("\x1B[1C"); }
380                    if pos > old_pos { print!("\x1B[1C"); }
381                }
382                else { typed_char(67, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
383            }
384            68 => { // left arrow
385                if gstate == 2 {
386                    let old_pos = pos;
387                    pos = max(pos as i32 - 1, 0_i32) as usize;
388                    gstate = 0;
389                    if pc == PrintChar::None { continue; }
390                    // if pos > 0 { print!("{}", 8 as char); }
391                    if pos < old_pos { print!("{}", 8 as char); }
392                }
393                else { typed_char(68, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
394            }
395            x => { typed_char(x, &mut res, &mut gstate, &mut hoen_state, &mut pos, pc); }
396        }
397    }
398    let string = charvec_to_string(&res);
399    ilist.add(&string);
400    string
401}
402
403pub fn string_to_bool(string: &str) -> bool {
404    matches!(string, "y" | "ye" | "yes" | "ok" | "+" | "t" | "tr" | "tru" | "true")
405}
406
407/// Small helper to parse string to a value
408///
409/// # Example
410///
411/// ```
412/// use term_basics_linux as tbl;
413/// print!("type your age: ");
414/// let user_input = tbl::input_field_simple(true);
415/// let age: Option<u8> = tbl::string_to_value(&user_input);
416/// if age.is_none() { println!("Invalid age!"); }
417/// else { println!("Your age: {}", age.unwrap()); }
418/// ```
419/// Uses ```string.parse::<T>();```
420pub fn string_to_value<T: std::str::FromStr>(string: &str) -> Option<T> {
421    let res = string.parse::<T>();
422    if res.is_err() { return Option::None; }
423    res.ok()
424}
425
426/// Flushes stdout.
427/// When you do print! or term-basics-linux equivalent, it will not print immediately.
428/// flush() will make sure everything is printed first, before you do something else.
429/// Input fields flush themselves before the query for input.
430///
431/// # Example
432///
433/// ```
434/// use term_basics_linux as tbl;
435/// print!("type: ");
436/// tbl::flush().expect("oh no");
437/// ```
438///
439pub fn flush() -> io::Result<()> {
440    std::io::stdout().flush()
441}
442
443/// Prints a message to the user.
444/// The user can  type its input next to the message on the same line.
445/// It will return the user input after the user pressed enter.
446/// It uses term_basics_linux::input_field and supports the same operations.
447///
448/// # Example
449///
450/// ```
451/// use term_basics_linux as tbl;
452/// print!("type your name: ");
453/// let name = tbl::input_field_simple(true);
454/// print!("Your name: ");
455/// println!("{name}");
456/// ```
457pub fn input_field_simple(newline: bool) -> String {
458    input_field(&mut InputList::new(0), PrintChar::Copy, newline)
459}
460
461pub fn input_field_scrollable(ilist: &mut InputList, newline: bool) -> String {
462    input_field(ilist, PrintChar::Copy, newline)
463}
464
465#[cfg(test)]
466mod tests {
467    #[test]
468    fn it_works() {
469        assert_eq!(2 + 2, 4);
470    }
471
472    #[test]
473    fn string_to_int0(){
474        let t: Option<u32> = super::string_to_value(&String::from("12981398"));
475        assert_eq!(t, Option::Some(12981398));
476    }
477
478    #[test]
479    fn string_to_int1(){
480        let t: Option<i32> = super::string_to_value(&String::from("-1234"));
481        assert_eq!(t, Option::Some(-1234));
482    }
483
484    #[test]
485    fn string_to_int2(){
486        let t: Option<u8> = super::string_to_value(&String::from("70000"));
487        assert_eq!(t, Option::None);
488    }
489
490    #[test]
491    fn string_to_int3(){
492        let t: Option<i32> = super::string_to_value(&String::from("23ohno23"));
493        assert_eq!(t, Option::None);
494    }
495
496    #[test]
497    fn string_to_float0(){
498        let t: Option<f32> = super::string_to_value(&String::from("34.5"));
499        assert_eq!(t, Option::Some(34.5));
500    }
501
502    #[test]
503    fn string_to_float1(){
504        let t: Option<f64> = super::string_to_value(&String::from("-0.00000000000001"));
505        assert_eq!(t, Option::Some(-0.00000000000001));
506    }
507
508    #[test]
509    fn string_to_bool0(){
510        assert!(super::string_to_bool(&String::from("yes")));
511    }
512
513    #[test]
514    fn string_to_bool1(){
515        let t: Option<bool> = super::string_to_value(&String::from("true"));
516        assert_eq!(t, Option::Some(true));
517    }
518
519    #[test]
520    fn string_to_bool2(){
521        let t: Option<bool> = super::string_to_value(&String::from("false"));
522        assert_eq!(t, Option::Some(false));
523    }
524}
525