veloren/server-cli/src/tui_runner.rs
Joshua Yanovski e7587c4d9d Added non-admin moderators and timed bans.
The security model has been updated to reflect this change (for example,
moderators cannot revert a ban by an administrator).  Ban history is
also now recorded in the ban file, and much more information about the
ban is stored (whitelists and administrators also have extra
information).

To support the new information without losing important information,
this commit also introduces a new migration path for editable settings
(both from legacy to the new format, and between versions).  Examples
of how to do this correctly, and migrate to new versions of a settings
file, are in the settings/ subdirectory.

As part of this effort, editable settings have been revamped to
guarantee atomic saves (due to the increased amount of information in
each file), some latent bugs in networking were fixed, and server-cli
has been updated to go through StructOpt for both calls through TUI
and argv, greatly simplifying parsing logic.
2021-05-09 21:19:16 -07:00

193 lines
5.9 KiB
Rust

use crate::{cli, Message, Shutdown, LOG};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
io,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
time::Duration,
};
use tracing::{debug, error, warn};
use tui::{
backend::CrosstermBackend,
layout::Rect,
text::Text,
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
pub struct Tui {
pub msg_r: mpsc::Receiver<Message>,
background: Option<std::thread::JoinHandle<()>>,
basic: bool,
running: Arc<AtomicBool>,
}
impl Tui {
fn handle_events(input: &mut String, msg_s: &mut mpsc::Sender<Message>) {
use crossterm::event::*;
if let Event::Key(event) = read().unwrap() {
match event.code {
KeyCode::Char('c') => {
if event.modifiers.contains(KeyModifiers::CONTROL) {
msg_s
.send(Message::Shutdown {
command: Shutdown::Immediate,
})
.unwrap()
} else {
input.push('c');
}
},
KeyCode::Char(c) => input.push(c),
KeyCode::Backspace => {
input.pop();
},
KeyCode::Enter => {
debug!(?input, "tui mode: command entered");
cli::parse_command(input, msg_s);
*input = String::new();
},
_ => {},
}
}
}
pub fn run(basic: bool) -> Self {
let (msg_s, msg_r) = mpsc::channel();
let running = Arc::new(AtomicBool::new(true));
let running2 = Arc::clone(&running);
let builder = std::thread::Builder::new().name("tui_runner".to_owned());
let background = if basic {
builder.spawn(|| Self::work_b(running2, msg_s)).unwrap();
None
} else {
Some(builder.spawn(|| Self::work_e(running2, msg_s)).unwrap())
};
Self {
msg_r,
background,
basic,
running,
}
}
/// In a seperate Thread
fn work_b(running: Arc<AtomicBool>, mut msg_s: mpsc::Sender<Message>) {
while running.load(Ordering::Relaxed) {
let mut line = String::new();
match io::stdin().read_line(&mut line) {
Err(e) => {
error!(
?e,
"couldn't read from stdin, cli commands are disabled now!"
);
break;
},
Ok(0) => {
//Docker seem to send EOF all the time
warn!("EOF received, cli commands are disabled now!");
break;
},
Ok(_) => {
debug!(?line, "basic mode: command entered");
crate::cli::parse_command(&line, &mut msg_s);
},
}
}
}
/// In a seperate Thread
fn work_e(running: Arc<AtomicBool>, mut msg_s: mpsc::Sender<Message>) {
// Start the tui
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
enable_raw_mode().unwrap();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
let mut input = String::new();
if let Err(e) = terminal.clear() {
error!(?e, "couldn't clean terminal");
};
while running.load(Ordering::Relaxed) {
if let Err(e) = terminal.draw(|f| {
let (log_rect, input_rect) = if f.size().height > 6 {
let mut log_rect = f.size();
log_rect.height -= 3;
let mut input_rect = f.size();
input_rect.y = input_rect.height - 3;
input_rect.height = 3;
(log_rect, input_rect)
} else {
(f.size(), Rect::default())
};
let block = Block::default().borders(Borders::ALL);
let wrap = Wrap {
scroll_callback: Some(Box::new(|text_area, lines| {
LOG.resize(text_area.height as usize);
let len = lines.len() as u16;
(len.saturating_sub(text_area.height), 0)
})),
..Default::default()
};
let logger = Paragraph::new(LOG.inner.lock().unwrap().clone())
.block(block)
.wrap(wrap);
f.render_widget(logger, log_rect);
let text: Text = input.as_str().into();
let block = Block::default().borders(Borders::ALL);
let size = block.inner(input_rect);
let x = (size.x + text.width() as u16).min(size.width);
let input_field = Paragraph::new(text).block(block);
f.render_widget(input_field, input_rect);
f.set_cursor(x, size.y);
}) {
warn!(?e, "couldn't draw frame");
};
if crossterm::event::poll(Duration::from_millis(100)).unwrap() {
Self::handle_events(&mut input, &mut msg_s);
};
}
}
pub fn shutdown(basic: bool) {
if !basic {
let mut stdout = io::stdout();
execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap();
disable_raw_mode().unwrap();
}
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
self.background.take().map(|m| m.join());
Tui::shutdown(self.basic);
}
}