From c399afa7c51616c4ca8539bc4af839ad63a3dd0e Mon Sep 17 00:00:00 2001 From: Capucho Date: Mon, 27 Jul 2020 17:26:34 +0100 Subject: [PATCH 01/16] Made the server cli great --- Cargo.lock | 141 ++++++++++++++++++++++++++++++++++--- server-cli/Cargo.toml | 3 + server-cli/src/main.rs | 153 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 282 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 818e711135..fdef68c4cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arc-swap" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" + [[package]] name = "arr_macro" version = "0.1.3" @@ -170,7 +176,7 @@ dependencies = [ "kv-log-macro", "log", "memchr", - "mio", + "mio 0.6.22", "mio-uds", "num_cpus", "once_cell", @@ -440,11 +446,17 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aa2097be53a00de9e8fc349fea6d76221f398f5c4fa550d420669906962d160" dependencies = [ - "mio", + "mio 0.6.22", "mio-extras", "nix 0.14.1", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.2.3" @@ -950,6 +962,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio 0.7.0", + "parking_lot 0.10.2", + "signal-hook", + "winapi 0.3.8", +] + +[[package]] +name = "crossterm_winapi" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "csv" version = "1.1.3" @@ -2527,12 +2564,26 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.1", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9971bc8349a361217a8f2a41f5d011274686bd4436465ba51730921039d7fb" +dependencies = [ + "lazy_static", + "libc", + "log", + "miow 0.3.5", + "ntapi", + "winapi 0.3.8", +] + [[package]] name = "mio-extras" version = "2.0.6" @@ -2541,7 +2592,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" dependencies = [ "lazycell", "log", - "mio", + "mio 0.6.22", "slab", ] @@ -2553,7 +2604,7 @@ checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ "iovec", "libc", - "mio", + "mio 0.6.22", ] [[package]] @@ -2568,6 +2619,16 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.8", +] + [[package]] name = "mopa" version = "0.2.2" @@ -2702,12 +2763,21 @@ dependencies = [ "fsevent-sys", "inotify", "libc", - "mio", + "mio 0.6.22", "mio-extras", "walkdir", "winapi 0.3.8", ] +[[package]] +name = "ntapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "num" version = "0.1.42" @@ -3909,6 +3979,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5752e017e03af9d735b4b069f53b7a7fd90fefafa04d8bd0c25581b0bff437f" +[[package]] +name = "signal-hook" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" +dependencies = [ + "libc", + "mio 0.7.0", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +dependencies = [ + "arc-swap", + "libc", +] + [[package]] name = "slab" version = "0.4.2" @@ -3956,6 +4047,18 @@ dependencies = [ "smithay-client-toolkit", ] +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + [[package]] name = "specs" version = "0.16.1" @@ -4266,7 +4369,7 @@ checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" dependencies = [ "bytes", "futures 0.1.29", - "mio", + "mio 0.6.22", "num_cpus", "tokio-current-thread", "tokio-executor", @@ -4328,7 +4431,7 @@ dependencies = [ "futures 0.1.29", "lazy_static", "log", - "mio", + "mio 0.6.22", "num_cpus", "parking_lot 0.9.0", "slab", @@ -4356,7 +4459,7 @@ dependencies = [ "bytes", "futures 0.1.29", "iovec", - "mio", + "mio 0.6.22", "tokio-io", "tokio-reactor", ] @@ -4517,6 +4620,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +[[package]] +name = "tui" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a977b0bb2e2033a6fef950f218f13622c3c34e59754b704ce3492dedab1dfe95" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "tuple_utils" version = "0.3.0" @@ -4785,8 +4901,11 @@ dependencies = [ name = "veloren-server-cli" version = "0.7.0" dependencies = [ + "crossterm", + "lazy_static", "tracing", "tracing-subscriber", + "tui", "veloren-common", "veloren-server", ] @@ -5025,7 +5144,7 @@ dependencies = [ "calloop", "downcast-rs", "libc", - "mio", + "mio 0.6.22", "nix 0.14.1", "wayland-commons", "wayland-scanner", @@ -5162,7 +5281,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio", + "mio 0.6.22", "mio-extras", "ndk", "ndk-glue", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 93ab4b095b..207caf9dfb 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -14,3 +14,6 @@ common = { package = "veloren-common", path = "../common" } tracing = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.2.3", default-features = false, features = ["env-filter", "fmt", "chrono", "ansi", "smallvec"] } +crossterm = "0.17" +tui = { version = "0.10", default-features = false, features = ['crossterm'] } +lazy_static = "1" diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 3668f8c97a..d5769a1307 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -1,15 +1,33 @@ #![deny(unsafe_code)] +#[macro_use] extern crate lazy_static; + use common::clock::Clock; use server::{Event, Input, Server, ServerSettings}; -use std::time::Duration; -use tracing::{info, Level}; +use tracing::{error, info, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; +use std::{ + io::{self, Write}, + sync::{mpsc, Arc, Mutex}, + time::Duration, +}; +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + text::Text, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + const TPS: u64 = 30; const RUST_LOG_ENV: &str = "RUST_LOG"; -fn main() { +lazy_static! { + static ref LOG: TuiLog<'static> = TuiLog::default(); +} + +fn main() -> io::Result<()> { // Init logging let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { Some(Ok(env)) => { @@ -33,8 +51,87 @@ fn main() { FmtSubscriber::builder() .with_max_level(Level::ERROR) .with_env_filter(filter) + .with_writer(|| LOG.clone()) .init(); + let (sender, receiver) = mpsc::channel(); + + std::thread::spawn(move || { + // Start the tui + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + + crossterm::terminal::enable_raw_mode().unwrap(); + + let mut input = String::new(); + + let _ = terminal.clear(); + + loop { + let _ = terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Max(u16::MAX), Constraint::Max(3)].as_ref()) + .split(f.size()); + + let block = Block::default().borders(Borders::ALL); + let size = block.inner(chunks[0]); + + LOG.resize(size.height as usize); + + let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) + .block(block) + .wrap(Wrap { trim: false }); + f.render_widget(logger, chunks[0]); + + let text: Text = input.as_str().into(); + + let block = Block::default().borders(Borders::ALL); + let size = block.inner(chunks[1]); + + let x = (size.x + text.width() as u16).min(size.width); + + let input_field = Paragraph::new(text).block(block); + f.render_widget(input_field, chunks[1]); + + f.set_cursor(x, size.y); + + use crossterm::event::{KeyModifiers, *}; + + if poll(Duration::from_millis(50)).unwrap() { + // It's guaranteed that the `read()` won't block when the `poll()` + // function returns `true` + match read().unwrap() { + Event::Key(event) => match event.code { + KeyCode::Char('c') => { + if event.modifiers.contains(KeyModifiers::CONTROL) { + sender.send(Message::Quit).unwrap() + } else { + input.push('c'); + } + }, + KeyCode::Char(c) => input.push(c), + KeyCode::Backspace => { + input.pop(); + }, + KeyCode::Enter => { + match input.as_str() { + "quit" => sender.send(Message::Quit).unwrap(), + _ => error!("invalid command"), + } + + input = String::new(); + }, + _ => {}, + }, + _ => {}, + } + } + }); + } + }); + info!("Starting server..."); // Set up an fps clock @@ -44,7 +141,6 @@ fn main() { let settings = ServerSettings::load(); let server_port = &settings.gameserver_address.port(); let metrics_port = &settings.metrics_address.port(); - // Create server let mut server = Server::new(settings).expect("Failed to create server instance!"); @@ -68,7 +164,56 @@ fn main() { // Clean up the server after a tick. server.cleanup(); + match receiver.try_recv() { + Ok(msg) => match msg { + Message::Quit => { + info!("Closing the server"); + break; + }, + }, + Err(e) => match e { + mpsc::TryRecvError::Empty => {}, + mpsc::TryRecvError::Disconnected => panic!(), + }, + }; + // Wait for the next tick. clock.tick(Duration::from_millis(1000 / TPS)); } + + Ok(()) +} + +#[derive(Debug, Default, Clone)] +struct TuiLog<'a> { + inner: Arc>>, +} + +impl<'a> TuiLog<'a> { + fn resize(&self, h: usize) { + let mut inner = self.inner.lock().unwrap(); + + if inner.height() > h { + let length = inner.height() - h; + inner.lines.drain(0..length); + } + } +} + +impl<'a> Write for TuiLog<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + let line = String::from_utf8(buf.into()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + self.inner.lock().unwrap().lines.push(From::from(line)); + + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} + +#[derive(Debug, Copy, Clone)] +enum Message { + Quit, } From a2d70e6ff6b1a16db7d4ce336c0f2e5591238d4b Mon Sep 17 00:00:00 2001 From: Capucho Date: Mon, 27 Jul 2020 22:09:33 +0100 Subject: [PATCH 02/16] Escape ansi sequences --- Cargo.lock | 10 +++++ server-cli/Cargo.toml | 1 + server-cli/src/main.rs | 84 ++++++++++++++++++++++++------------------ 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdef68c4cb..88637aa608 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8052e2d8aabbb8d556d6abbcce2a22b9590996c5f849b9c7ce4544a2e3b984e" +[[package]] +name = "ansi-parser" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761ac675f1638a6a49e26f6ac3a4067ca3fefa8029816ae4ef8d3fa3d06a5194" +dependencies = [ + "nom 4.2.3", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -4901,6 +4910,7 @@ dependencies = [ name = "veloren-server-cli" version = "0.7.0" dependencies = [ + "ansi-parser", "crossterm", "lazy_static", "tracing", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 207caf9dfb..2559651874 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -17,3 +17,4 @@ tracing-subscriber = { version = "0.2.3", default-features = false, features = [ crossterm = "0.17" tui = { version = "0.10", default-features = false, features = ['crossterm'] } lazy_static = "1" +ansi-parser = "0.6" diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index d5769a1307..70f44f65ce 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -27,6 +27,54 @@ lazy_static! { static ref LOG: TuiLog<'static> = TuiLog::default(); } +#[derive(Debug, Default, Clone)] +struct TuiLog<'a> { + inner: Arc>>, +} + +impl<'a> TuiLog<'a> { + fn resize(&self, h: usize) { + let mut inner = self.inner.lock().unwrap(); + + if inner.height() > h { + let length = inner.height() - h; + inner.lines.drain(0..length); + } + } +} + +impl<'a> Write for TuiLog<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + use ansi_parser::{AnsiParser, AnsiSequence, Output}; + use tui::text::{Span,Spans}; + + let line = String::from_utf8(buf.into()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let mut spans = Vec::new(); + + for out in line.ansi_parse() { + match out { + Output::TextBlock(text) => spans.push(text.to_string().into()), + Output::Escape(seq) => info!("{:?}",seq) + } + } + + self.inner.lock().unwrap().lines.push(Spans(spans)); + + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +enum Message { + Quit, +} + fn main() -> io::Result<()> { // Init logging let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { @@ -99,7 +147,7 @@ fn main() -> io::Result<()> { use crossterm::event::{KeyModifiers, *}; - if poll(Duration::from_millis(50)).unwrap() { + if poll(Duration::from_millis(10)).unwrap() { // It's guaranteed that the `read()` won't block when the `poll()` // function returns `true` match read().unwrap() { @@ -183,37 +231,3 @@ fn main() -> io::Result<()> { Ok(()) } - -#[derive(Debug, Default, Clone)] -struct TuiLog<'a> { - inner: Arc>>, -} - -impl<'a> TuiLog<'a> { - fn resize(&self, h: usize) { - let mut inner = self.inner.lock().unwrap(); - - if inner.height() > h { - let length = inner.height() - h; - inner.lines.drain(0..length); - } - } -} - -impl<'a> Write for TuiLog<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - let line = String::from_utf8(buf.into()) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - self.inner.lock().unwrap().lines.push(From::from(line)); - - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { Ok(()) } -} - -#[derive(Debug, Copy, Clone)] -enum Message { - Quit, -} From 83ff13a035e39ca3cbefcaa5b06a2d270f025c2d Mon Sep 17 00:00:00 2001 From: Capucho Date: Mon, 27 Jul 2020 22:49:34 +0100 Subject: [PATCH 03/16] Coloring done very badly --- server-cli/src/main.rs | 55 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 70f44f65ce..e5849560d4 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -46,28 +46,69 @@ impl<'a> TuiLog<'a> { impl<'a> Write for TuiLog<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { use ansi_parser::{AnsiParser, AnsiSequence, Output}; - use tui::text::{Span,Spans}; + use tui::{ + style::{Color, Modifier}, + text::{Span, Spans}, + }; let line = String::from_utf8(buf.into()) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let mut spans = Vec::new(); + let mut span = Span::raw(""); for out in line.ansi_parse() { match out { - Output::TextBlock(text) => spans.push(text.to_string().into()), - Output::Escape(seq) => info!("{:?}",seq) + Output::TextBlock(text) => { + span.content = format!("{}{}", span.content.to_owned(), text).into() + }, + Output::Escape(seq) => { + if span.content.len() != 0 { + spans.push(span); + + span = Span::raw(""); + } + + match seq { + AnsiSequence::SetGraphicsMode(values) => { + const COLOR_TABLE: [Color; 8] = [ + Color::Black, + Color::Red, + Color::Green, + Color::Yellow, + Color::Blue, + Color::Magenta, + Color::Cyan, + Color::White, + ]; + + let mut iter = values.iter(); + + match iter.next().unwrap() { + 0 => {}, + 2 => span.style.add_modifier = Modifier::DIM, + idx @ 30..=37 => { + span.style.fg = Some(COLOR_TABLE[(idx - 30) as usize]) + }, + _ => println!("{:#?}", values), + } + }, + _ => println!("{:#?}", seq), + } + }, } } + if span.content.len() != 0 { + spans.push(span); + } + self.inner.lock().unwrap().lines.push(Spans(spans)); Ok(buf.len()) } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } + fn flush(&mut self) -> io::Result<()> { Ok(()) } } #[derive(Debug, Copy, Clone)] @@ -148,8 +189,6 @@ fn main() -> io::Result<()> { use crossterm::event::{KeyModifiers, *}; if poll(Duration::from_millis(10)).unwrap() { - // It's guaranteed that the `read()` won't block when the `poll()` - // function returns `true` match read().unwrap() { Event::Key(event) => match event.code { KeyCode::Char('c') => { From 6a83da0dfd85f20eaa0d48509e929544c5648a19 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 10:26:22 +0100 Subject: [PATCH 04/16] Force migration text trough tracing --- server/src/persistence/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 8047372312..70afd430fe 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -17,7 +17,7 @@ extern crate diesel; use diesel::{connection::SimpleConnection, prelude::*}; use diesel_migrations::embed_migrations; use std::{env, fs, path::PathBuf}; -use tracing::warn; +use tracing::{info, warn}; // See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html // This macro is called at build-time, and produces the necessary migration info @@ -27,16 +27,28 @@ use tracing::warn; // when needed. embed_migrations!(); +struct TracingOut; + +impl std::io::Write for TracingOut { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + info!("{}", String::from_utf8_lossy(buf)); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } +} + /// Runs any pending database migrations. This is executed during server startup pub fn run_migrations(db_dir: &str) -> Result<(), diesel_migrations::RunMigrationsError> { let db_dir = &apply_saves_dir_override(db_dir); let _ = fs::create_dir(format!("{}/", db_dir)); + embedded_migrations::run_with_output( &establish_connection(db_dir).expect( "If we cannot execute migrations, we should not be allowed to launch the server, so \ we don't populate it with bad data.", ), - &mut std::io::stdout(), + &mut std::io::LineWriter::new(TracingOut), ) } From a893ccc22841271c8e2de6a0dd7538fb9cf3cc98 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 11:02:48 +0100 Subject: [PATCH 05/16] Fix the offsetting problem after using the cli --- server-cli/src/main.rs | 126 ++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index e5849560d4..1a62cf63bf 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -7,6 +7,11 @@ use server::{Event, Input, Server, ServerSettings}; use tracing::{error, info, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{ io::{self, Write}, sync::{mpsc, Arc, Mutex}, @@ -145,14 +150,73 @@ fn main() -> io::Result<()> { let (sender, receiver) = mpsc::channel(); + start_tui(sender); + + info!("Starting server..."); + + // Set up an fps clock + let mut clock = Clock::start(); + + // Load settings + let settings = ServerSettings::load(); + let server_port = &settings.gameserver_address.port(); + let metrics_port = &settings.metrics_address.port(); + // Create server + let mut server = Server::new(settings).expect("Failed to create server instance!"); + + info!("Server is ready to accept connections."); + info!(?metrics_port, "starting metrics at port"); + info!(?server_port, "starting server at port"); + + loop { + let events = server + .tick(Input::default(), clock.get_last_delta()) + .expect("Failed to tick server"); + + for event in events { + match event { + Event::ClientConnected { entity: _ } => info!("Client connected!"), + Event::ClientDisconnected { entity: _ } => info!("Client disconnected!"), + Event::Chat { entity: _, msg } => info!("[Client] {}", msg), + } + } + + // Clean up the server after a tick. + server.cleanup(); + + match receiver.try_recv() { + Ok(msg) => match msg { + Message::Quit => { + info!("Closing the server"); + break; + }, + }, + Err(e) => match e { + mpsc::TryRecvError::Empty => {}, + mpsc::TryRecvError::Disconnected => panic!(), + }, + }; + + // Wait for the next tick. + clock.tick(Duration::from_millis(1000 / TPS)); + } + + stop_tui(); + + Ok(()) +} + +fn start_tui(sender: mpsc::Sender) { + enable_raw_mode().unwrap(); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + std::thread::spawn(move || { // Start the tui let stdout = io::stdout(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend).unwrap(); - crossterm::terminal::enable_raw_mode().unwrap(); - let mut input = String::new(); let _ = terminal.clear(); @@ -218,55 +282,11 @@ fn main() -> io::Result<()> { }); } }); - - info!("Starting server..."); - - // Set up an fps clock - let mut clock = Clock::start(); - - // Load settings - let settings = ServerSettings::load(); - let server_port = &settings.gameserver_address.port(); - let metrics_port = &settings.metrics_address.port(); - // Create server - let mut server = Server::new(settings).expect("Failed to create server instance!"); - - info!("Server is ready to accept connections."); - info!(?metrics_port, "starting metrics at port"); - info!(?server_port, "starting server at port"); - - loop { - let events = server - .tick(Input::default(), clock.get_last_delta()) - .expect("Failed to tick server"); - - for event in events { - match event { - Event::ClientConnected { entity: _ } => info!("Client connected!"), - Event::ClientDisconnected { entity: _ } => info!("Client disconnected!"), - Event::Chat { entity: _, msg } => info!("[Client] {}", msg), - } - } - - // Clean up the server after a tick. - server.cleanup(); - - match receiver.try_recv() { - Ok(msg) => match msg { - Message::Quit => { - info!("Closing the server"); - break; - }, - }, - Err(e) => match e { - mpsc::TryRecvError::Empty => {}, - mpsc::TryRecvError::Disconnected => panic!(), - }, - }; - - // Wait for the next tick. - clock.tick(Duration::from_millis(1000 / TPS)); - } - - Ok(()) +} + +fn stop_tui() { + let mut stdout = io::stdout(); + + disable_raw_mode().unwrap(); + execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); } From bf8e455839c7863f2b33ffdfc0e5e216303b0e6e Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 11:32:29 +0100 Subject: [PATCH 06/16] Make it easier to add commands and added the help command --- server-cli/src/main.rs | 58 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 1a62cf63bf..68d1fd3eb5 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -4,7 +4,7 @@ use common::clock::Clock; use server::{Event, Input, Server, ServerSettings}; -use tracing::{error, info, Level}; +use tracing::{error, info, warn, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; use crossterm::{ @@ -28,6 +28,34 @@ use tui::{ const TPS: u64 = 30; const RUST_LOG_ENV: &str = "RUST_LOG"; +const COMMANDS: [Command; 2] = [ + Command { + name: "quit", + description: "Closes the server", + args: 0, + cmd: |_, sender| sender.send(Message::Quit).unwrap(), + }, + Command { + name: "help", + description: "List all command available", + args: 0, + cmd: |_, _| { + info!("===== Help ====="); + for command in COMMANDS.iter() { + info!("{} - {}", command.name, command.description) + } + info!("================"); + }, + }, +]; + +struct Command<'a> { + pub name: &'a str, + pub description: &'a str, + pub args: usize, + pub cmd: fn(Vec<&str>, &mut mpsc::Sender), +} + lazy_static! { static ref LOG: TuiLog<'static> = TuiLog::default(); } @@ -206,7 +234,7 @@ fn main() -> io::Result<()> { Ok(()) } -fn start_tui(sender: mpsc::Sender) { +fn start_tui(mut sender: mpsc::Sender) { enable_raw_mode().unwrap(); let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); @@ -267,9 +295,29 @@ fn start_tui(sender: mpsc::Sender) { input.pop(); }, KeyCode::Enter => { - match input.as_str() { - "quit" => sender.send(Message::Quit).unwrap(), - _ => error!("invalid command"), + let mut args = input.as_str().split_whitespace(); + + if let Some(cmd_name) = args.next() { + if let Some(cmd) = + COMMANDS.iter().find(|cmd| cmd.name == cmd_name) + { + let args = args.collect::>(); + + if args.len() > cmd.args { + warn!("{} only takes {} arguments", cmd_name, cmd.args); + let cmd = cmd.cmd; + + cmd(args, &mut sender) + } else if args.len() < cmd.args { + error!("{} takes {} arguments", cmd_name, cmd.args); + } else { + let cmd = cmd.cmd; + + cmd(args, &mut sender) + } + } else { + error!("{} not found", cmd_name); + } } input = String::new(); From 332cb20df19841216cc3573097f2667e21119be7 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 14:47:01 +0100 Subject: [PATCH 07/16] Added flags to toggle the tui --- Cargo.lock | 1 + server-cli/Cargo.toml | 1 + server-cli/src/main.rs | 74 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88637aa608..7abfea157c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4911,6 +4911,7 @@ name = "veloren-server-cli" version = "0.7.0" dependencies = [ "ansi-parser", + "clap", "crossterm", "lazy_static", "tracing", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 2559651874..46b23af5db 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -18,3 +18,4 @@ crossterm = "0.17" tui = { version = "0.10", default-features = false, features = ['crossterm'] } lazy_static = "1" ansi-parser = "0.6" +clap = "2.33" diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 68d1fd3eb5..e1efc26fb0 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -7,6 +7,7 @@ use server::{Event, Input, Server, ServerSettings}; use tracing::{error, info, warn, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; +use clap::{App, Arg}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, @@ -28,16 +29,23 @@ use tui::{ const TPS: u64 = 30; const RUST_LOG_ENV: &str = "RUST_LOG"; +#[derive(Debug, Clone)] +enum Message { + Quit, +} + const COMMANDS: [Command; 2] = [ Command { name: "quit", description: "Closes the server", + split_spaces: true, args: 0, cmd: |_, sender| sender.send(Message::Quit).unwrap(), }, Command { name: "help", description: "List all command available", + split_spaces: true, args: 0, cmd: |_, _| { info!("===== Help ====="); @@ -52,8 +60,10 @@ const COMMANDS: [Command; 2] = [ struct Command<'a> { pub name: &'a str, pub description: &'a str, + // Whether or not the command splits the arguments on whitespace + pub split_spaces: bool, pub args: usize, - pub cmd: fn(Vec<&str>, &mut mpsc::Sender), + pub cmd: fn(Vec, &mut mpsc::Sender), } lazy_static! { @@ -144,12 +154,29 @@ impl<'a> Write for TuiLog<'a> { fn flush(&mut self) -> io::Result<()> { Ok(()) } } -#[derive(Debug, Copy, Clone)] -enum Message { - Quit, -} - fn main() -> io::Result<()> { + let matches = App::new("Veloren server cli") + .version( + format!( + "{}-{}", + env!("CARGO_PKG_VERSION"), + common::util::GIT_HASH.to_string() + ) + .as_str(), + ) + .author("The veloren devs ") + .about("The veloren server cli provides a easy to use interface to start a veloren server") + .arg( + Arg::with_name("basic") + .short("b") + .long("basic") + .help("Disables the tui") + .takes_value(false), + ) + .get_matches(); + + let basic = matches.is_present("basic"); + // Init logging let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { Some(Ok(env)) => { @@ -170,15 +197,21 @@ fn main() -> io::Result<()> { .add_directive(LevelFilter::INFO.into()), }; - FmtSubscriber::builder() + let subscriber = FmtSubscriber::builder() .with_max_level(Level::ERROR) - .with_env_filter(filter) - .with_writer(|| LOG.clone()) - .init(); + .with_env_filter(filter); + + if basic { + subscriber.init(); + } else { + subscriber.with_writer(|| LOG.clone()).init(); + } let (sender, receiver) = mpsc::channel(); - start_tui(sender); + if !basic { + start_tui(sender); + } info!("Starting server..."); @@ -229,7 +262,9 @@ fn main() -> io::Result<()> { clock.tick(Duration::from_millis(1000 / TPS)); } - stop_tui(); + if !basic { + stop_tui(); + } Ok(()) } @@ -303,12 +338,23 @@ fn start_tui(mut sender: mpsc::Sender) { { let args = args.collect::>(); - if args.len() > cmd.args { + let (arg_len, args) = if cmd.split_spaces { + ( + args.len(), + args.into_iter() + .map(|s| s.to_string()) + .collect::>(), + ) + } else { + (1, vec![args.into_iter().collect::()]) + }; + + if arg_len > cmd.args { warn!("{} only takes {} arguments", cmd_name, cmd.args); let cmd = cmd.cmd; cmd(args, &mut sender) - } else if args.len() < cmd.args { + } else if arg_len < cmd.args { error!("{} takes {} arguments", cmd_name, cmd.args); } else { let cmd = cmd.cmd; From 8fd052768e5dd9ae750b97bc72e9b401a118c745 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 16:02:36 +0100 Subject: [PATCH 08/16] Fix typo and clippy warnings --- server-cli/src/main.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index e1efc26fb0..42f1186583 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -165,7 +165,7 @@ fn main() -> io::Result<()> { .as_str(), ) .author("The veloren devs ") - .about("The veloren server cli provides a easy to use interface to start a veloren server") + .about("The veloren server cli provides an easy to use interface to start a veloren server") .arg( Arg::with_name("basic") .short("b") @@ -316,8 +316,8 @@ fn start_tui(mut sender: mpsc::Sender) { use crossterm::event::{KeyModifiers, *}; if poll(Duration::from_millis(10)).unwrap() { - match read().unwrap() { - Event::Key(event) => match event.code { + if let Event::Key(event) = read().unwrap() { + match event.code { KeyCode::Char('c') => { if event.modifiers.contains(KeyModifiers::CONTROL) { sender.send(Message::Quit).unwrap() @@ -349,17 +349,24 @@ fn start_tui(mut sender: mpsc::Sender) { (1, vec![args.into_iter().collect::()]) }; - if arg_len > cmd.args { - warn!("{} only takes {} arguments", cmd_name, cmd.args); - let cmd = cmd.cmd; + match arg_len.cmp(&cmd.args) { + std::cmp::Ordering::Less => { + error!("{} takes {} arguments", cmd_name, cmd.args) + }, + std::cmp::Ordering::Greater => { + warn!( + "{} only takes {} arguments", + cmd_name, cmd.args + ); + let cmd = cmd.cmd; - cmd(args, &mut sender) - } else if arg_len < cmd.args { - error!("{} takes {} arguments", cmd_name, cmd.args); - } else { - let cmd = cmd.cmd; + cmd(args, &mut sender) + }, + std::cmp::Ordering::Equal => { + let cmd = cmd.cmd; - cmd(args, &mut sender) + cmd(args, &mut sender) + }, } } else { error!("{} not found", cmd_name); @@ -369,8 +376,7 @@ fn start_tui(mut sender: mpsc::Sender) { input = String::new(); }, _ => {}, - }, - _ => {}, + } } } }); From aa676a4327fc67c40fe4d2c2976bc2abdbd8b5bd Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 16:48:31 +0100 Subject: [PATCH 09/16] Updated the Dockerfile --- server-cli/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-cli/Dockerfile b/server-cli/Dockerfile index 3cb85181f9..3ed1434712 100644 --- a/server-cli/Dockerfile +++ b/server-cli/Dockerfile @@ -13,4 +13,4 @@ COPY ./assets/common /opt/assets/common COPY ./assets/world /opt/assets/world WORKDIR /opt -CMD [ "sh", "-c", "RUST_BACKTRACE=1 /opt/veloren-server-cli" ] +CMD [ "sh", "-c", "RUST_BACKTRACE=1 /opt/veloren-server-cli -b" ] From 752b2510eedfe06c7c365a54800f94f80113c5d8 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 17:29:08 +0100 Subject: [PATCH 10/16] Fix resize issue --- server-cli/src/main.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 42f1186583..e26eaba461 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -20,7 +20,6 @@ use std::{ }; use tui::{ backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, text::Text, widgets::{Block, Borders, Paragraph, Wrap}, Terminal, @@ -286,30 +285,32 @@ fn start_tui(mut sender: mpsc::Sender) { loop { let _ = terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Max(u16::MAX), Constraint::Max(3)].as_ref()) - .split(f.size()); + 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; let block = Block::default().borders(Borders::ALL); - let size = block.inner(chunks[0]); + let size = block.inner(log_rect); LOG.resize(size.height as usize); let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) .block(block) .wrap(Wrap { trim: false }); - f.render_widget(logger, chunks[0]); + f.render_widget(logger, log_rect); let text: Text = input.as_str().into(); let block = Block::default().borders(Borders::ALL); - let size = block.inner(chunks[1]); + 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, chunks[1]); + f.render_widget(input_field, input_rect); f.set_cursor(x, size.y); From 8b1ba19b4fe88d3b33a62746f244d14812c9dca2 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 28 Jul 2020 17:50:24 +0100 Subject: [PATCH 11/16] Fix panic when window is too small --- server-cli/src/main.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index e26eaba461..5feb3c807c 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -20,6 +20,7 @@ use std::{ }; use tui::{ backend::CrosstermBackend, + layout::Rect, text::Text, widgets::{Block, Borders, Paragraph, Wrap}, Terminal, @@ -273,6 +274,12 @@ fn start_tui(mut sender: mpsc::Sender) { let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + stop_tui(); + hook(info); + })); + std::thread::spawn(move || { // Start the tui let stdout = io::stdout(); @@ -285,12 +292,18 @@ fn start_tui(mut sender: mpsc::Sender) { loop { let _ = terminal.draw(|f| { - let mut log_rect = f.size(); - log_rect.height -= 3; + 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; + 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 size = block.inner(log_rect); @@ -314,7 +327,7 @@ fn start_tui(mut sender: mpsc::Sender) { f.set_cursor(x, size.y); - use crossterm::event::{KeyModifiers, *}; + use crossterm::event::*; if poll(Duration::from_millis(10)).unwrap() { if let Event::Key(event) = read().unwrap() { From 337860de9307a22471088d9be552c73da4c059c6 Mon Sep 17 00:00:00 2001 From: Capucho Date: Wed, 29 Jul 2020 18:40:26 +0100 Subject: [PATCH 12/16] Fix lack of scrolling --- server-cli/src/main.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 5feb3c807c..db6ee6e9c8 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -84,6 +84,29 @@ impl<'a> TuiLog<'a> { inner.lines.drain(0..length); } } + + fn height(&self, rect: Rect) -> u16 { + // TODO: There's probably a better solution + let inner = self.inner.lock().unwrap(); + let mut h = 0; + + for line in inner.lines.iter() { + let mut w = 0; + + for word in line.0.iter() { + if word.width() + w > rect.width as usize { + h += (word.width() / rect.width as usize).min(1); + w = word.width() % rect.width as usize; + } else { + w += word.width(); + } + } + + h += 1; + } + + h as u16 + } } impl<'a> Write for TuiLog<'a> { @@ -310,9 +333,14 @@ fn start_tui(mut sender: mpsc::Sender) { LOG.resize(size.height as usize); + let scroll = (LOG.height(size) as i16 - size.height as i16).max(0) as u16; + + print!("{} {} {}", LOG.height(size) as i16, size.width, size.height); + let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) .block(block) - .wrap(Wrap { trim: false }); + .wrap(Wrap { trim: false }) + .scroll((scroll, 0)); f.render_widget(logger, log_rect); let text: Text = input.as_str().into(); From f5c8f3fcf873b2de7c1d530593a6f4e746129b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=A4rtens?= Date: Mon, 31 Aug 2020 10:41:11 +0200 Subject: [PATCH 13/16] move tui in multiple files --- server-cli/src/main.rs | 322 ++--------------------------------- server-cli/src/tui_runner.rs | 220 ++++++++++++++++++++++++ server-cli/src/tuilog.rs | 112 ++++++++++++ 3 files changed, 345 insertions(+), 309 deletions(-) create mode 100644 server-cli/src/tui_runner.rs create mode 100644 server-cli/src/tuilog.rs diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index db6ee6e9c8..484e9e4e24 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -1,182 +1,29 @@ #![deny(unsafe_code)] +mod tui_runner; +mod tuilog; + #[macro_use] extern crate lazy_static; +use crate::{ + tui_runner::{Message, Tui}, + tuilog::TuiLog, +}; use common::clock::Clock; use server::{Event, Input, Server, ServerSettings}; use tracing::{error, info, warn, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; use clap::{App, Arg}; -use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use std::{ - io::{self, Write}, - sync::{mpsc, Arc, Mutex}, - time::Duration, -}; -use tui::{ - backend::CrosstermBackend, - layout::Rect, - text::Text, - widgets::{Block, Borders, Paragraph, Wrap}, - Terminal, -}; +use std::{io, sync::mpsc, time::Duration}; const TPS: u64 = 30; const RUST_LOG_ENV: &str = "RUST_LOG"; -#[derive(Debug, Clone)] -enum Message { - Quit, -} - -const COMMANDS: [Command; 2] = [ - Command { - name: "quit", - description: "Closes the server", - split_spaces: true, - args: 0, - cmd: |_, sender| sender.send(Message::Quit).unwrap(), - }, - Command { - name: "help", - description: "List all command available", - split_spaces: true, - args: 0, - cmd: |_, _| { - info!("===== Help ====="); - for command in COMMANDS.iter() { - info!("{} - {}", command.name, command.description) - } - info!("================"); - }, - }, -]; - -struct Command<'a> { - pub name: &'a str, - pub description: &'a str, - // Whether or not the command splits the arguments on whitespace - pub split_spaces: bool, - pub args: usize, - pub cmd: fn(Vec, &mut mpsc::Sender), -} - lazy_static! { static ref LOG: TuiLog<'static> = TuiLog::default(); } -#[derive(Debug, Default, Clone)] -struct TuiLog<'a> { - inner: Arc>>, -} - -impl<'a> TuiLog<'a> { - fn resize(&self, h: usize) { - let mut inner = self.inner.lock().unwrap(); - - if inner.height() > h { - let length = inner.height() - h; - inner.lines.drain(0..length); - } - } - - fn height(&self, rect: Rect) -> u16 { - // TODO: There's probably a better solution - let inner = self.inner.lock().unwrap(); - let mut h = 0; - - for line in inner.lines.iter() { - let mut w = 0; - - for word in line.0.iter() { - if word.width() + w > rect.width as usize { - h += (word.width() / rect.width as usize).min(1); - w = word.width() % rect.width as usize; - } else { - w += word.width(); - } - } - - h += 1; - } - - h as u16 - } -} - -impl<'a> Write for TuiLog<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - use ansi_parser::{AnsiParser, AnsiSequence, Output}; - use tui::{ - style::{Color, Modifier}, - text::{Span, Spans}, - }; - - let line = String::from_utf8(buf.into()) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - let mut spans = Vec::new(); - let mut span = Span::raw(""); - - for out in line.ansi_parse() { - match out { - Output::TextBlock(text) => { - span.content = format!("{}{}", span.content.to_owned(), text).into() - }, - Output::Escape(seq) => { - if span.content.len() != 0 { - spans.push(span); - - span = Span::raw(""); - } - - match seq { - AnsiSequence::SetGraphicsMode(values) => { - const COLOR_TABLE: [Color; 8] = [ - Color::Black, - Color::Red, - Color::Green, - Color::Yellow, - Color::Blue, - Color::Magenta, - Color::Cyan, - Color::White, - ]; - - let mut iter = values.iter(); - - match iter.next().unwrap() { - 0 => {}, - 2 => span.style.add_modifier = Modifier::DIM, - idx @ 30..=37 => { - span.style.fg = Some(COLOR_TABLE[(idx - 30) as usize]) - }, - _ => println!("{:#?}", values), - } - }, - _ => println!("{:#?}", seq), - } - }, - } - } - - if span.content.len() != 0 { - spans.push(span); - } - - self.inner.lock().unwrap().lines.push(Spans(spans)); - - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { Ok(()) } -} - fn main() -> io::Result<()> { let matches = App::new("Veloren server cli") .version( @@ -199,6 +46,7 @@ fn main() -> io::Result<()> { .get_matches(); let basic = matches.is_present("basic"); + let (mut tui, msg_r) = Tui::new(); // Init logging let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { @@ -230,10 +78,8 @@ fn main() -> io::Result<()> { subscriber.with_writer(|| LOG.clone()).init(); } - let (sender, receiver) = mpsc::channel(); - if !basic { - start_tui(sender); + tui.run(); } info!("Starting server..."); @@ -268,7 +114,7 @@ fn main() -> io::Result<()> { // Clean up the server after a tick. server.cleanup(); - match receiver.try_recv() { + match msg_r.try_recv() { Ok(msg) => match msg { Message::Quit => { info!("Closing the server"); @@ -285,150 +131,8 @@ fn main() -> io::Result<()> { clock.tick(Duration::from_millis(1000 / TPS)); } - if !basic { - stop_tui(); - } + drop(tui); + std::thread::sleep(Duration::from_millis(10)); Ok(()) } - -fn start_tui(mut sender: mpsc::Sender) { - enable_raw_mode().unwrap(); - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - - let hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - stop_tui(); - hook(info); - })); - - std::thread::spawn(move || { - // Start the tui - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).unwrap(); - - let mut input = String::new(); - - let _ = terminal.clear(); - - loop { - let _ = 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 size = block.inner(log_rect); - - LOG.resize(size.height as usize); - - let scroll = (LOG.height(size) as i16 - size.height as i16).max(0) as u16; - - print!("{} {} {}", LOG.height(size) as i16, size.width, size.height); - - let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) - .block(block) - .wrap(Wrap { trim: false }) - .scroll((scroll, 0)); - 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); - - use crossterm::event::*; - - if poll(Duration::from_millis(10)).unwrap() { - if let Event::Key(event) = read().unwrap() { - match event.code { - KeyCode::Char('c') => { - if event.modifiers.contains(KeyModifiers::CONTROL) { - sender.send(Message::Quit).unwrap() - } else { - input.push('c'); - } - }, - KeyCode::Char(c) => input.push(c), - KeyCode::Backspace => { - input.pop(); - }, - KeyCode::Enter => { - let mut args = input.as_str().split_whitespace(); - - if let Some(cmd_name) = args.next() { - if let Some(cmd) = - COMMANDS.iter().find(|cmd| cmd.name == cmd_name) - { - let args = args.collect::>(); - - let (arg_len, args) = if cmd.split_spaces { - ( - args.len(), - args.into_iter() - .map(|s| s.to_string()) - .collect::>(), - ) - } else { - (1, vec![args.into_iter().collect::()]) - }; - - match arg_len.cmp(&cmd.args) { - std::cmp::Ordering::Less => { - error!("{} takes {} arguments", cmd_name, cmd.args) - }, - std::cmp::Ordering::Greater => { - warn!( - "{} only takes {} arguments", - cmd_name, cmd.args - ); - let cmd = cmd.cmd; - - cmd(args, &mut sender) - }, - std::cmp::Ordering::Equal => { - let cmd = cmd.cmd; - - cmd(args, &mut sender) - }, - } - } else { - error!("{} not found", cmd_name); - } - } - - input = String::new(); - }, - _ => {}, - } - } - } - }); - } - }); -} - -fn stop_tui() { - let mut stdout = io::stdout(); - - disable_raw_mode().unwrap(); - execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); -} diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs new file mode 100644 index 0000000000..874299f378 --- /dev/null +++ b/server-cli/src/tui_runner.rs @@ -0,0 +1,220 @@ +use crate::LOG; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + io::{self, Write}, + sync::mpsc, + time::Duration, +}; +use tracing::{error, info, warn}; +use tui::{ + backend::CrosstermBackend, + layout::Rect, + text::Text, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + +#[derive(Debug, Clone)] +pub enum Message { + Quit, +} + +pub struct Command<'a> { + pub name: &'a str, + pub description: &'a str, + // Whether or not the command splits the arguments on whitespace + pub split_spaces: bool, + pub args: usize, + pub cmd: fn(Vec, &mut mpsc::Sender), +} + +pub const COMMANDS: [Command; 2] = [ + Command { + name: "quit", + description: "Closes the server", + split_spaces: true, + args: 0, + cmd: |_, sender| sender.send(Message::Quit).unwrap(), + }, + Command { + name: "help", + description: "List all command available", + split_spaces: true, + args: 0, + cmd: |_, _| { + info!("===== Help ====="); + for command in COMMANDS.iter() { + info!("{} - {}", command.name, command.description) + } + info!("================"); + }, + }, +]; + +pub struct Tui { + msg_s: Option>, + background: Option>, +} + +impl Tui { + pub fn new() -> (Self, mpsc::Receiver) { + let (msg_s, msg_r) = mpsc::channel(); + ( + Self { + msg_s: Some(msg_s), + background: None, + }, + msg_r, + ) + } + + fn inner() {} + + fn handle_events(input: &mut String, msg_s: &mut mpsc::Sender) { + 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::Quit).unwrap() + } else { + input.push('c'); + } + }, + KeyCode::Char(c) => input.push(c), + KeyCode::Backspace => { + input.pop(); + }, + KeyCode::Enter => { + let mut args = input.as_str().split_whitespace(); + + if let Some(cmd_name) = args.next() { + if let Some(cmd) = COMMANDS.iter().find(|cmd| cmd.name == cmd_name) { + let args = args.collect::>(); + + let (arg_len, args) = if cmd.split_spaces { + ( + args.len(), + args.into_iter() + .map(|s| s.to_string()) + .collect::>(), + ) + } else { + (1, vec![args.into_iter().collect::()]) + }; + + match arg_len.cmp(&cmd.args) { + std::cmp::Ordering::Less => { + error!("{} takes {} arguments", cmd_name, cmd.args) + }, + std::cmp::Ordering::Greater => { + warn!("{} only takes {} arguments", cmd_name, cmd.args); + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + std::cmp::Ordering::Equal => { + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + } + } else { + error!("{} not found", cmd_name); + } + } + + *input = String::new(); + }, + _ => {}, + } + } + } + + pub fn run(&mut self) { + enable_raw_mode().unwrap(); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + hook(info); + })); + + let mut msg_s = self.msg_s.take().unwrap(); + + self.background = Some(std::thread::spawn(move || { + // Start the tui + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + + let mut input = String::new(); + + let _ = terminal.clear(); + + loop { + let _ = 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 size = block.inner(log_rect); + + LOG.resize(size.height as usize); + + let scroll = (LOG.height(size) as i16 - size.height as i16).max(0) as u16; + + print!("{} {} {}", LOG.height(size) as i16, size.width, size.height); + + let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((scroll, 0)); + 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); + + use crossterm::event::*; + + if poll(Duration::from_millis(10)).unwrap() { + Self::handle_events(&mut input, &mut msg_s); + }; + }); + } + })); + } +} + +impl Drop for Tui { + fn drop(&mut self) { + let mut stdout = io::stdout(); + + disable_raw_mode().unwrap(); + execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); + } +} diff --git a/server-cli/src/tuilog.rs b/server-cli/src/tuilog.rs new file mode 100644 index 0000000000..d02f7a1d0c --- /dev/null +++ b/server-cli/src/tuilog.rs @@ -0,0 +1,112 @@ +use std::{ + io::{self, Write}, + sync::{Arc, Mutex}, +}; +use tui::{layout::Rect, text::Text}; + +#[derive(Debug, Default, Clone)] +pub struct TuiLog<'a> { + pub inner: Arc>>, +} + +impl<'a> TuiLog<'a> { + pub fn resize(&self, h: usize) { + let mut inner = self.inner.lock().unwrap(); + + if inner.height() > h { + let length = inner.height() - h; + inner.lines.drain(0..length); + } + } + + pub fn height(&self, rect: Rect) -> u16 { + // TODO: There's probably a better solution + let inner = self.inner.lock().unwrap(); + let mut h = 0; + + for line in inner.lines.iter() { + let mut w = 0; + + for word in line.0.iter() { + if word.width() + w > rect.width as usize { + h += (word.width() / rect.width as usize).min(1); + w = word.width() % rect.width as usize; + } else { + w += word.width(); + } + } + + h += 1; + } + + h as u16 + } +} + +impl<'a> Write for TuiLog<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + use ansi_parser::{AnsiParser, AnsiSequence, Output}; + use tui::{ + style::{Color, Modifier}, + text::{Span, Spans}, + }; + + let line = String::from_utf8(buf.into()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let mut spans = Vec::new(); + let mut span = Span::raw(""); + + for out in line.ansi_parse() { + match out { + Output::TextBlock(text) => { + span.content = format!("{}{}", span.content.to_owned(), text).into() + }, + Output::Escape(seq) => { + if span.content.len() != 0 { + spans.push(span); + + span = Span::raw(""); + } + + match seq { + AnsiSequence::SetGraphicsMode(values) => { + const COLOR_TABLE: [Color; 8] = [ + Color::Black, + Color::Red, + Color::Green, + Color::Yellow, + Color::Blue, + Color::Magenta, + Color::Cyan, + Color::White, + ]; + + let mut iter = values.iter(); + + match iter.next().unwrap() { + 0 => {}, + 2 => span.style.add_modifier = Modifier::DIM, + idx @ 30..=37 => { + span.style.fg = Some(COLOR_TABLE[(idx - 30) as usize]) + }, + _ => println!("{:#?}", values), + } + }, + _ => println!("{:#?}", seq), + } + }, + } + } + + if span.content.len() != 0 { + spans.push(span); + } + + self.inner.lock().unwrap().lines.push(Spans(spans)); + + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} From 36350a08b178a2f58051207d70f4fba00e60400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=A4rtens?= Date: Mon, 31 Aug 2020 14:38:54 +0200 Subject: [PATCH 14/16] clean shutdown and do a full frame all 10 ticks. also increase polling time to 100ms and dont poll while drawing but afterwards --- server-cli/src/main.rs | 2 +- server-cli/src/tui_runner.rs | 54 ++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 484e9e4e24..f40ba83a9b 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -11,7 +11,7 @@ use crate::{ }; use common::clock::Clock; use server::{Event, Input, Server, ServerSettings}; -use tracing::{error, info, warn, Level}; +use tracing::{info, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; use clap::{App, Arg}; diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs index 874299f378..5773998eb3 100644 --- a/server-cli/src/tui_runner.rs +++ b/server-cli/src/tui_runner.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use std::{ io::{self, Write}, - sync::mpsc, + sync::{Arc, mpsc, atomic::{AtomicBool, Ordering}}, time::Duration, }; use tracing::{error, info, warn}; @@ -58,6 +58,7 @@ pub const COMMANDS: [Command; 2] = [ pub struct Tui { msg_s: Option>, background: Option>, + running: Arc, } impl Tui { @@ -67,13 +68,12 @@ impl Tui { Self { msg_s: Some(msg_s), background: None, + running: Arc::new(AtomicBool::new(true)), }, msg_r, ) } - fn inner() {} - fn handle_events(input: &mut String, msg_s: &mut mpsc::Sender) { use crossterm::event::*; if let Event::Key(event) = read().unwrap() { @@ -142,10 +142,12 @@ impl Tui { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { + Self::shutdown(); hook(info); })); let mut msg_s = self.msg_s.take().unwrap(); + let running = self.running.clone(); self.background = Some(std::thread::spawn(move || { // Start the tui @@ -155,10 +157,19 @@ impl Tui { let mut input = String::new(); - let _ = terminal.clear(); + if let Err(e) = terminal.clear() { + error!(?e, "clouldn't clean terminal"); + }; + let mut i: u64 = 0; - loop { - let _ = terminal.draw(|f| { + while running.load(Ordering::Relaxed) { + i += 1; + // This is a tmp fix that does a full redraw all 10 ticks, in case the backend breaks (which happens sometimes) + if i.rem_euclid(10) == 0 { + let size = terminal.size().unwrap(); + terminal.resize(size).unwrap(); + } + 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; @@ -179,7 +190,7 @@ impl Tui { let scroll = (LOG.height(size) as i16 - size.height as i16).max(0) as u16; - print!("{} {} {}", LOG.height(size) as i16, size.width, size.height); + //trace!(?i, "{} {} {}", LOG.height(size) as i16, size.width, size.height); let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) .block(block) @@ -198,23 +209,32 @@ impl Tui { f.render_widget(input_field, input_rect); f.set_cursor(x, size.y); - - use crossterm::event::*; - - if poll(Duration::from_millis(10)).unwrap() { - Self::handle_events(&mut input, &mut msg_s); - }; - }); + }) { + warn!(?e, "couldn't draw frame"); + }; + if crossterm::event::poll(Duration::from_millis(100)).unwrap() { + Self::handle_events(&mut input, &mut msg_s); + }; } + + if let Err(e) = terminal.clear() { + error!(?e, "clouldn't clean terminal"); + }; })); } -} -impl Drop for Tui { - fn drop(&mut self) { + fn shutdown() { let mut stdout = io::stdout(); disable_raw_mode().unwrap(); execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); } } + +impl Drop for Tui { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + self.background.take().map(|m| m.join()); + Self::shutdown(); + } +} From 6e24ff31ee114a827f89bd069cec7eaaa1acf6f6 Mon Sep 17 00:00:00 2001 From: Capucho Date: Tue, 1 Sep 2020 15:33:41 +0100 Subject: [PATCH 15/16] Fix all the problems with the tui --- Cargo.lock | 3 +-- server-cli/Cargo.toml | 7 ++++++- server-cli/src/main.rs | 3 --- server-cli/src/tui_runner.rs | 38 ++++++++++++++---------------------- server-cli/src/tuilog.rs | 36 ++++++---------------------------- 5 files changed, 28 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7abfea157c..e35d60dd5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4632,8 +4632,7 @@ checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" [[package]] name = "tui" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a977b0bb2e2033a6fef950f218f13622c3c34e59754b704ce3492dedab1dfe95" +source = "git+https://github.com/fdehau/tui-rs.git?branch=paragraph-scroll#54b841fab6cfdb38e8dc1382176e965787964b4c" dependencies = [ "bitflags", "cassowary", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 46b23af5db..cdb05ded2a 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -15,7 +15,12 @@ common = { package = "veloren-common", path = "../common" } tracing = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.2.3", default-features = false, features = ["env-filter", "fmt", "chrono", "ansi", "smallvec"] } crossterm = "0.17" -tui = { version = "0.10", default-features = false, features = ['crossterm'] } lazy_static = "1" ansi-parser = "0.6" clap = "2.33" + +[dependencies.tui] +git = "https://github.com/fdehau/tui-rs.git" +branch="paragraph-scroll" +default-features = false +features = ['crossterm'] \ No newline at end of file diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index f40ba83a9b..b47c89d4f9 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -131,8 +131,5 @@ fn main() -> io::Result<()> { clock.tick(Duration::from_millis(1000 / TPS)); } - drop(tui); - std::thread::sleep(Duration::from_millis(10)); - Ok(()) } diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs index 5773998eb3..cdc2a2e1d9 100644 --- a/server-cli/src/tui_runner.rs +++ b/server-cli/src/tui_runner.rs @@ -6,7 +6,10 @@ use crossterm::{ }; use std::{ io::{self, Write}, - sync::{Arc, mpsc, atomic::{AtomicBool, Ordering}}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, + }, time::Duration, }; use tracing::{error, info, warn}; @@ -136,10 +139,11 @@ impl Tui { } pub fn run(&mut self) { - enable_raw_mode().unwrap(); let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + enable_raw_mode().unwrap(); + let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { Self::shutdown(); @@ -160,15 +164,8 @@ impl Tui { if let Err(e) = terminal.clear() { error!(?e, "clouldn't clean terminal"); }; - let mut i: u64 = 0; while running.load(Ordering::Relaxed) { - i += 1; - // This is a tmp fix that does a full redraw all 10 ticks, in case the backend breaks (which happens sometimes) - if i.rem_euclid(10) == 0 { - let size = terminal.size().unwrap(); - terminal.resize(size).unwrap(); - } if let Err(e) = terminal.draw(|f| { let (log_rect, input_rect) = if f.size().height > 6 { let mut log_rect = f.size(); @@ -184,18 +181,17 @@ impl Tui { }; let block = Block::default().borders(Borders::ALL); - let size = block.inner(log_rect); - LOG.resize(size.height as usize); - - let scroll = (LOG.height(size) as i16 - size.height as i16).max(0) as u16; - - //trace!(?i, "{} {} {}", LOG.height(size) as i16, size.width, size.height); + let mut wrap = Wrap::default(); + 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) + })); let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) .block(block) - .wrap(Wrap { trim: false }) - .scroll((scroll, 0)); + .wrap(wrap); f.render_widget(logger, log_rect); let text: Text = input.as_str().into(); @@ -212,22 +208,18 @@ impl Tui { }) { warn!(?e, "couldn't draw frame"); }; - if crossterm::event::poll(Duration::from_millis(100)).unwrap() { + if crossterm::event::poll(Duration::from_millis(10)).unwrap() { Self::handle_events(&mut input, &mut msg_s); }; } - - if let Err(e) = terminal.clear() { - error!(?e, "clouldn't clean terminal"); - }; })); } fn shutdown() { let mut stdout = io::stdout(); - disable_raw_mode().unwrap(); execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); + disable_raw_mode().unwrap(); } } diff --git a/server-cli/src/tuilog.rs b/server-cli/src/tuilog.rs index d02f7a1d0c..273626477b 100644 --- a/server-cli/src/tuilog.rs +++ b/server-cli/src/tuilog.rs @@ -2,7 +2,8 @@ use std::{ io::{self, Write}, sync::{Arc, Mutex}, }; -use tui::{layout::Rect, text::Text}; +use tracing::warn; +use tui::text::Text; #[derive(Debug, Default, Clone)] pub struct TuiLog<'a> { @@ -13,33 +14,7 @@ impl<'a> TuiLog<'a> { pub fn resize(&self, h: usize) { let mut inner = self.inner.lock().unwrap(); - if inner.height() > h { - let length = inner.height() - h; - inner.lines.drain(0..length); - } - } - - pub fn height(&self, rect: Rect) -> u16 { - // TODO: There's probably a better solution - let inner = self.inner.lock().unwrap(); - let mut h = 0; - - for line in inner.lines.iter() { - let mut w = 0; - - for word in line.0.iter() { - if word.width() + w > rect.width as usize { - h += (word.width() / rect.width as usize).min(1); - w = word.width() % rect.width as usize; - } else { - w += word.width(); - } - } - - h += 1; - } - - h as u16 + inner.lines.truncate(h); } } @@ -86,14 +61,15 @@ impl<'a> Write for TuiLog<'a> { match iter.next().unwrap() { 0 => {}, + 1 => span.style.add_modifier = Modifier::BOLD, 2 => span.style.add_modifier = Modifier::DIM, idx @ 30..=37 => { span.style.fg = Some(COLOR_TABLE[(idx - 30) as usize]) }, - _ => println!("{:#?}", values), + _ => warn!("Unknown color {:#?}", values), } }, - _ => println!("{:#?}", seq), + _ => warn!("Unknown sequence {:#?}", seq), } }, } From 5fd0e0a5b70ac37f40623563e636bdb08098f8e4 Mon Sep 17 00:00:00 2001 From: Capucho Date: Wed, 2 Sep 2020 12:47:17 +0100 Subject: [PATCH 16/16] Added commands to basic mode --- server-cli/src/main.rs | 4 +- server-cli/src/tui_runner.rs | 207 +++++++++++++++++++---------------- 2 files changed, 111 insertions(+), 100 deletions(-) diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index b47c89d4f9..526c1be55d 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -78,9 +78,7 @@ fn main() -> io::Result<()> { subscriber.with_writer(|| LOG.clone()).init(); } - if !basic { - tui.run(); - } + tui.run(basic); info!("Starting server..."); diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs index cdc2a2e1d9..8b2432dac6 100644 --- a/server-cli/src/tui_runner.rs +++ b/server-cli/src/tui_runner.rs @@ -93,43 +93,7 @@ impl Tui { input.pop(); }, KeyCode::Enter => { - let mut args = input.as_str().split_whitespace(); - - if let Some(cmd_name) = args.next() { - if let Some(cmd) = COMMANDS.iter().find(|cmd| cmd.name == cmd_name) { - let args = args.collect::>(); - - let (arg_len, args) = if cmd.split_spaces { - ( - args.len(), - args.into_iter() - .map(|s| s.to_string()) - .collect::>(), - ) - } else { - (1, vec![args.into_iter().collect::()]) - }; - - match arg_len.cmp(&cmd.args) { - std::cmp::Ordering::Less => { - error!("{} takes {} arguments", cmd_name, cmd.args) - }, - std::cmp::Ordering::Greater => { - warn!("{} only takes {} arguments", cmd_name, cmd.args); - let cmd = cmd.cmd; - - cmd(args, msg_s) - }, - std::cmp::Ordering::Equal => { - let cmd = cmd.cmd; - - cmd(args, msg_s) - }, - } - } else { - error!("{} not found", cmd_name); - } - } + parse_command(input, msg_s); *input = String::new(); }, @@ -138,12 +102,7 @@ impl Tui { } } - pub fn run(&mut self) { - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - - enable_raw_mode().unwrap(); - + pub fn run(&mut self, basic: bool) { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { Self::shutdown(); @@ -153,66 +112,82 @@ impl Tui { let mut msg_s = self.msg_s.take().unwrap(); let running = self.running.clone(); - self.background = Some(std::thread::spawn(move || { - // Start the tui - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).unwrap(); + if basic { + std::thread::spawn(move || { + while running.load(Ordering::Relaxed) { + let mut buf = String::new(); - let mut input = String::new(); + io::stdin().read_line(&mut buf).unwrap(); - if let Err(e) = terminal.clear() { - error!(?e, "clouldn't clean terminal"); - }; + parse_command(&buf, &mut msg_s); + } + }); + } else { + self.background = Some(std::thread::spawn(move || { + // Start the tui + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - 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; + enable_raw_mode().unwrap(); - let mut input_rect = f.size(); - input_rect.y = input_rect.height - 3; - input_rect.height = 3; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); - (log_rect, input_rect) - } else { - (f.size(), Rect::default()) + let mut input = String::new(); + + if let Err(e) = terminal.clear() { + error!(?e, "clouldn'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 mut wrap = Wrap::default(); + 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) + })); + + 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"); }; - - let block = Block::default().borders(Borders::ALL); - - let mut wrap = Wrap::default(); - 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) - })); - - 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(10)).unwrap() { - Self::handle_events(&mut input, &mut msg_s); - }; - } - })); + if crossterm::event::poll(Duration::from_millis(10)).unwrap() { + Self::handle_events(&mut input, &mut msg_s); + }; + } + })); + } } fn shutdown() { @@ -230,3 +205,41 @@ impl Drop for Tui { Self::shutdown(); } } + +fn parse_command(input: &str, msg_s: &mut mpsc::Sender) { + let mut args = input.split_whitespace(); + + if let Some(cmd_name) = args.next() { + if let Some(cmd) = COMMANDS.iter().find(|cmd| cmd.name == cmd_name) { + let args = args.collect::>(); + + let (arg_len, args) = if cmd.split_spaces { + ( + args.len(), + args.into_iter() + .map(|s| s.to_string()) + .collect::>(), + ) + } else { + (1, vec![args.into_iter().collect::()]) + }; + + match arg_len.cmp(&cmd.args) { + std::cmp::Ordering::Less => error!("{} takes {} arguments", cmd_name, cmd.args), + std::cmp::Ordering::Greater => { + warn!("{} only takes {} arguments", cmd_name, cmd.args); + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + std::cmp::Ordering::Equal => { + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + } + } else { + error!("{} not found", cmd_name); + } + } +}