bp3d_os/shell/
core.rs

1// Copyright (c) 2025, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above copyright notice,
11//       this list of conditions and the following disclaimer in the documentation
12//       and/or other materials provided with the distribution.
13//     * Neither the name of BlockProject 3D nor the names of its contributors
14//       may be used to endorse or promote products derived from this software
15//       without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29use crate::shell::input_thread::{input_thread, InputEvent};
30use crate::shell::os::{clear_remaining, get_window_size, move_cursor, write, Terminal};
31use crate::shell_println;
32use std::sync::mpsc;
33use std::thread::JoinHandle;
34
35//FIXME: The shift key is broken under windows.
36
37/// Represents an event emitted from the input abstraction.
38pub enum Event {
39    /// A command string was submitted to the application.
40    CommandReceived(String),
41
42    /// Application exit was requested.
43    ExitRequested,
44}
45
46/// Represents any object which can be used to send [Event] structures.
47pub trait SendChannel: Send + 'static {
48    /// Sends an [Event] to the underlying application.
49    ///
50    /// # Arguments
51    ///
52    /// * `event`: the event to send.
53    ///
54    /// returns: ()
55    fn send(&self, event: Event);
56}
57
58/// Represents an interactive shell
59pub struct Shell {
60    _os: Terminal,
61    input_thread: JoinHandle<()>,
62    app_thread: JoinHandle<()>,
63    _send_ch: mpsc::Sender<InputEvent>,
64}
65
66fn print_prompt(row: i32, prompt: &'static str) {
67    move_cursor(0, row);
68    write(prompt);
69    move_cursor(prompt.len() as _, row);
70}
71
72enum Window {
73    StartEnd(usize, usize),
74    Start(usize),
75    Full,
76}
77
78fn string_window(pos: usize, col: i32, prompt: &'static str, str: &str) -> Window {
79    let maxsize = col as usize - prompt.len() - 1;
80    if str.len() > maxsize {
81        if pos >= str.len() {
82            Window::Start(str.len() - maxsize)
83        } else {
84            let mut start = pos;
85            let mut end = pos + maxsize;
86            while end >= str.len() {
87                end -= 1;
88                start = start.saturating_sub(1);
89            }
90            Window::StartEnd(start, end)
91        }
92    } else {
93        Window::Full
94    }
95}
96
97fn reset_string(pos: usize, col: i32, row: i32, prompt: &'static str, str: &str) {
98    print_prompt(row, prompt);
99    let window = string_window(pos, col, prompt, str);
100    match window {
101        Window::Start(start) => write(&str[start..]),
102        Window::StartEnd(start, end) => write(&str[start..end]),
103        Window::Full => write(str),
104    }
105    clear_remaining();
106}
107
108fn move_to_pos(pos: usize, col: i32, row: i32, prompt: &'static str, str: &str) {
109    let window = string_window(pos, col, prompt, str);
110    match window {
111        Window::StartEnd(start, _) => move_cursor((prompt.len() + (pos - start)) as _, row),
112        Window::Start(start) => move_cursor((prompt.len() + (pos - start)) as _, row),
113        Window::Full => move_cursor((prompt.len() + pos) as _, row),
114    }
115}
116
117fn application_thread<T: SendChannel>(
118    prompt: &'static str,
119    recv_ch: mpsc::Receiver<InputEvent>,
120    master_send_ch: T,
121) {
122    let mut history = Vec::new();
123    let mut hindex = 0;
124    let mut cur_line = String::new();
125    let (col, row) = get_window_size();
126    let mut pos = 0;
127    print_prompt(row, prompt);
128    loop {
129        let msg = recv_ch.recv().unwrap();
130        match msg {
131            InputEvent::End => {
132                master_send_ch.send(Event::ExitRequested);
133                break;
134            }
135            InputEvent::NewLine => {
136                write("\n");
137                print_prompt(row, prompt);
138                history.push(cur_line.clone());
139                hindex = history.len();
140                master_send_ch.send(Event::CommandReceived(cur_line.clone()));
141                cur_line.clear();
142                pos = 0;
143            }
144            InputEvent::Complete => {
145                shell_println!("Not yet implemented");
146            }
147            InputEvent::HistoryPrev => {
148                if history.is_empty() {
149                    continue;
150                }
151                hindex = hindex.saturating_sub(1);
152                let msg = &history[hindex];
153                cur_line = msg.clone();
154                pos = cur_line.len();
155                reset_string(pos, col, row, prompt, &cur_line);
156            }
157            InputEvent::HistoryNext => {
158                if history.is_empty() {
159                    continue;
160                }
161                if hindex != history.len() {
162                    hindex += 1;
163                }
164                if hindex == history.len() {
165                    reset_string(0, col, row, prompt, "");
166                    cur_line.clear();
167                    pos = 0;
168                    continue;
169                }
170                let msg = &history[hindex];
171                cur_line = msg.clone();
172                pos = cur_line.len();
173                reset_string(pos, col, row, prompt, &cur_line);
174            }
175            InputEvent::LineStart => {
176                pos = 0;
177                reset_string(pos, col, row, prompt, &cur_line);
178                move_to_pos(pos, col, row, prompt, &cur_line);
179            }
180            InputEvent::LineEnd => {
181                pos = cur_line.len();
182                reset_string(pos, col, row, prompt, &cur_line);
183                move_to_pos(pos, col, row, prompt, &cur_line);
184            }
185            InputEvent::Input(s) => {
186                cur_line.insert_str(pos, &s);
187                pos += s.len();
188                reset_string(pos, col, row, prompt, &cur_line);
189                move_to_pos(pos, col, row, prompt, &cur_line);
190            }
191            InputEvent::Left => {
192                if pos == 0 {
193                    continue;
194                }
195                pos -= 1;
196                reset_string(pos, col, row, prompt, &cur_line);
197                move_to_pos(pos, col, row, prompt, &cur_line);
198            }
199            InputEvent::Right => {
200                if pos >= cur_line.len() {
201                    continue;
202                }
203                pos += 1;
204                reset_string(pos, col, row, prompt, &cur_line);
205                move_to_pos(pos, col, row, prompt, &cur_line);
206            }
207            InputEvent::Delete => {
208                if pos == 0 {
209                    continue;
210                }
211                cur_line.remove(pos - 1);
212                pos -= 1;
213                reset_string(pos, col, row, prompt, &cur_line);
214                move_to_pos(pos, col, row, prompt, &cur_line);
215            }
216        }
217    }
218}
219
220impl Shell {
221    /// Creates a new interactive shell type application.
222    ///
223    /// This internally creates the [Terminal] instance to set up the OS terminal properly.
224    ///
225    /// # Arguments
226    ///
227    /// * `prompt`: a static prompt string to display as input prefix.
228    /// * `master_send_ch`: the master channel where application events should be submitted.
229    ///
230    /// returns: Shell
231    pub fn new<T: SendChannel>(prompt: &'static str, master_send_ch: T) -> Self {
232        let (send_ch, recv_ch) = mpsc::channel();
233        let motherfuckingrust = send_ch.clone();
234        let input_thread = std::thread::spawn(|| {
235            input_thread(motherfuckingrust);
236        });
237        let app_thread = std::thread::spawn(move || {
238            application_thread(prompt, recv_ch, master_send_ch);
239        });
240        Self {
241            _os: Terminal::new(),
242            input_thread,
243            app_thread,
244            _send_ch: send_ch,
245        }
246    }
247
248    /// Gracefully exits this interactive shell.
249    pub fn exit(self) {
250        // Should interrupt the syscall and make the syscall return -1.
251        #[cfg(unix)]
252        {
253            // Use SIGUSR2 because SIGUSR1 is reserved for application use.
254            use std::os::unix::thread::JoinHandleExt;
255
256            // Attach to SIGUSR2 an empty function to use the EINTR syscall error.
257            extern "C" fn useless() {}
258            let mut sig2: std::mem::MaybeUninit<libc::sigaction> = std::mem::MaybeUninit::uninit();
259            let mut sig: libc::sigaction = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
260            sig.sa_sigaction = useless as _;
261            unsafe { libc::sigaction(libc::SIGUSR2, &sig as _, sig2.as_mut_ptr()) };
262
263            // Send a signal to the input thread which should raise EINTR on the getchar function.
264            let pthread = self.input_thread.as_pthread_t();
265            unsafe { libc::pthread_kill(pthread, libc::SIGUSR2) };
266
267            // Join the threads.
268            self.input_thread.join().unwrap();
269            self.app_thread.join().unwrap();
270
271            // Reset the previous action attached to SIGUSR2 in case the application would be using
272            // that particular signal.
273            unsafe { libc::sigaction(libc::SIGUSR2, sig2.as_ptr(), std::ptr::null_mut()) };
274        }
275        #[cfg(windows)]
276        {
277            // Cancel all pending IO operations on standard input.
278            let handle = unsafe {
279                windows_sys::Win32::System::Console::GetStdHandle(
280                    windows_sys::Win32::System::Console::STD_INPUT_HANDLE,
281                )
282            };
283            unsafe { windows_sys::Win32::System::IO::CancelIoEx(handle, std::ptr::null()) };
284
285            // Join the threads.
286            self.input_thread.join().unwrap();
287            self.app_thread.join().unwrap();
288        }
289    }
290}