veloren_server_cli/
tui_runner.rs

1use crate::{LOG, Message, Shutdown, cli};
2use crossterm::{
3    event::{DisableMouseCapture, EnableMouseCapture},
4    execute,
5    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
6};
7use ratatui::{
8    Terminal,
9    backend::CrosstermBackend,
10    layout::{Position, Rect},
11    text::Text,
12    widgets::{Block, Borders, Paragraph},
13};
14use std::{
15    io,
16    sync::{
17        Arc,
18        atomic::{AtomicBool, Ordering},
19        mpsc,
20    },
21    time::Duration,
22};
23use tracing::{debug, error, warn};
24
25pub struct Tui {
26    pub msg_r: mpsc::Receiver<Message>,
27    background: Option<std::thread::JoinHandle<()>>,
28    basic: bool,
29    running: Arc<AtomicBool>,
30}
31
32impl Tui {
33    fn handle_events(input: &mut String, msg_s: &mut mpsc::Sender<Message>) {
34        use crossterm::event::*;
35        if let Event::Key(event) = read().unwrap() {
36            match event.code {
37                KeyCode::Char('c') => {
38                    if event.modifiers.contains(KeyModifiers::CONTROL) {
39                        msg_s
40                            .send(Message::Shutdown {
41                                command: Shutdown::Immediate,
42                            })
43                            .unwrap()
44                    } else {
45                        input.push('c');
46                    }
47                },
48                KeyCode::Char(c) => input.push(c),
49                KeyCode::Backspace => {
50                    input.pop();
51                },
52                KeyCode::Enter => {
53                    debug!(?input, "tui mode: command entered");
54                    cli::parse_command(input, msg_s);
55
56                    *input = String::new();
57                },
58                _ => {},
59            }
60        }
61    }
62
63    pub fn run(basic: bool) -> Self {
64        let (msg_s, msg_r) = mpsc::channel();
65        let running = Arc::new(AtomicBool::new(true));
66        let running2 = Arc::clone(&running);
67
68        let builder = std::thread::Builder::new().name("tui_runner".to_owned());
69        let background = if basic {
70            builder.spawn(|| Self::work_b(running2, msg_s)).unwrap();
71            None
72        } else {
73            Some(builder.spawn(|| Self::work_e(running2, msg_s)).unwrap())
74        };
75
76        Self {
77            msg_r,
78            background,
79            basic,
80            running,
81        }
82    }
83
84    /// In a seperate Thread
85    fn work_b(running: Arc<AtomicBool>, mut msg_s: mpsc::Sender<Message>) {
86        while running.load(Ordering::Relaxed) {
87            let mut line = String::new();
88
89            match io::stdin().read_line(&mut line) {
90                Err(e) => {
91                    error!(
92                        ?e,
93                        "couldn't read from stdin, cli commands are disabled now!"
94                    );
95                    break;
96                },
97                Ok(0) => {
98                    //Docker seem to send EOF all the time
99                    warn!("EOF received, cli commands are disabled now!");
100                    break;
101                },
102                Ok(_) => {
103                    debug!(?line, "basic mode: command entered");
104                    cli::parse_command(line.trim(), &mut msg_s);
105                },
106            }
107        }
108    }
109
110    /// In a seperate Thread
111    fn work_e(running: Arc<AtomicBool>, mut msg_s: mpsc::Sender<Message>) {
112        // Start the tui
113        let mut stdout = io::stdout();
114        execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
115
116        enable_raw_mode().unwrap();
117
118        let backend = CrosstermBackend::new(stdout);
119        let mut terminal = Terminal::new(backend).unwrap();
120
121        let mut input = String::new();
122
123        if let Err(e) = terminal.clear() {
124            error!(?e, "couldn't clean terminal");
125        };
126
127        while running.load(Ordering::Relaxed) {
128            if let Err(e) = terminal.draw(|f| {
129                let (log_rect, input_rect) = if f.area().height > 6 {
130                    let mut log_rect = f.area();
131                    log_rect.height -= 3;
132
133                    let mut input_rect = f.area();
134                    input_rect.y = input_rect.height - 3;
135                    input_rect.height = 3;
136
137                    (log_rect, input_rect)
138                } else {
139                    (f.area(), Rect::default())
140                };
141
142                let block = Block::default().borders(Borders::ALL);
143
144                LOG.resize(log_rect.height as usize);
145                let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()).block(block);
146                f.render_widget(logger, log_rect);
147
148                let text: Text = input.as_str().into();
149
150                let block = Block::default().borders(Borders::ALL);
151                let size = block.inner(input_rect);
152
153                let x = (size.x + text.width() as u16).min(size.width);
154
155                let input_field = Paragraph::new(text).block(block);
156                f.render_widget(input_field, input_rect);
157
158                f.set_cursor_position(Position { x, y: size.y });
159            }) {
160                warn!(?e, "couldn't draw frame");
161            };
162            if crossterm::event::poll(Duration::from_millis(100)).unwrap() {
163                Self::handle_events(&mut input, &mut msg_s);
164            };
165        }
166    }
167
168    pub fn shutdown(basic: bool) {
169        if !basic {
170            let mut stdout = io::stdout();
171            execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap();
172            disable_raw_mode().unwrap();
173        }
174    }
175}
176
177impl Drop for Tui {
178    fn drop(&mut self) {
179        self.running.store(false, Ordering::Relaxed);
180        self.background.take().map(|m| m.join());
181        Tui::shutdown(self.basic);
182    }
183}