erg_common/
stdin.rs

1use std::sync::OnceLock;
2
3#[cfg(not(feature = "full-repl"))]
4use std::io::{stdin, BufRead, BufReader};
5
6#[cfg(feature = "full-repl")]
7use crossterm::{
8    cursor::MoveToColumn,
9    event::{read, Event, KeyCode, KeyEvent, KeyModifiers},
10    execute,
11    style::Print,
12    terminal::{disable_raw_mode, enable_raw_mode},
13    terminal::{Clear, ClearType},
14};
15#[cfg(feature = "full-repl")]
16use std::process::Command;
17#[cfg(feature = "full-repl")]
18use std::process::Output;
19
20use crate::shared::Shared;
21
22/// e.g.
23/// ```erg
24/// >>> print! 1
25/// >>>
26/// >>> while! False, do!:
27/// >>>    print! ""
28/// >>>
29/// ```
30/// ↓
31///
32/// `{ lineno: 5, buf: ["print! 1\n", "\n", "while! False, do!:\n", "print! \"\"\n", "\n"] }`
33#[derive(Debug)]
34pub struct StdinReader {
35    block_begin: usize,
36    lineno: usize,
37    buf: Vec<String>,
38    #[cfg(feature = "full-repl")]
39    history_input_position: usize,
40    indent: u16,
41}
42
43impl StdinReader {
44    #[cfg(all(feature = "full-repl", target_os = "linux"))]
45    fn access_clipboard() -> Option<Output> {
46        if let Ok(str) = std::fs::read("/proc/sys/kernel/osrelease") {
47            if let Ok(str) = std::str::from_utf8(&str) {
48                if str.to_ascii_lowercase().contains("microsoft") {
49                    return Some(
50                        Command::new("powershell")
51                            .args(["get-clipboard"])
52                            .output()
53                            .expect("failed to get clipboard"),
54                    );
55                }
56            }
57        }
58        match Command::new("xsel")
59            .args(["--output", "--clipboard"])
60            .output()
61        {
62            Ok(output) => Some(output),
63            Err(_) => {
64                execute!(
65                    std::io::stdout(),
66                    Print("You need to install `xsel` to use the paste feature on Linux desktop"),
67                )
68                .unwrap();
69                None
70            }
71        }
72    }
73    #[cfg(all(feature = "full-repl", target_os = "macos"))]
74    fn access_clipboard() -> Option<Output> {
75        Some(
76            Command::new("pbpast")
77                .output()
78                .expect("failed to get clipboard"),
79        )
80    }
81
82    #[cfg(all(feature = "full-repl", target_os = "windows"))]
83    fn access_clipboard() -> Option<Output> {
84        Some(
85            Command::new("powershell")
86                .args(["get-clipboard"])
87                .output()
88                .expect("failed to get clipboard"),
89        )
90    }
91
92    #[cfg(not(feature = "full-repl"))]
93    pub fn read(&mut self) -> String {
94        let mut line = "".to_string();
95        let stdin = stdin();
96        let mut reader = BufReader::new(stdin.lock());
97        reader.read_line(&mut line).unwrap();
98        self.lineno += 1;
99        self.buf.push(line.trim_end().to_string());
100        self.buf.last().cloned().unwrap_or_default()
101    }
102
103    #[cfg(feature = "full-repl")]
104    pub fn read(&mut self) -> String {
105        enable_raw_mode().unwrap();
106        let mut output = std::io::stdout();
107        let mut line = String::new();
108        self.input(&mut line).unwrap();
109        disable_raw_mode().unwrap();
110        execute!(output, MoveToColumn(0)).unwrap();
111        self.lineno += 1;
112        self.buf.push(line);
113        self.buf.last().cloned().unwrap_or_default()
114    }
115
116    #[cfg(feature = "full-repl")]
117    fn input(&mut self, line: &mut String) -> std::io::Result<()> {
118        let mut position = 0;
119        let mut consult_history = false;
120        let mut stdout = std::io::stdout();
121        while let Event::Key(KeyEvent {
122            code, modifiers, ..
123        }) = read()?
124        {
125            consult_history = false;
126            match (code, modifiers) {
127                (KeyCode::Char('z'), KeyModifiers::CONTROL)
128                | (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
129                    println!();
130                    line.clear();
131                    line.push_str(":exit");
132                    return Ok(());
133                }
134                (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
135                    let output = match Self::access_clipboard() {
136                        None => {
137                            continue;
138                        }
139                        Some(output) => output,
140                    };
141                    let clipboard = {
142                        let this = String::from_utf8_lossy(&output.stdout).to_string();
143                        this.trim_matches(|c: char| c.is_whitespace())
144                            .to_string()
145                            .replace(['\n', '\r'], "")
146                            .replace(|c: char| c.len_utf8() >= 2, "")
147                    };
148                    line.insert_str(position, &clipboard);
149                    position += clipboard.len();
150                }
151                (_, KeyModifiers::CONTROL) => continue,
152                (KeyCode::Tab, _) => {
153                    line.insert_str(position, "    ");
154                    position += 4;
155                }
156                (KeyCode::Home, _) => {
157                    position = 0;
158                }
159                (KeyCode::End, _) => {
160                    position = line.len();
161                }
162                (KeyCode::Backspace, _) => {
163                    if position == 0 {
164                        continue;
165                    }
166                    line.remove(position - 1);
167                    position -= 1;
168                }
169                (KeyCode::Delete, _) => {
170                    if position == line.len() {
171                        continue;
172                    }
173                    line.remove(position);
174                }
175                (KeyCode::Up, _) => {
176                    consult_history = true;
177                    if self.history_input_position == 0 {
178                        continue;
179                    }
180                    self.history_input_position -= 1;
181                    execute!(stdout, MoveToColumn(4), Clear(ClearType::UntilNewLine))?;
182                    if let Some(l) = self.buf.get(self.history_input_position) {
183                        position = l.len();
184                        line.clear();
185                        line.push_str(l);
186                    }
187                }
188                (KeyCode::Down, _) => {
189                    if self.history_input_position == self.buf.len() {
190                        continue;
191                    }
192                    if self.history_input_position == self.buf.len() - 1 {
193                        *line = "".to_string();
194                        position = 0;
195                        self.history_input_position += 1;
196                        execute!(
197                            stdout,
198                            MoveToColumn(4),
199                            Clear(ClearType::UntilNewLine),
200                            MoveToColumn(self.indent * 4),
201                            Print(line.to_owned()),
202                            MoveToColumn(self.indent * 4 + position as u16)
203                        )?;
204                        continue;
205                    }
206                    self.history_input_position += 1;
207                    execute!(stdout, MoveToColumn(4), Clear(ClearType::UntilNewLine))?;
208                    if let Some(l) = self.buf.get(self.history_input_position) {
209                        position = l.len();
210                        line.clear();
211                        line.push_str(l);
212                    }
213                }
214                (KeyCode::Left, _) => {
215                    if position == 0 {
216                        continue;
217                    }
218                    position -= 1;
219                }
220                (KeyCode::Right, _) => {
221                    if position == line.len() {
222                        continue;
223                    }
224                    position += 1;
225                }
226                (KeyCode::Enter, _) => {
227                    println!();
228                    break;
229                }
230                // TODO: check a full-width char and possible to insert
231                (KeyCode::Char(c), _) if c.len_utf8() < 2 => {
232                    line.insert(position, c);
233                    position += 1;
234                }
235                _ => {}
236            }
237            execute!(
238                stdout,
239                MoveToColumn(4),
240                Clear(ClearType::UntilNewLine),
241                MoveToColumn(self.indent * 4),
242                Print(line.to_owned()),
243                MoveToColumn(self.indent * 4 + position as u16)
244            )?;
245        }
246        if !consult_history {
247            self.history_input_position = self.buf.len() + 1;
248        }
249        Ok(())
250    }
251
252    pub fn reread(&self) -> String {
253        self.buf.last().cloned().unwrap_or_default()
254    }
255
256    pub fn reread_lines(&self, ln_begin: usize, ln_end: usize) -> Vec<String> {
257        if let Some(lines) = self.buf.get(ln_begin - 1..=ln_end - 1) {
258            lines.to_vec()
259        } else {
260            self.buf.clone()
261        }
262    }
263
264    pub fn last_line(&mut self) -> Option<&mut String> {
265        self.buf.last_mut()
266    }
267}
268
269#[derive(Debug)]
270pub struct GlobalStdin(OnceLock<Shared<StdinReader>>);
271
272pub static GLOBAL_STDIN: GlobalStdin = GlobalStdin(OnceLock::new());
273
274impl GlobalStdin {
275    fn get(&'static self) -> &'static Shared<StdinReader> {
276        self.0.get_or_init(|| {
277            Shared::new(StdinReader {
278                block_begin: 1,
279                lineno: 1,
280                buf: vec![],
281                #[cfg(feature = "full-repl")]
282                history_input_position: 1,
283                indent: 1,
284            })
285        })
286    }
287
288    pub fn read(&'static self) -> String {
289        self.get().borrow_mut().read()
290    }
291
292    pub fn reread(&'static self) -> String {
293        self.get().borrow_mut().reread()
294    }
295
296    pub fn reread_lines(&'static self, ln_begin: usize, ln_end: usize) -> Vec<String> {
297        self.get().borrow_mut().reread_lines(ln_begin, ln_end)
298    }
299
300    pub fn lineno(&'static self) -> usize {
301        self.get().borrow_mut().lineno
302    }
303
304    pub fn block_begin(&'static self) -> usize {
305        self.get().borrow_mut().block_begin
306    }
307
308    pub fn set_block_begin(&'static self, n: usize) {
309        self.get().borrow_mut().block_begin = n;
310    }
311
312    pub fn set_indent(&'static self, n: usize) {
313        self.get().borrow_mut().indent = n as u16;
314    }
315
316    pub fn insert_whitespace(&'static self, whitespace: &str) {
317        if let Some(line) = self.get().borrow_mut().last_line() {
318            line.insert_str(0, whitespace);
319        }
320    }
321}