From 1a46405a49cb12eeb0ec9dcb7ba77ab1eadd46c0 Mon Sep 17 00:00:00 2001 From: Shagnor Date: Wed, 28 Dec 2022 20:00:55 +1100 Subject: [PATCH] Start ansi formatting utilities ready for look command. --- Cargo.lock | 9 +- Cargo.toml | 2 + ansi/Cargo.toml | 7 + ansi/src/lib.rs | 229 ++++++++++++++++++ blastmud_game/Cargo.toml | 2 +- .../src/message_handler/new_session.rs | 2 +- .../src/message_handler/user_commands.rs | 25 +- .../message_handler/user_commands/agree.rs | 2 +- .../src/message_handler/user_commands/help.rs | 2 +- .../src/message_handler/user_commands/look.rs | 14 ++ .../src/message_handler/user_commands/quit.rs | 2 +- .../message_handler/user_commands/register.rs | 2 +- blastmud_game/src/static_content.rs | 2 +- blastmud_game/src/static_content/room.rs | 2 +- 14 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 ansi/Cargo.toml create mode 100644 ansi/src/lib.rs create mode 100644 blastmud_game/src/message_handler/user_commands/look.rs diff --git a/Cargo.lock b/Cargo.lock index dc754133..1c1ab904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,13 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi" +version = "0.1.0" +dependencies = [ + "ansi_macro", +] + [[package]] name = "ansi_macro" version = "0.1.0" @@ -103,7 +110,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" name = "blastmud_game" version = "0.1.0" dependencies = [ - "ansi_macro", + "ansi", "async-trait", "base64 0.20.0", "bcrypt", diff --git a/Cargo.toml b/Cargo.toml index dfdab68d..57e4241b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,6 @@ members = [ "blastmud_listener", "blastmud_interfaces", "blastmud_game", + "ansi_macro", + "ansi", ] diff --git a/ansi/Cargo.toml b/ansi/Cargo.toml new file mode 100644 index 00000000..3ad12337 --- /dev/null +++ b/ansi/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ansi" +version = "0.1.0" +edition = "2021" + +[dependencies] +ansi_macro = { path = "../ansi_macro" } diff --git a/ansi/src/lib.rs b/ansi/src/lib.rs new file mode 100644 index 00000000..9b7a9f34 --- /dev/null +++ b/ansi/src/lib.rs @@ -0,0 +1,229 @@ +pub use ansi_macro::ansi; +use std::rc::Rc; + +/// Removes all non-printable characters except tabs and newlines. +/// Doesn't attempt to remove printable characters as part of an +/// escape - so use this for untrusted input that you don't expect +/// to contain ansi escapes at all. +pub fn ignore_special_characters(input: &str) -> String { + input.chars().filter(|c| *c == '\t' || *c == '\n' || + (*c >= ' ' && *c <= '~')).collect() +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +struct AnsiState { + col: u64, + background: u64, // 0 means default. + foreground: u64, + bold: bool, + underline: bool, + strike: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +struct AnsiEvent<'l> ( + AnsiParseToken<'l>, + Rc +); + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +enum AnsiParseToken<'l> { + Character(char), + ControlSeq(&'l str), + Newline, +} + +/// Emits events with only LF, spaces, tabs, and a small set of +/// character attributes (colours, bold, underline). Anything else +/// sent will be emitted as printable characters. Tabs are replaced +/// with 4 spaces. +#[derive(Clone, Debug)] +struct AnsiIterator<'l> { + underlying: std::iter::Enumerate>, + input: &'l str, + state: Rc, + pending_col: bool, + inject_spaces: u64, +} + + +impl AnsiIterator<'_> { + fn new<'l>(input: &'l str) -> AnsiIterator<'l> { + AnsiIterator { underlying: input.chars().enumerate(), + input: input, + state: Rc::new(AnsiState { + col: 0, + background: 0, + foreground: 0, + bold: false, + underline: false, + strike: false + }), + pending_col: false, + inject_spaces: 0 + } + } +} + +impl <'l>Iterator for AnsiIterator<'l> { + type Item = AnsiEvent<'l>; + + fn next(self: &mut Self) -> Option> { + if self.pending_col { + Rc::make_mut(&mut self.state).col += 1; + self.pending_col = false; + } + if self.inject_spaces > 0 { + self.pending_col = true; + self.inject_spaces -= 1; + return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone())); + } + while let Some((i0, c)) = self.underlying.next() { + if c == '\n' { + Rc::make_mut(&mut self.state).col = 0; + return Some(AnsiEvent::<'l>(AnsiParseToken::Newline, self.state.clone())); + } else if c == '\t' { + for _ in 0..4 { + self.pending_col = true; + self.inject_spaces = 3; + return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone())); + } + } else if c >= ' ' && c <= '~' { + self.pending_col = true; + return Some(AnsiEvent::<'l>(AnsiParseToken::Character(c), self.state.clone())); + } else if c == '\x1b' { + if let Some((_, c2)) = self.underlying.next() { + if c2 != '[' { + continue; + } + } + if let Some((_, cs1)) = self.underlying.next() { + let mut imax = i0; + let mut cs_no: i64 = cs1 as i64 - b'0' as i64; + if cs_no < 0 || cs_no > 9 { + continue; + } + if let Some((i2, cs2)) = self.underlying.next() { + let cs_no2: i64 = cs2 as i64 - b'0' as i64; + if cs_no2 >= 0 && cs_no2 <= 9 { + if let Some((i3, cs3)) = self.underlying.next() { + if cs3 == 'm' { + cs_no *= 10; + cs_no += cs_no2; + imax = i3; + } else { continue; } + } + } else if cs2 != 'm' { + continue; + } else { + imax = i2; + } + let st = Rc::make_mut(&mut self.state); + match cs_no { + 0 => { + st.background = 0; + st.foreground = 0; + st.bold = false; + st.underline = false; + st.strike = false; + } + 1 => { st.bold = true; } + 4 => { st.underline = true; } + 9 => { st.strike = true; } + 24 => { st.underline = false; } + i if i >= 30 && i <= 37 => { + st.foreground = i as u64 - 29; + } + i if i >= 40 && i <= 47 => { + st.foreground = i as u64 - 39; + } + _ => continue + } + drop(st); + return Some(AnsiEvent::<'l>( + AnsiParseToken::ControlSeq( + &self.input[i0..(imax + 1)] + ), self.state.clone())); + } + } + } + } + None + } + +} + +/// Strips out basic colours / character formatting codes cleanly. Tabs are +/// changed to spaces, and newlines are preserved. All other ANSI non-printables +/// are stripped but might display incorrectly. +pub fn strip_special_characters(input: &str) -> String { + let mut buf: String = String::new(); + let it = AnsiIterator::new(input); + for AnsiEvent(e, _) in it { + match e { + AnsiParseToken::Character(c) => buf.push(c), + AnsiParseToken::Newline => buf.push('\n'), + _ => {} + } + } + buf +} + +/// Allows basic colours / character formatting codes. Tabs are +/// changed to spaces, and newlines are preserved. All other ANSI non-printables +/// are stripped but might display incorrectly. +pub fn limit_special_characters(input: &str) -> String { + let mut buf: String = String::new(); + let it = AnsiIterator::new(input); + for AnsiEvent(e, _) in it { + match e { + AnsiParseToken::Character(c) => buf.push(c), + AnsiParseToken::Newline => buf.push('\n'), + AnsiParseToken::ControlSeq(t) => buf.push_str(t) + } + } + buf +} + +/// Flows a second column around a first column, limiting the width of both +/// columns as specified, and adding a gutter. +pub fn flow_around(col1: &str, col1_width: u64, gutter: &str, + col2: &str, col2_width: u64) -> String { + "not yet".to_owned() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ignore_special_characters_removes_esc() { + assert_eq!(ignore_special_characters("hello\x1b[world"), "hello[world"); + } + + #[test] + fn strip_special_characters_makes_plaintext() { + assert_eq!(strip_special_characters("a\tb"), "a b"); + assert_eq!( + strip_special_characters(ansi!("helloworld")), + "helloworld"); + assert_eq!( + strip_special_characters("hello\r\x07world\n"), + "helloworld\n"); + assert_eq!( + strip_special_characters("hello\r\x07world\n"), + "helloworld\n"); + assert_eq!( + strip_special_characters("Test\x1b[5;5fing"), + "Test5fing"); + } + + #[test] + fn limit_special_characters_strips_some_things() { + assert_eq!(limit_special_characters(ansi!("abcd")), + ansi!("abcd")); + assert_eq!(limit_special_characters("Test\x1b[5;5fing"), + "Test5fing"); + } + +} diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index afcdbe73..a3853f1d 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] base64 = "0.20.0" blastmud_interfaces = { path = "../blastmud_interfaces" } -ansi_macro = { path = "../ansi_macro" } +ansi = { path = "../ansi" } deadpool = "0.9.5" deadpool-postgres = { version = "0.10.3", features = ["serde"] } futures = "0.3.25" diff --git a/blastmud_game/src/message_handler/new_session.rs b/blastmud_game/src/message_handler/new_session.rs index 658b5e53..2755a83b 100644 --- a/blastmud_game/src/message_handler/new_session.rs +++ b/blastmud_game/src/message_handler/new_session.rs @@ -1,7 +1,7 @@ use crate::message_handler::ListenerSession; use crate::DResult; use crate::db::DBPool; -use ansi_macro::ansi; +use ansi::ansi; use std::default::Default; use crate::models::session::Session; diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index fafcda76..27b4a9c5 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -1,20 +1,21 @@ use super::ListenerSession; use crate::DResult; use crate::db::{DBTrans, DBPool}; -use ansi_macro::ansi; +use ansi::ansi; use phf::phf_map; use async_trait::async_trait; use crate::models::{session::Session, user::User}; use log::warn; -mod parsing; -mod ignore; -mod help; -mod quit; -mod less_explicit_mode; -mod register; mod agree; +mod help; +mod ignore; +mod less_explicit_mode; mod login; +mod look; +mod parsing; +mod quit; +mod register; pub struct VerbContext<'l> { session: &'l ListenerSession, @@ -59,14 +60,16 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! { }; static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! { - "less_explicit_mode" => less_explicit_mode::VERB, - "register" => register::VERB, - "login" => login::VERB, + "agree" => agree::VERB, "connect" => login::VERB, - "agree" => agree::VERB + "less_explicit_mode" => less_explicit_mode::VERB, + "login" => login::VERB, + "register" => register::VERB, }; static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { + "l" => look::VERB, + "look" => look::VERB, }; fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> { diff --git a/blastmud_game/src/message_handler/user_commands/agree.rs b/blastmud_game/src/message_handler/user_commands/agree.rs index 95130312..1059e5be 100644 --- a/blastmud_game/src/message_handler/user_commands/agree.rs +++ b/blastmud_game/src/message_handler/user_commands/agree.rs @@ -1,7 +1,7 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error}; use crate::models::user::{User, UserTermData}; use async_trait::async_trait; -use ansi_macro::ansi; +use ansi::ansi; use chrono::Utc; pub struct Verb; diff --git a/blastmud_game/src/message_handler/user_commands/help.rs b/blastmud_game/src/message_handler/user_commands/help.rs index 9f9bc4f0..916f5573 100644 --- a/blastmud_game/src/message_handler/user_commands/help.rs +++ b/blastmud_game/src/message_handler/user_commands/help.rs @@ -3,7 +3,7 @@ use super::{ CommandHandlingError::UserError }; use async_trait::async_trait; -use ansi_macro::ansi; +use ansi::ansi; use phf::phf_map; static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs new file mode 100644 index 00000000..fd5239b1 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -0,0 +1,14 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError}; +use async_trait::async_trait; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + let user = ctx.user_dat.as_ref() + .ok_or_else(|| UserError("Not logged in".to_owned()))?; + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/quit.rs b/blastmud_game/src/message_handler/user_commands/quit.rs index 0dab4614..4b740564 100644 --- a/blastmud_game/src/message_handler/user_commands/quit.rs +++ b/blastmud_game/src/message_handler/user_commands/quit.rs @@ -2,7 +2,7 @@ use super::{ VerbContext, UserVerb, UserVerbRef, UResult }; use async_trait::async_trait; -use ansi_macro::ansi; +use ansi::ansi; pub struct Verb; #[async_trait] diff --git a/blastmud_game/src/message_handler/user_commands/register.rs b/blastmud_game/src/message_handler/user_commands/register.rs index ff22ce21..2e9d2be2 100644 --- a/blastmud_game/src/message_handler/user_commands/register.rs +++ b/blastmud_game/src/message_handler/user_commands/register.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use super::{user_error, parsing::parse_username}; use crate::models::{user::User, item::Item}; use chrono::Utc; -use ansi_macro::ansi; +use ansi::ansi; use tokio::time; pub struct Verb; diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs index 085ac493..82262e54 100644 --- a/blastmud_game/src/static_content.rs +++ b/blastmud_game/src/static_content.rs @@ -1,7 +1,6 @@ use crate::DResult; use crate::db::DBPool; use crate::models::item::Item; -use itertools::Itertools; use std::collections::{BTreeSet, BTreeMap}; use log::info; @@ -69,6 +68,7 @@ pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> { #[cfg(test)] mod test { + use itertools::Itertools; use super::*; #[test] diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index 0fda9676..077545e8 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -1,7 +1,7 @@ use super::StaticItem; use once_cell::sync::OnceCell; use std::collections::BTreeMap; -use ansi_macro::ansi; +use ansi::ansi; use crate::models::item::Item; pub struct Zone {