mirror of
https://github.com/LordMZTE/mcstat.git
synced 2024-05-05 06:11:11 +02:00
fix incorrect description width with formatting
This commit is contained in:
parent
8c7790e3e2
commit
656984ab69
|
@ -14,7 +14,6 @@ itertools = "0.10.3"
|
||||||
miette = { version = "4.3.0", features = ["fancy"] }
|
miette = { version = "4.3.0", features = ["fancy"] }
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
smart-default = "0.6.0"
|
smart-default = "0.6.0"
|
||||||
term_size = "0.3.2"
|
|
||||||
termcolor = "1.1.3"
|
termcolor = "1.1.3"
|
||||||
trust-dns-resolver = { version = "0.21.2", features = ["tokio-runtime"] }
|
trust-dns-resolver = { version = "0.21.2", features = ["tokio-runtime"] }
|
||||||
unicode-width = "0.1.9"
|
unicode-width = "0.1.9"
|
||||||
|
|
92
src/lib.rs
92
src/lib.rs
|
@ -1,17 +1,10 @@
|
||||||
use crate::output::Table;
|
use crate::output::Table;
|
||||||
use async_minecraft_ping::StatusResponse;
|
use async_minecraft_ping::StatusResponse;
|
||||||
use crossterm::{
|
|
||||||
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
|
|
||||||
ExecutableCommand,
|
|
||||||
};
|
|
||||||
use image::{DynamicImage, ImageFormat};
|
use image::{DynamicImage, ImageFormat};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use miette::{bail, miette, IntoDiagnostic, WrapErr};
|
use miette::{bail, miette, IntoDiagnostic, WrapErr};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{io::Cursor, net::IpAddr};
|
||||||
io::{self, Cursor, Write},
|
|
||||||
net::IpAddr,
|
|
||||||
};
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use trust_dns_resolver::TokioAsyncResolver;
|
use trust_dns_resolver::TokioAsyncResolver;
|
||||||
|
|
||||||
|
@ -96,87 +89,6 @@ pub async fn resolve_address(addr_and_port: &str) -> miette::Result<(String, u16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print mincraft-formatted text to `out` using crossterm
|
|
||||||
pub fn print_mc_formatted(s: &str, mut out: impl Write) -> io::Result<()> {
|
|
||||||
macro_rules! exec {
|
|
||||||
(fg, $color:ident) => {
|
|
||||||
exec!(SetForegroundColor(Color::$color))
|
|
||||||
};
|
|
||||||
|
|
||||||
(at, $attr:ident) => {
|
|
||||||
exec!(SetAttribute(Attribute::$attr))
|
|
||||||
};
|
|
||||||
|
|
||||||
($action:expr) => {{
|
|
||||||
out.execute($action)?;
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut splits = s.split('§');
|
|
||||||
if let Some(n) = splits.next() {
|
|
||||||
exec!(Print(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut empty = true;
|
|
||||||
for split in splits {
|
|
||||||
empty = false;
|
|
||||||
if let Some(c) = split.chars().next() {
|
|
||||||
match c {
|
|
||||||
// Colors
|
|
||||||
'0' => exec!(fg, Black),
|
|
||||||
'1' => exec!(fg, DarkBlue),
|
|
||||||
'2' => exec!(fg, DarkGreen),
|
|
||||||
'3' => exec!(fg, DarkCyan),
|
|
||||||
'4' => exec!(fg, DarkRed),
|
|
||||||
'5' => exec!(fg, DarkMagenta),
|
|
||||||
'6' => exec!(fg, DarkYellow),
|
|
||||||
'7' => exec!(fg, Grey),
|
|
||||||
'8' => exec!(fg, DarkGrey),
|
|
||||||
'9' => exec!(fg, Blue),
|
|
||||||
'a' => exec!(fg, Green),
|
|
||||||
'b' => exec!(fg, Cyan),
|
|
||||||
'c' => exec!(fg, Red),
|
|
||||||
'd' => exec!(fg, Magenta),
|
|
||||||
'e' => exec!(fg, Yellow),
|
|
||||||
'f' => exec!(fg, White),
|
|
||||||
|
|
||||||
// Formatting
|
|
||||||
// Obfuscated. This is the closest thing, althogh not many terminals support it.
|
|
||||||
'k' => exec!(at, RapidBlink),
|
|
||||||
'l' => exec!(at, Bold),
|
|
||||||
'm' => exec!(at, CrossedOut),
|
|
||||||
'n' => exec!(at, Underlined),
|
|
||||||
'o' => exec!(at, Italic),
|
|
||||||
'r' => exec!(ResetColor),
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
exec!(Print(&split[1..]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to reset color if there were no escape codes.
|
|
||||||
if !empty {
|
|
||||||
exec!(ResetColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mc_formatted_to_ansi(s: &str) -> io::Result<String> {
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut c = Cursor::new(&mut bytes);
|
|
||||||
print_mc_formatted(s, &mut c)?;
|
|
||||||
|
|
||||||
// this shouldn't be able to fail, as we started of with a valid utf8 string.
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let out = String::from_utf8(bytes).unwrap();
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let out = unsafe { String::from_utf8_unchecked(bytes) };
|
|
||||||
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// formats a iterator to a readable list
|
/// formats a iterator to a readable list
|
||||||
///
|
///
|
||||||
/// if `second_column`, the right strings will also be displayed
|
/// if `second_column`, the right strings will also be displayed
|
||||||
|
@ -187,7 +99,7 @@ pub fn get_table<'a>(
|
||||||
if second_column {
|
if second_column {
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
table.small_entry(entry.0, entry.1);
|
table.small_entry(entry.0, entry.1.to_string());
|
||||||
}
|
}
|
||||||
let mut cursor = Cursor::new(Vec::<u8>::new());
|
let mut cursor = Cursor::new(Vec::<u8>::new());
|
||||||
table.print(&mut cursor).unwrap();
|
table.print(&mut cursor).unwrap();
|
||||||
|
|
27
src/main.rs
27
src/main.rs
|
@ -8,9 +8,8 @@ use tokio::time;
|
||||||
|
|
||||||
use mcstat::{
|
use mcstat::{
|
||||||
get_table,
|
get_table,
|
||||||
mc_formatted_to_ansi,
|
|
||||||
none_if_empty,
|
none_if_empty,
|
||||||
output::Table,
|
output::{McFormatContent, Table},
|
||||||
parse_base64_image,
|
parse_base64_image,
|
||||||
resolve_address,
|
resolve_address,
|
||||||
EitherStatusResponse,
|
EitherStatusResponse,
|
||||||
|
@ -184,13 +183,7 @@ fn format_table(
|
||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
|
|
||||||
if let Some((w, _)) = term_size::dimensions() {
|
if let Some(s) = none_if_empty!(McFormatContent(response.description.get_text().clone())) {
|
||||||
table.max_block_width = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = none_if_empty!(mc_formatted_to_ansi(response.description.get_text())
|
|
||||||
.unwrap_or_else(|e| format!("Error: {}", e)))
|
|
||||||
{
|
|
||||||
table.big_entry("Description", s);
|
table.big_entry("Description", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,26 +191,24 @@ fn format_table(
|
||||||
let desc = &big_desc.extra;
|
let desc = &big_desc.extra;
|
||||||
let txt = desc.iter().map(|p| p.text.clone()).collect::<String>();
|
let txt = desc.iter().map(|p| p.text.clone()).collect::<String>();
|
||||||
if let Some(s) = none_if_empty!(txt) {
|
if let Some(s) = none_if_empty!(txt) {
|
||||||
table.big_entry("Extra Description", s);
|
table.big_entry("Extra Description", McFormatContent(s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(s) = none_if_empty!(
|
if let Some(s) = none_if_empty!(McFormatContent(player_sample)) {
|
||||||
mc_formatted_to_ansi(&player_sample).unwrap_or_else(|e| format!("Error: {}", e))
|
|
||||||
) {
|
|
||||||
table.big_entry("Player Sample", s);
|
table.big_entry("Player Sample", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.blank();
|
table.blank();
|
||||||
|
|
||||||
if let Some(s) = none_if_empty!(&response.version.name) {
|
if let Some(s) = none_if_empty!(response.version.name.clone()) {
|
||||||
table.small_entry("Server Version", s);
|
table.small_entry("Server Version", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.small_entry("Online Players", &response.players.online);
|
table.small_entry("Online Players", response.players.online.to_string());
|
||||||
table.small_entry("Max Players", &response.players.max);
|
table.small_entry("Max Players", response.players.max.to_string());
|
||||||
table.small_entry("Ping", ping);
|
table.small_entry("Ping", ping.to_string());
|
||||||
table.small_entry("Protocol Version", &response.version.protocol);
|
table.small_entry("Protocol Version", response.version.protocol.to_string());
|
||||||
|
|
||||||
table.blank();
|
table.blank();
|
||||||
|
|
||||||
|
|
171
src/output.rs
171
src/output.rs
|
@ -1,16 +1,17 @@
|
||||||
use smart_default::SmartDefault;
|
use crossterm::{
|
||||||
|
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
|
ExecutableCommand,
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{max, min},
|
cmp::max,
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
#[derive(SmartDefault)]
|
#[derive(Default)]
|
||||||
pub struct Table {
|
pub struct Table {
|
||||||
pub entries: Vec<Box<dyn TableEntry>>,
|
pub entries: Vec<Box<dyn TableEntry>>,
|
||||||
pub small_entry_width: usize,
|
pub small_entry_width: usize,
|
||||||
#[default(usize::MAX)]
|
|
||||||
pub max_block_width: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Table {
|
impl Table {
|
||||||
|
@ -34,20 +35,17 @@ impl Table {
|
||||||
self.entries.push(Box::new(BlankTableEntry));
|
self.entries.push(Box::new(BlankTableEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn small_entry(&mut self, name: impl ToString, val: impl ToString) {
|
pub fn small_entry(&mut self, name: impl ToString, val: impl TableContent + 'static) {
|
||||||
let name = name.to_string();
|
let name = name.to_string();
|
||||||
self.set_small_width(name.width());
|
self.set_small_width(name.width());
|
||||||
|
|
||||||
self.entries
|
self.entries
|
||||||
.push(Box::new(SmallTableEntry(name, val.to_string())));
|
.push(Box::new(SmallTableEntry(name, Box::new(val))));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn big_entry(&mut self, name: impl ToString, val: impl ToString) {
|
pub fn big_entry(&mut self, name: impl ToString, val: impl TableContent + 'static) {
|
||||||
self.entries.push(Box::new(BigTableEntry::new(
|
self.entries
|
||||||
name.to_string(),
|
.push(Box::new(BigTableEntry::new(name.to_string(), val)));
|
||||||
val.to_string(),
|
|
||||||
self.max_block_width,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_small_width(&mut self, width: usize) {
|
fn set_small_width(&mut self, width: usize) {
|
||||||
|
@ -57,57 +55,152 @@ impl Table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait TableContent {
|
||||||
|
fn width(&self) -> usize;
|
||||||
|
fn write_to(&self, out: &mut dyn Write) -> io::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableContent for String {
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
self.lines().map(|s| s.width()).max().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to(&self, out: &mut dyn Write) -> io::Result<()> {
|
||||||
|
out.write_all(self.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table content of a pretty string with minecraft-formatted markup
|
||||||
|
pub struct McFormatContent(pub String);
|
||||||
|
|
||||||
|
impl McFormatContent {
|
||||||
|
// compatibility with the `none_if_empty` macro
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableContent for McFormatContent {
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
.lines()
|
||||||
|
.map(|l| {
|
||||||
|
// need to count chars because of § being 2 bytes
|
||||||
|
l.chars().count() - l.matches('§').count() * 2
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to(&self, out: &mut dyn Write) -> io::Result<()> {
|
||||||
|
macro_rules! exec {
|
||||||
|
(fg, $color:ident) => {
|
||||||
|
exec!(SetForegroundColor(Color::$color))
|
||||||
|
};
|
||||||
|
|
||||||
|
(at, $attr:ident) => {
|
||||||
|
exec!(SetAttribute(Attribute::$attr))
|
||||||
|
};
|
||||||
|
|
||||||
|
($action:expr) => {{
|
||||||
|
out.execute($action)?;
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut splits = self.0.split('§');
|
||||||
|
if let Some(n) = splits.next() {
|
||||||
|
exec!(Print(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut empty = true;
|
||||||
|
for split in splits {
|
||||||
|
empty = false;
|
||||||
|
if let Some(c) = split.chars().next() {
|
||||||
|
match c {
|
||||||
|
// Colors
|
||||||
|
'0' => exec!(fg, Black),
|
||||||
|
'1' => exec!(fg, DarkBlue),
|
||||||
|
'2' => exec!(fg, DarkGreen),
|
||||||
|
'3' => exec!(fg, DarkCyan),
|
||||||
|
'4' => exec!(fg, DarkRed),
|
||||||
|
'5' => exec!(fg, DarkMagenta),
|
||||||
|
'6' => exec!(fg, DarkYellow),
|
||||||
|
'7' => exec!(fg, Grey),
|
||||||
|
'8' => exec!(fg, DarkGrey),
|
||||||
|
'9' => exec!(fg, Blue),
|
||||||
|
'a' => exec!(fg, Green),
|
||||||
|
'b' => exec!(fg, Cyan),
|
||||||
|
'c' => exec!(fg, Red),
|
||||||
|
'd' => exec!(fg, Magenta),
|
||||||
|
'e' => exec!(fg, Yellow),
|
||||||
|
'f' => exec!(fg, White),
|
||||||
|
|
||||||
|
// Formatting
|
||||||
|
// Obfuscated. This is the closest thing, althogh not many terminals support it.
|
||||||
|
'k' => exec!(at, RapidBlink),
|
||||||
|
'l' => exec!(at, Bold),
|
||||||
|
'm' => exec!(at, CrossedOut),
|
||||||
|
'n' => exec!(at, Underlined),
|
||||||
|
'o' => exec!(at, Italic),
|
||||||
|
'r' => exec!(ResetColor),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
exec!(Print(&split[1..]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to reset color if there were no escape codes.
|
||||||
|
if !empty {
|
||||||
|
exec!(ResetColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait TableEntry {
|
pub trait TableEntry {
|
||||||
fn print(&self, out: &mut dyn Write, table: &Table) -> io::Result<()>;
|
fn print(&self, out: &mut dyn Write, table: &Table) -> io::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SmallTableEntry(String, String);
|
pub struct SmallTableEntry(String, Box<dyn TableContent>);
|
||||||
|
|
||||||
impl TableEntry for SmallTableEntry {
|
impl TableEntry for SmallTableEntry {
|
||||||
fn print(&self, out: &mut dyn Write, table: &Table) -> io::Result<()> {
|
fn print(&self, out: &mut dyn Write, table: &Table) -> io::Result<()> {
|
||||||
writeln!(
|
write!(
|
||||||
out,
|
out,
|
||||||
"{: <width$} | {}",
|
"{: <width$} | ",
|
||||||
self.0,
|
self.0,
|
||||||
self.1,
|
|
||||||
width = table.small_entry_width
|
width = table.small_entry_width
|
||||||
)
|
)?;
|
||||||
|
self.1.write_to(out)?;
|
||||||
|
out.write_all(b"\n")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BigTableEntry {
|
pub struct BigTableEntry {
|
||||||
name: String,
|
name: String,
|
||||||
val: String,
|
val: Box<dyn TableContent>,
|
||||||
width: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableEntry for BigTableEntry {
|
impl TableEntry for BigTableEntry {
|
||||||
fn print(&self, out: &mut dyn Write, _table: &Table) -> io::Result<()> {
|
fn print(&self, out: &mut dyn Write, _table: &Table) -> io::Result<()> {
|
||||||
writeln!(
|
let width = max(self.val.width(), self.name.width() + 4);
|
||||||
out,
|
|
||||||
"{:=^width$}\n{}\n{:=<width$}",
|
writeln!(out, "{:=^width$}", self.name)?;
|
||||||
self.name,
|
self.val.write_to(out)?;
|
||||||
self.val,
|
writeln!(out, "\n{:=<width$}", "")?;
|
||||||
"",
|
|
||||||
width = self.width,
|
Ok(())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BigTableEntry {
|
impl BigTableEntry {
|
||||||
pub fn new(name: String, val: String, maxwidth: usize) -> Self {
|
pub fn new(name: String, val: impl TableContent + 'static) -> Self {
|
||||||
let val_width = min(
|
|
||||||
max(
|
|
||||||
val.lines().map(|s| s.width()).max().unwrap_or_default(),
|
|
||||||
name.width() + 4,
|
|
||||||
),
|
|
||||||
maxwidth,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
width: max(name.width(), val_width),
|
|
||||||
name,
|
name,
|
||||||
val,
|
val: Box::new(val),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue