veloren_server_cli/
tui_runner.rs1use 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 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 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 fn work_e(running: Arc<AtomicBool>, mut msg_s: mpsc::Sender<Message>) {
112 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}