mirror of
https://github.com/LordMZTE/mcstat.git
synced 2024-05-03 21:31:11 +02:00
switch to structopt
This commit is contained in:
parent
95d856eab4
commit
1efda6db50
21
Cargo.toml
21
Cargo.toml
|
@ -7,23 +7,24 @@ edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.32"
|
anyhow = "1.0.40"
|
||||||
asciify = "0.1.6"
|
asciify = "0.1.6"
|
||||||
async-minecraft-ping = { git = "https://github.com/LordMZTE/async-minecraft-ping.git", tag = "v0.2.4" }
|
async-minecraft-ping = { git = "https://github.com/LordMZTE/async-minecraft-ping.git", tag = "v0.2.5" }
|
||||||
clap = { version = "^2.33", features = ["yaml"] }
|
clap = "2.33.3"
|
||||||
image = "0.23.9"
|
image = "0.23.14"
|
||||||
image-base64 = "0.1.0"
|
image-base64 = "0.1.0"
|
||||||
itertools = "0.9.0"
|
itertools = "0.10.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
serde_json = "1.0.58"
|
serde_json = "1.0.64"
|
||||||
smart-default = "0.6.0"
|
smart-default = "0.6.0"
|
||||||
|
structopt = "0.3.21"
|
||||||
term_size = "0.3.2"
|
term_size = "0.3.2"
|
||||||
termcolor = "1"
|
termcolor = "1.1.2"
|
||||||
tokio = { version = "0.2.22", features = ["full"] }
|
tokio = { version = "1.5.0", features = ["full"] }
|
||||||
unicode-width = "0.1.8"
|
unicode-width = "0.1.8"
|
||||||
yaml-rust = "0.3.5"
|
yaml-rust = "0.4.5"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
yaml-rust = "0.3.5"
|
yaml-rust = "0.4.5"
|
||||||
|
|
19
build.rs
19
build.rs
|
@ -1,19 +0,0 @@
|
||||||
use clap::Shell;
|
|
||||||
use std::{env, str::FromStr};
|
|
||||||
|
|
||||||
include!("src/cli.rs");
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let outdir = match env::var_os("OUT_DIR") {
|
|
||||||
None => return,
|
|
||||||
Some(d) => d,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut app = get_app();
|
|
||||||
for s in Shell::variants()
|
|
||||||
.iter()
|
|
||||||
.map(|v| Shell::from_str(v).unwrap())
|
|
||||||
{
|
|
||||||
app.gen_completions("mcstat", s, &outdir);
|
|
||||||
}
|
|
||||||
}
|
|
66
src/args.yml
66
src/args.yml
|
@ -1,66 +0,0 @@
|
||||||
name: "mcstat"
|
|
||||||
about: "queries information about a minecraft server"
|
|
||||||
args:
|
|
||||||
- ip:
|
|
||||||
help: "the ip of the server to ping. you may also specify the port, if it is not specified or invalid it will default to 25565"
|
|
||||||
takes_value: true
|
|
||||||
index: 1
|
|
||||||
required: true
|
|
||||||
- protocol-version:
|
|
||||||
long: "protocol"
|
|
||||||
help: "the protocol version to use"
|
|
||||||
default_value: "751"
|
|
||||||
takes_value: true
|
|
||||||
- timeout:
|
|
||||||
long: "timeout"
|
|
||||||
short: t
|
|
||||||
help: "the time before the server ping times out in milliseconds"
|
|
||||||
takes_value: true
|
|
||||||
default_value: "5000"
|
|
||||||
- raw:
|
|
||||||
short: r
|
|
||||||
help: "if supplied, the raw json response from the server will be printed"
|
|
||||||
- mods:
|
|
||||||
short: m
|
|
||||||
help: "if supplied, a mod list will be printed"
|
|
||||||
- modversions:
|
|
||||||
short: v
|
|
||||||
help: "if supplied, mods will also have their version info printed"
|
|
||||||
requires: "mods"
|
|
||||||
- channels:
|
|
||||||
long: "channels"
|
|
||||||
help: "displays forge mod channels if the server sends them"
|
|
||||||
|
|
||||||
# IMAGE ARGS
|
|
||||||
# TODO due to a bug in clap, the image argument is always required because size has a default value
|
|
||||||
- image:
|
|
||||||
short: "i"
|
|
||||||
help: "if the server's favicon should be printed as ASCII art"
|
|
||||||
required: false
|
|
||||||
- color:
|
|
||||||
short: "c"
|
|
||||||
help: "if the favicon image should be printed with ANSI color formatting or monochrome"
|
|
||||||
- size:
|
|
||||||
short: "s"
|
|
||||||
help: "the size of the image"
|
|
||||||
takes_value: true
|
|
||||||
default_value: "16"
|
|
||||||
- deep:
|
|
||||||
short: "d"
|
|
||||||
help: "if provided the ascii image will have more different characters"
|
|
||||||
- invert:
|
|
||||||
short: "n"
|
|
||||||
help: "inverts the ascii image thickness"
|
|
||||||
|
|
||||||
groups:
|
|
||||||
- img-flags:
|
|
||||||
# TODO uncomment once bug is fixed
|
|
||||||
# requires:
|
|
||||||
# - image
|
|
||||||
multiple: true
|
|
||||||
required: false
|
|
||||||
args:
|
|
||||||
- color
|
|
||||||
- size
|
|
||||||
- deep
|
|
||||||
- invert
|
|
14
src/cli.rs
14
src/cli.rs
|
@ -1,14 +0,0 @@
|
||||||
use clap::{App, YamlLoader};
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use yaml_rust::Yaml;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref YAML: Yaml = YamlLoader::load_from_str(include_str!("args.yml"))
|
|
||||||
.unwrap()
|
|
||||||
.pop()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_app() -> App<'static, 'static> {
|
|
||||||
App::from_yaml(&YAML)
|
|
||||||
}
|
|
|
@ -71,6 +71,8 @@ pub fn get_table<'a>(
|
||||||
table.print(&mut cursor).unwrap();
|
table.print(&mut cursor).unwrap();
|
||||||
String::from_utf8(cursor.into_inner()).unwrap()
|
String::from_utf8(cursor.into_inner()).unwrap()
|
||||||
} else {
|
} else {
|
||||||
entries.map(|x| x.0).intersperse("\n").collect()
|
// this syntax is used due to a nightly function which will be added to rust
|
||||||
|
// also called intersperse
|
||||||
|
Itertools::intersperse(entries.map(|x| x.0), "\n").collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
151
src/main.rs
151
src/main.rs
|
@ -1,31 +1,88 @@
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use asciify::AsciiBuilder;
|
use asciify::AsciiBuilder;
|
||||||
use async_minecraft_ping::{ConnectionConfig, ServerDescription, StatusResponse};
|
use async_minecraft_ping::{ConnectionConfig, ServerDescription, StatusResponse};
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use structopt::StructOpt;
|
||||||
use termcolor::{Buffer, BufferWriter, ColorChoice, WriteColor};
|
use termcolor::{Buffer, BufferWriter, ColorChoice, WriteColor};
|
||||||
use time::{Duration, Instant};
|
use time::{Duration, Instant};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
use mcstat::{get_table, none_if_empty, output::Table, remove_formatting, AsciiConfig};
|
use mcstat::{get_table, none_if_empty, output::Table, remove_formatting, AsciiConfig};
|
||||||
|
|
||||||
mod cli;
|
#[derive(Debug, StructOpt)]
|
||||||
|
#[structopt(
|
||||||
|
name = "mcstat",
|
||||||
|
about = "queries information about a minecraft server"
|
||||||
|
)]
|
||||||
|
struct Opt {
|
||||||
|
#[structopt(
|
||||||
|
index = 1,
|
||||||
|
help = "the ip of the server to ping. you may also specify the port, if it is not \
|
||||||
|
specified or invalid it will default to 25565"
|
||||||
|
)]
|
||||||
|
ip: String,
|
||||||
|
|
||||||
/// this message is used if getting a value from the arguments fails
|
#[structopt(
|
||||||
const ARGUMENT_FAIL_MESSAGE: &str = "failed to get value from args";
|
long = "protocol",
|
||||||
|
help = "the protocol version to use",
|
||||||
|
default_value = "751"
|
||||||
|
)]
|
||||||
|
protocol_version: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
help = "the time before the server ping times out in milliseconds",
|
||||||
|
default_value = "5000"
|
||||||
|
)]
|
||||||
|
timeout: u64,
|
||||||
|
|
||||||
|
#[structopt(long, short, help = "print raw json response")]
|
||||||
|
raw: bool,
|
||||||
|
|
||||||
|
#[structopt(long, short, help = "print mod list")]
|
||||||
|
mods: bool,
|
||||||
|
|
||||||
|
#[structopt(long, short = "v", requires = "mods", help = "also prints mod versions")]
|
||||||
|
modversions: bool,
|
||||||
|
|
||||||
|
#[structopt(long, help = "displays forge mod channels if the server sends them")]
|
||||||
|
channels: bool,
|
||||||
|
|
||||||
|
#[structopt(long, short, help = "print the server's favicon as ASCII art")]
|
||||||
|
image: bool,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
short = "c",
|
||||||
|
requires = "image",
|
||||||
|
help = "print the server's favicon with color"
|
||||||
|
)]
|
||||||
|
color: bool,
|
||||||
|
|
||||||
|
#[structopt(short, requires = "image", help = "size of the favicon ascii art")]
|
||||||
|
size: Option<u32>,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
requires = "image",
|
||||||
|
help = "print the ascii art favicon with more diverse chars"
|
||||||
|
)]
|
||||||
|
deep: bool,
|
||||||
|
|
||||||
|
#[structopt(long, short = "n", requires = "image", help = "inverts ascii art favicon")]
|
||||||
|
invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let matches = cli::get_app().get_matches();
|
let opt = Opt::from_args_safe()?;
|
||||||
|
|
||||||
// region Network
|
|
||||||
let mut ip = matches
|
|
||||||
.value_of("ip")
|
|
||||||
.context(ARGUMENT_FAIL_MESSAGE)?
|
|
||||||
.splitn(2, ':');
|
|
||||||
|
|
||||||
|
let mut ip = opt.ip.splitn(2, ':');
|
||||||
let config = ConnectionConfig::build(ip.next().context("invalid ip")?.to_owned())
|
let config = ConnectionConfig::build(ip.next().context("invalid ip")?.to_owned())
|
||||||
.with_port(
|
.with_port(
|
||||||
ip.next()
|
ip.next()
|
||||||
|
@ -33,26 +90,10 @@ async fn main() -> Result<()> {
|
||||||
.and_then(|p| if p > 0 { Ok(p) } else { Err(()) })
|
.and_then(|p| if p > 0 { Ok(p) } else { Err(()) })
|
||||||
.unwrap_or(25565),
|
.unwrap_or(25565),
|
||||||
)
|
)
|
||||||
.with_protocol_version(
|
.with_protocol_version(opt.protocol_version);
|
||||||
matches
|
|
||||||
.value_of("protocol-version")
|
|
||||||
.context(ARGUMENT_FAIL_MESSAGE)?
|
|
||||||
.parse()
|
|
||||||
.context("invalid protocol version")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// create timeout for server connection
|
// create timeout for server connection
|
||||||
let mut timeout = time::delay_for(Duration::from_millis(
|
let (raw_response, ping) = time::timeout(Duration::from_millis(opt.timeout), async {
|
||||||
matches
|
|
||||||
.value_of("timeout")
|
|
||||||
.context(ARGUMENT_FAIL_MESSAGE)?
|
|
||||||
.parse()
|
|
||||||
.context("timeout is invalid value")?,
|
|
||||||
));
|
|
||||||
|
|
||||||
let (raw_response, ping) = tokio::select! {
|
|
||||||
_ = &mut timeout => Err(anyhow!("Connection to server timed out")),
|
|
||||||
r = async {
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut con = config.connect().await?;
|
let mut con = config.connect().await?;
|
||||||
// we end the timer here, because at this point, we've sent ONE request to the server,
|
// we end the timer here, because at this point, we've sent ONE request to the server,
|
||||||
|
@ -62,11 +103,10 @@ async fn main() -> Result<()> {
|
||||||
let end_time = Instant::now();
|
let end_time = Instant::now();
|
||||||
|
|
||||||
let status = con.status_raw().await?;
|
let status = con.status_raw().await?;
|
||||||
Ok((status, end_time - start_time))
|
Result::<_, anyhow::Error>::Ok((status, end_time - start_time))
|
||||||
} => r,
|
}).await.context("Connection to server timed out.")??;
|
||||||
}?;
|
|
||||||
|
|
||||||
if matches.is_present("raw") {
|
if opt.raw {
|
||||||
println!("{}", raw_response);
|
println!("{}", raw_response);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -75,22 +115,17 @@ async fn main() -> Result<()> {
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Image
|
// region Image
|
||||||
let image_size: u32 = matches
|
|
||||||
.value_of("size")
|
|
||||||
.context("failed to get value from args")?
|
|
||||||
.parse()
|
|
||||||
.context("image size must be number")?;
|
|
||||||
let mut image = None;
|
let mut image = None;
|
||||||
|
|
||||||
if let (Some(favicon), true) = (&response.favicon, matches.is_present("image")) {
|
if let (Some(favicon), true) = (&response.favicon, opt.image) {
|
||||||
// The image parsing and asciifying is done while the table is printing
|
// The image parsing and asciifying is done while the table is printing
|
||||||
image = Some(tokio::spawn(asciify_base64_image(
|
image = Some(tokio::spawn(asciify_base64_image(
|
||||||
favicon.clone(),
|
favicon.clone(),
|
||||||
AsciiConfig {
|
AsciiConfig {
|
||||||
size: Some(image_size),
|
size: Some(opt.size.unwrap_or(16)),
|
||||||
colored: matches.is_present("color"),
|
colored: opt.color,
|
||||||
deep: matches.is_present("deep"),
|
deep: opt.deep,
|
||||||
invert: matches.is_present("invert"),
|
invert: opt.invert,
|
||||||
},
|
},
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -99,16 +134,16 @@ async fn main() -> Result<()> {
|
||||||
// region printing
|
// region printing
|
||||||
// if the server has mods, and the user hasn't used the -m argument, notify
|
// if the server has mods, and the user hasn't used the -m argument, notify
|
||||||
// that.
|
// that.
|
||||||
if let (false, Some(_)) = (matches.is_present("mods"), response.forge_mod_info()) {
|
if let (false, Some(_)) = (opt.mods, response.forge_mod_info()) {
|
||||||
println!("This server has mods. To show them use the -m argument\n")
|
println!("This server has mods. To show them use the -m argument\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
format_table(
|
format_table(
|
||||||
&response,
|
&response,
|
||||||
ping.as_millis(),
|
ping.as_millis(),
|
||||||
matches.is_present("mods"),
|
opt.mods,
|
||||||
matches.is_present("modversions"),
|
opt.modversions,
|
||||||
matches.is_present("channels"),
|
opt.channels,
|
||||||
)
|
)
|
||||||
.stdout()?;
|
.stdout()?;
|
||||||
|
|
||||||
|
@ -164,15 +199,19 @@ fn format_table(
|
||||||
modversions: bool,
|
modversions: bool,
|
||||||
channels: bool,
|
channels: bool,
|
||||||
) -> Table {
|
) -> Table {
|
||||||
let player_sample = response
|
// this syntax is used due to a nightly function which will be added to rust
|
||||||
.players
|
// also called intersperse
|
||||||
.sample
|
let player_sample = Itertools::intersperse(
|
||||||
.as_ref()
|
response
|
||||||
.unwrap_or(&vec![])
|
.players
|
||||||
.iter()
|
.sample
|
||||||
.map(|p| p.name.as_str())
|
.as_ref()
|
||||||
.intersperse("\n")
|
.unwrap_or(&vec![])
|
||||||
.collect::<String>();
|
.iter()
|
||||||
|
.map(|p| p.name.as_str()),
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue