initial functional release

This commit is contained in:
Penelope Gwen 2025-11-05 17:53:31 -08:00
commit 9178c029bf
6 changed files with 1377 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1114
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "mc-server-info"
version = "0.1.0"
edition = "2024"
[dependencies]
ansi_term = "0.12.1"
clap = { version = "4.5.51", features = ["derive"] }
config = { version = "0.15.18", features = ["toml"] }
mccolors-rust = "0.1.3"
rust-mc-status = "1.1.1"
serde = "1.0.228"
serde_json = "1.0.145"
text-style = "0.3.0"
tokio = { version = "*", features = ["full"] }
xdg = "3.0.0"

22
README.md Normal file
View file

@ -0,0 +1,22 @@
# mc-server-info
how many of these are there now
uses [rust-mc-status](https://github.com/NameOfShadow/rust-mc-status) to retrieve minecraft server statuses
## features
- reads server list and configuration from config.toml
- can output to either CLI or JSON format
- saves server icon to cache directory
## todo
- read server/server list from arguments
- more configurable output
## Usage
`mc-server-info [cli|json]`
## disclaimer
- I made this primarily for my own use as a preconfigured way of passing data to [eww](https://github.com/elkowar/eww/) widgets without running a billion scripts around other tools
- This tool *might* `sudo rm -rf --no-preserve-root /` on any machines that run or access LLMs and other generative model software

60
license.md Normal file
View file

@ -0,0 +1,60 @@
# 🏳️‍🌈 Opinionated Queer License v1.3
© Copyright [Penelope Gwen](https://pogmom.me)
## Permissions
The creators of this Work (“The Licensor”) grant permission
to any person, group or legal entity that doesn't violate the prohibitions below (“The User”),
to do everything with this Work that would otherwise infringe their copyright or any patent claims,
subject to the following conditions:
## Obligations
The User must give appropriate credit to the Licensor,
provide a copy of this license or a (clickable, if the medium allows) link to
[oql.avris.it/license/v1.3](https://oql.avris.it/license/v1.2),
and indicate whether and what kind of changes were made.
The User may do so in any reasonable manner,
but not in any way that suggests the Licensor endorses the User or their use.
## Prohibitions
No one may use this Work for prejudiced or bigoted purposes, including but not limited to:
racism, xenophobia, queerphobia, queer exclusionism, homophobia, transphobia, enbyphobia, misogyny.
No one may use this Work to inflict or facilitate violence or abuse of human rights,
as defined in either of the following documents:
[Universal Declaration of Human Rights](https://www.un.org/en/about-us/universal-declaration-of-human-rights),
[European Convention on Human Rights](https://prd-echr.coe.int/web/echr/european-convention-on-human-rights)
along with the rulings of the [European Court of Human Rights](https://www.echr.coe.int/).
No entity that commits such abuses or materially supports entities that do
may use the Work for any reason.
No law enforcement, carceral institutions, immigration enforcement entities, military entities or military contractors
may use the Work for any reason. This also applies to any individuals employed by those entities.
No business entity where the ratio of pay (salaried, freelance, stocks, or other benefits)
between the highest and lowest individual in the entity is greater than 50 : 1
may use the Work for any reason.
No private business run for profit with more than a thousand employees
may use the Work for any reason.
Unless the User has made substantial changes to the Work,
or uses it only as a part of a new work (eg. as a library, as a part of an anthology, etc.),
they are prohibited from selling the Work.
That prohibition includes processing the Work with machine learning models.
## Sanctions
If the Licensor notifies the User that they have not complied with the rules of the license,
they can keep their license by complying within 30 days after the notice.
If they do not do so, their license ends immediately.
## Warranty
This Work is provided “as is”, without warranty of any kind, express or implied.
The Licensor will not be liable to anyone for any damages related to the Work or this license,
under any kind of legal claim as far as the law allows.

164
src/main.rs Normal file
View file

@ -0,0 +1,164 @@
use rust_mc_status::{McClient, ServerData, ServerEdition};
use serde_json::json;
use std::{path::PathBuf, process::exit, str::FromStr, time::Duration};
use config::Config;
use std::{fs,io::Write};
use serde::{Deserialize, Serialize};
use mccolors_rust::{mcreplace, mcremove};
use clap::{Parser, ValueEnum};
use ansi_term::Style;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(value_enum, default_value_t = DisplayMode::Cli)]
display_mode: DisplayMode
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum DisplayMode {
Cli,
Json
}
#[derive(Serialize,Deserialize,Debug,Default)]
struct ServerConf {
address: String,
edition: String
}
#[derive(Serialize,Deserialize,Debug,Default)]
struct ConfigData {
servers: Vec<ServerConf>
}
#[derive(Clone,Debug,Default,Serialize)]
struct Players {
online: i64,
max: i64,
list: Option<Vec<String>>
}
#[derive(Clone,Debug,Default,Serialize)]
struct ServerDetails {
address: String,
edition: String,
version: String,
motd: String,
players: Players,
ping: f64,
icon_path: Option<PathBuf>
// icon_path: Option<String>
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let xdg_dirs = xdg::BaseDirectories::with_prefix("mc-server-info");
let config_path = xdg_dirs
.place_config_file("config.toml")
.expect("cannot create configuration directory");
let cache_path = match xdg_dirs.get_cache_home() {
Some(c) => c,
None => PathBuf::from_str("/tmp/").unwrap(),
};
if !cache_path.exists() {
let _ = fs::DirBuilder::new().create(&cache_path);
}
if !config_path.exists() {
let mut config_file = fs::File::create(config_path)?;
write!(&mut config_file, "")?;
exit(1);
}
let mut server_list: Vec<ServerDetails> = vec![];
let conf = Config::builder().add_source(config::File::with_name(config_path.to_str().unwrap())).build().unwrap();
match conf.try_deserialize::<ConfigData>() {
Ok(config) => {
for server in config.servers {
server_list.push(get_server_info(server, cli.display_mode, cache_path.clone()).await.unwrap());
}
},
Err(e) => eprintln!("{:?}", e),
}
match cli.display_mode {
DisplayMode::Cli => {
for server in server_list {
output_cli(server)
}
},
DisplayMode::Json => println!("{}", json!(server_list)),
}
Ok(())
}
fn output_cli(server: ServerDetails) {
println!("{} ({} {})",Style::new().bold().paint(server.address),server.edition,server.version);
println!(" {}",server.motd.replace("\n", "\n "));
println!(" {}ms",server.ping);
println!(" {}/{} {}",Style::new().bold().paint(server.players.online.to_string()),Style::new().bold().paint(server.players.max.to_string()),Style::new().bold().paint("players online"));
for player in server.players.list.unwrap_or_default() {
println!(" {}",player);
}
}
//#[tokio::main]
async fn get_server_info(server: ServerConf, mode: DisplayMode, cache_dir: PathBuf) -> Result<ServerDetails, String> {
let client = McClient::new()
.with_timeout(Duration::from_secs(5))
.with_max_parallel(10);
let edition = match server.edition.as_str() {
"java" => ServerEdition::Java,
"bedrock" => ServerEdition::Bedrock,
_ => ServerEdition::Java
};
let mut server_data: ServerDetails = ServerDetails { address: server.address.clone(), edition: server.edition, ..Default::default() };//Default::default();
// server_data.address = server.address.clone();
// server_data.edition = server.edition;
match client.ping(&server.address, edition).await {
Ok(status) => {
server_data.ping = status.latency;
match status.data {
ServerData::Java(java_status) => {
server_data.motd = match mode {
DisplayMode::Cli => mcreplace(&java_status.description.replace('§', "&")),
DisplayMode::Json => mcremove(&java_status.description.replace('§', "&")),
};
server_data.version = java_status.version.clone().name;
server_data.icon_path = Some(cache_dir.join(format!("{}.png",server_data.address))); //cache_dir.join(format!("{}.png",server_data.address)));
//println!("{:?}",server_data.icon_path);
//java_status.save_favicon(server_data.icon_path.unwrap().as_str());
match java_status.save_favicon(server_data.icon_path.clone().unwrap().to_str().unwrap()) {
Ok(_) => (),
Err(_) => server_data.icon_path = None,
}
server_data.players.max = java_status.players.max;
server_data.players.online = java_status.players.online;
server_data.players.list = match java_status.players.sample {
Some(player_list) => {
let mut player_names: Vec<String> = vec![];
for player in player_list {
player_names.push(player.name);
}
Some(player_names)
},
None => None,
}
},
ServerData::Bedrock(bedrock_status) => {
server_data.motd = match mode {
DisplayMode::Cli => format!("{}\n{}",mcreplace(&bedrock_status.motd.replace('§', "&")),mcreplace(&bedrock_status.motd2.replace('§', "&"))),
DisplayMode::Json => format!("{}\n{}",mcremove(&bedrock_status.motd.replace('§', "&")),mcremove(&bedrock_status.motd2.replace('§', "&"))),
};
server_data.icon_path = None;
server_data.version = bedrock_status.version;
server_data.players.max = bedrock_status.max_players.parse::<i64>().unwrap();
server_data.players.online = bedrock_status.online_players.parse::<i64>().unwrap();
server_data.players.list = None;
},
}
},
Err(e) => eprintln!("{}", e),
}
Ok(server_data)
}