mirror of
https://github.com/Cyrix126/gupaxx.git
synced 2024-12-22 14:49:21 +00:00
feat: construct XvB tab
feat: add Hero checkbox ui and hovering help feat: add private stats ui and thread feat: debug error of API connection in console
This commit is contained in:
parent
f2d0c9a288
commit
0a9375130f
7 changed files with 234 additions and 105 deletions
|
@ -161,7 +161,7 @@ path_xmr: {:#?}\n
|
|||
}
|
||||
Tab::Xvb => {
|
||||
debug!("App | Entering [XvB] Tab");
|
||||
crate::disk::state::Xvb::show(&mut self.state.xvb, self.size, &self.state.p2pool.address, ctx, ui, &self.xvb_api);
|
||||
crate::disk::state::Xvb::show(&mut self.state.xvb, self.size, &self.state.p2pool.address, ctx, ui, &self.xvb_api, lock!(self.xvb).is_alive());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -361,7 +361,7 @@ fn xvb(
|
|||
xvb_api: &Arc<Mutex<PubXvbApi>>,
|
||||
) {
|
||||
//
|
||||
let api = lock!(xvb_api);
|
||||
let api = &lock!(xvb_api).stats_pub;
|
||||
let enabled = xvb_alive;
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.group(|ui| {
|
||||
|
@ -477,7 +477,6 @@ fn xvb(
|
|||
)),
|
||||
);
|
||||
}
|
||||
drop(api);
|
||||
});
|
||||
// by round
|
||||
});
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use egui::TextStyle::Name;
|
||||
use egui::{Hyperlink, Image, Label, RichText, TextEdit, Vec2};
|
||||
use egui::{vec2, Hyperlink, Image, Layout, RichText, TextEdit, Ui, Vec2};
|
||||
use log::debug;
|
||||
|
||||
use crate::helper::xvb::PubXvbApi;
|
||||
use crate::utils::constants::{GREEN, LIGHT_GRAY, ORANGE, RED, XVB_HELP, XVB_TOKEN_LEN};
|
||||
use crate::utils::constants::{
|
||||
GREEN, LIGHT_GRAY, ORANGE, RED, XVB_DONATED_1H_FIELD, XVB_DONATED_24H_FIELD, XVB_FAILURE_FIELD,
|
||||
XVB_HELP, XVB_HERO_SELECT, XVB_TOKEN_FIELD, XVB_TOKEN_LEN,
|
||||
};
|
||||
use crate::utils::macros::lock;
|
||||
use crate::utils::regex::Regexes;
|
||||
use crate::{
|
||||
|
@ -22,12 +25,13 @@ impl crate::disk::state::Xvb {
|
|||
_ctx: &egui::Context,
|
||||
ui: &mut egui::Ui,
|
||||
api: &Arc<Mutex<PubXvbApi>>,
|
||||
xvb_is_alive: bool,
|
||||
) {
|
||||
ui.reset_style();
|
||||
let website_height = size.y / 10.0;
|
||||
// let width = size.x - SPACE;
|
||||
// let height = size.y - SPACE;
|
||||
let width = size.x;
|
||||
let text_edit = size.y / 25.0;
|
||||
// logo and website link
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_sized(
|
||||
|
@ -61,43 +65,103 @@ impl crate::disk::state::Xvb {
|
|||
});
|
||||
// input token
|
||||
let len_token = format!("{}", self.token.len());
|
||||
let text_check;
|
||||
let color;
|
||||
if self.token.is_empty() {
|
||||
text_check = format!("[{}/{}] ➖", len_token, XVB_TOKEN_LEN);
|
||||
color = LIGHT_GRAY;
|
||||
let (text, color) = if self.token.is_empty() {
|
||||
(
|
||||
format!("{} [{}/{}] ➖", XVB_TOKEN_FIELD, len_token, XVB_TOKEN_LEN),
|
||||
LIGHT_GRAY,
|
||||
)
|
||||
} else if self.token.parse::<u32>().is_ok() && self.token.len() < XVB_TOKEN_LEN {
|
||||
text_check = format!("[{}/{}] ", len_token, XVB_TOKEN_LEN);
|
||||
color = GREEN;
|
||||
(
|
||||
format!("{} [{}/{}]", XVB_TOKEN_FIELD, len_token, XVB_TOKEN_LEN),
|
||||
GREEN,
|
||||
)
|
||||
} else if self.token.parse::<u32>().is_ok() && self.token.len() == XVB_TOKEN_LEN {
|
||||
text_check = "✔".to_string();
|
||||
color = GREEN;
|
||||
(format!("{} ✔", XVB_TOKEN_FIELD), GREEN)
|
||||
} else {
|
||||
text_check = format!("[{}/{}] ❌", len_token, XVB_TOKEN_LEN);
|
||||
color = RED;
|
||||
}
|
||||
(
|
||||
format!("{} [{}/{}] ❌", XVB_TOKEN_FIELD, len_token, XVB_TOKEN_LEN),
|
||||
RED,
|
||||
)
|
||||
};
|
||||
// let width = width - SPACE;
|
||||
// ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0);
|
||||
ui.group(|ui| {
|
||||
let width = width - SPACE;
|
||||
ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0);
|
||||
ui.label("Your Token:");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(
|
||||
[width / 8.0, text_edit],
|
||||
TextEdit::singleline(&mut self.token),
|
||||
);
|
||||
|
||||
ui.add(Label::new(RichText::new(text_check).color(color)))
|
||||
});
|
||||
})
|
||||
.response
|
||||
.on_hover_text_at_pointer(XVB_HELP);
|
||||
// why does this group is not centered into the parent group ?
|
||||
ui.with_layout(Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.group(|ui| {
|
||||
ui.colored_label(color, text);
|
||||
// ui.add_sized(
|
||||
// [width / 8.0, text_edit],
|
||||
// Label::new(RichText::new(text).color(color)),
|
||||
// );
|
||||
ui.add(
|
||||
TextEdit::singleline(&mut self.token)
|
||||
.char_limit(XVB_TOKEN_LEN)
|
||||
.desired_width(width / 8.0)
|
||||
.vertical_align(egui::Align::Center),
|
||||
);
|
||||
})
|
||||
.response
|
||||
.on_hover_text_at_pointer(XVB_HELP);
|
||||
// hero option
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(width / 24.0);
|
||||
ui.checkbox(&mut self.hero, "Hero")
|
||||
.on_hover_text(XVB_HERO_SELECT);
|
||||
})
|
||||
});
|
||||
})
|
||||
});
|
||||
// need to warn the user if no address is set in p2pool tab
|
||||
if !Regexes::addr_ok(address) {
|
||||
debug!("XvB Tab | Rendering warning text");
|
||||
ui.label(RichText::new("You don't have any payout address set in the P2pool Tab !\nXvB process needs one to function properly.")
|
||||
.color(ORANGE));
|
||||
}
|
||||
// hero option
|
||||
// private stats
|
||||
let priv_stats = &lock!(api).stats_priv;
|
||||
ui.set_enabled(xvb_is_alive);
|
||||
// ui.vertical_centered(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// widget takes a third less space for two separator.
|
||||
let width_stat =
|
||||
(ui.available_width() / 3.0) - (12.0 + ui.style().spacing.item_spacing.x) / 3.0;
|
||||
// 0.0 means minimum
|
||||
let height_stat = 0.0;
|
||||
let size_stat = vec2(width_stat, height_stat);
|
||||
ui.add_sized(size_stat, |ui: &mut Ui| {
|
||||
ui.group(|ui| {
|
||||
let size_stat = vec2(
|
||||
ui.available_width(),
|
||||
0.0, // + ui.spacing().item_spacing.y,
|
||||
);
|
||||
ui.add_sized(size_stat, |ui: &mut Ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(XVB_FAILURE_FIELD);
|
||||
ui.label(priv_stats.fails.to_string());
|
||||
})
|
||||
.response
|
||||
});
|
||||
ui.separator();
|
||||
ui.add_sized(size_stat, |ui: &mut Ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(XVB_DONATED_1H_FIELD);
|
||||
ui.label(priv_stats.donor_1hr_avg.to_string());
|
||||
})
|
||||
.response
|
||||
});
|
||||
ui.separator();
|
||||
ui.add_sized(size_stat, |ui: &mut Ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(XVB_DONATED_24H_FIELD);
|
||||
ui.label(priv_stats.donor_24hr_avg.to_string());
|
||||
})
|
||||
.response
|
||||
});
|
||||
})
|
||||
.response
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::{bail, Result};
|
||||
use hyper::StatusCode;
|
||||
use hyper_tls::HttpsConnector;
|
||||
use anyhow::{Result};
|
||||
|
||||
|
||||
|
||||
|
||||
use super::*;
|
||||
use crate::{components::node::RemoteNode, disk::status::*};
|
||||
|
@ -246,6 +247,7 @@ pub struct Xmrig {
|
|||
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize, Default)]
|
||||
pub struct Xvb {
|
||||
pub token: String,
|
||||
pub hero: bool,
|
||||
pub node: XvbNode,
|
||||
}
|
||||
|
||||
|
@ -328,36 +330,6 @@ impl Default for P2pool {
|
|||
}
|
||||
}
|
||||
|
||||
impl Xvb {
|
||||
pub async fn is_token_exist(address: String, token: String) -> Result<()> {
|
||||
let https = HttpsConnector::new();
|
||||
let client = hyper::Client::builder().build(https);
|
||||
if let Ok(request) = hyper::Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!(
|
||||
"{}/cgi-bin/p2pool_bonus_history_api.cgi?address={}&token={}",
|
||||
XVB_URL, address, token
|
||||
))
|
||||
.body(hyper::Body::empty())
|
||||
{
|
||||
match client.request(request).await {
|
||||
Ok(resp) => match resp.status() {
|
||||
StatusCode::OK => Ok(()),
|
||||
StatusCode::UNPROCESSABLE_ENTITY => {
|
||||
bail!("the token is invalid for this xmr address.")
|
||||
}
|
||||
_ => bail!("The status of the response is not expected"),
|
||||
},
|
||||
Err(err) => {
|
||||
bail!("error from response: {}", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("request could not be build")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Xmrig {
|
||||
fn with_threads(max_threads: usize, current_threads: usize) -> Self {
|
||||
let xmrig = Self::default();
|
||||
|
|
|
@ -99,6 +99,7 @@ mod test {
|
|||
|
||||
[xvb]
|
||||
token = ""
|
||||
hero = false
|
||||
node = "Europe"
|
||||
[version]
|
||||
gupax = "v1.3.0"
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use anyhow::{bail, Result};
|
||||
use bytes::Bytes;
|
||||
use derive_more::Display;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::StatusCode;
|
||||
use hyper_tls::HttpsConnector;
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::Deserialize;
|
||||
|
@ -10,8 +13,8 @@ use std::{
|
|||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::utils::constants::XVB_URL;
|
||||
use crate::{
|
||||
disk::state::Xvb,
|
||||
helper::{ProcessSignal, ProcessState},
|
||||
utils::{
|
||||
constants::{HORI_CONSOLE, XVB_URL_PUBLIC_API},
|
||||
|
@ -69,8 +72,8 @@ impl Helper {
|
|||
let gui_api = Arc::clone(&lock!(helper).gui_api_xvb);
|
||||
let pub_api = Arc::clone(&lock!(helper).pub_api_xvb);
|
||||
let process = Arc::clone(&lock!(helper).xvb);
|
||||
let state_xvb = state_xvb.clone();
|
||||
let state_p2pool = state_p2pool.clone();
|
||||
let state_xvb_check = state_xvb.clone();
|
||||
let state_p2pool_check = state_p2pool.clone();
|
||||
|
||||
// 2. Set process state
|
||||
debug!("XvB | Setting process state...");
|
||||
|
@ -83,7 +86,7 @@ impl Helper {
|
|||
// verify if token and address are existent on XvB server
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let resp: anyhow::Result<()> = rt.block_on(async move {
|
||||
Xvb::is_token_exist(state_p2pool.address, state_xvb.token).await?;
|
||||
XvbPrivStats::request_api(&state_p2pool_check.address, &state_xvb_check.token).await?;
|
||||
Ok(())
|
||||
});
|
||||
match resp {
|
||||
|
@ -105,9 +108,17 @@ impl Helper {
|
|||
lock2!(helper, xvb).state = ProcessState::NotMining;
|
||||
}
|
||||
}
|
||||
|
||||
let state_xvb_thread = state_xvb.clone();
|
||||
let state_p2pool_thread = state_p2pool.clone();
|
||||
thread::spawn(move || {
|
||||
Self::spawn_xvb_watchdog(client, gui_api, pub_api, process);
|
||||
Self::spawn_xvb_watchdog(
|
||||
client,
|
||||
gui_api,
|
||||
pub_api,
|
||||
process,
|
||||
&state_xvb_thread,
|
||||
&state_p2pool_thread,
|
||||
);
|
||||
});
|
||||
}
|
||||
#[tokio::main]
|
||||
|
@ -116,6 +127,8 @@ impl Helper {
|
|||
gui_api: Arc<Mutex<PubXvbApi>>,
|
||||
pub_api: Arc<Mutex<PubXvbApi>>,
|
||||
process: Arc<Mutex<Process>>,
|
||||
state_xvb: &crate::disk::state::Xvb,
|
||||
state_p2pool: &crate::disk::state::P2pool,
|
||||
) {
|
||||
info!("XvB started");
|
||||
|
||||
|
@ -142,14 +155,11 @@ impl Helper {
|
|||
// if since is 0, send request because it's the first time.
|
||||
let since = lock!(gui_api).tick;
|
||||
if since >= 60 || since == 0 {
|
||||
// *lock!(pub_api) = PubXvbApi::new();
|
||||
// *lock!(gui_api) = PubXvbApi::new();
|
||||
debug!("XvB Watchdog | Attempting HTTP API request...");
|
||||
match PubXvbApi::request_xvb_public_api(client.clone(), XVB_URL_PUBLIC_API).await {
|
||||
debug!("XvB Watchdog | Attempting HTTP public API request...");
|
||||
match XvbPubStats::request_api(client.clone()).await {
|
||||
Ok(new_data) => {
|
||||
debug!("XvB Watchdog | HTTP API request OK");
|
||||
*lock!(&pub_api) = new_data;
|
||||
lock!(gui_api).tick += 0;
|
||||
lock!(&pub_api).stats_pub = new_data;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
|
@ -168,6 +178,45 @@ impl Helper {
|
|||
break;
|
||||
}
|
||||
}
|
||||
debug!("XvB Watchdog | Attempting HTTP private API request...");
|
||||
match XvbPrivStats::request_api(&state_p2pool.address, &state_xvb.token).await {
|
||||
Ok(b) => {
|
||||
debug!("XvB Watchdog | HTTP API request OK");
|
||||
let new_data = match serde_json::from_slice::<XvbPrivStats>(&b) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
warn!("XvB Watchdog | Data provided from private API is not deserializ-able.Error: {}", e);
|
||||
// output the error to console
|
||||
if let Err(e) = writeln!(
|
||||
lock!(gui_api).output,
|
||||
"XvB Watchdog | Data provided from private API is not deserializ-able.Error: {}", e
|
||||
) {
|
||||
error!("XvB Watchdog | GUI status write failed: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
lock!(&pub_api).stats_priv = new_data;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"XvB Watchdog | Could not send HTTP private API request to: {}\n:{}",
|
||||
XVB_URL, err
|
||||
);
|
||||
// output the error to console
|
||||
if let Err(e) = writeln!(
|
||||
lock!(gui_api).output,
|
||||
"Failure to retrieve private stats from {}",
|
||||
XVB_URL
|
||||
) {
|
||||
error!("XvB Watchdog | GUI status write failed: {}", e);
|
||||
}
|
||||
lock!(process).state = ProcessState::Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lock!(gui_api).tick += 0;
|
||||
}
|
||||
|
||||
lock!(gui_api).tick += 1;
|
||||
|
@ -188,14 +237,17 @@ impl Helper {
|
|||
}
|
||||
//---------------------------------------------------------------------------------------------------- Public XvB API
|
||||
use serde_this_or_that::as_u64;
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PubXvbApi {
|
||||
#[serde(skip)]
|
||||
pub output: String,
|
||||
#[serde(skip)]
|
||||
pub uptime: HumanTime,
|
||||
#[serde(skip)]
|
||||
pub tick: u8,
|
||||
pub stats_pub: XvbPubStats,
|
||||
pub stats_priv: XvbPrivStats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct XvbPubStats {
|
||||
pub time_remain: u32, // remaining time of round in minutes
|
||||
pub bonus_hr: f64,
|
||||
pub donate_hr: f64, // donated hr from all donors
|
||||
|
@ -217,6 +269,62 @@ pub struct PubXvbApi {
|
|||
pub reward_yearly: Vec<f64>,
|
||||
}
|
||||
|
||||
impl XvbPubStats {
|
||||
#[inline]
|
||||
// Send an HTTP request to XvB's API, serialize it into [Self] and return it
|
||||
async fn request_api(
|
||||
client: hyper::Client<HttpsConnector<HttpConnector>>,
|
||||
) -> std::result::Result<Self, anyhow::Error> {
|
||||
let request = hyper::Request::builder()
|
||||
.method("GET")
|
||||
.uri(XVB_URL_PUBLIC_API)
|
||||
.body(hyper::Body::empty())?;
|
||||
let response =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(8), client.request(request))
|
||||
.await?;
|
||||
// let response = client.request(request).await;
|
||||
|
||||
let body = hyper::body::to_bytes(response?.body_mut()).await?;
|
||||
Ok(serde_json::from_slice::<Self>(&body)?)
|
||||
}
|
||||
}
|
||||
impl XvbPrivStats {
|
||||
pub async fn request_api(address: &str, token: &str) -> Result<Bytes> {
|
||||
let https = HttpsConnector::new();
|
||||
let client = hyper::Client::builder().build(https);
|
||||
if let Ok(request) = hyper::Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!(
|
||||
"{}/cgi-bin/p2pool_bonus_history_api.cgi?address={}&token={}",
|
||||
XVB_URL, address, token
|
||||
))
|
||||
.body(hyper::Body::empty())
|
||||
{
|
||||
match client.request(request).await {
|
||||
Ok(mut resp) => match resp.status() {
|
||||
StatusCode::OK => Ok(hyper::body::to_bytes(resp.body_mut()).await?),
|
||||
StatusCode::UNPROCESSABLE_ENTITY => {
|
||||
bail!("the token is invalid for this xmr address.")
|
||||
}
|
||||
_ => bail!("The status of the response is not expected"),
|
||||
},
|
||||
Err(err) => {
|
||||
bail!("error from response: {}", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("request could not be build")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct XvbPrivStats {
|
||||
pub fails: u8,
|
||||
pub donor_1hr_avg: f32,
|
||||
pub donor_24hr_avg: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Display, Deserialize)]
|
||||
pub enum XvbRound {
|
||||
#[default]
|
||||
|
@ -257,24 +365,6 @@ impl PubXvbApi {
|
|||
..pub_api.clone()
|
||||
};
|
||||
}
|
||||
#[inline]
|
||||
// Send an HTTP request to XvB's API, serialize it into [Self] and return it
|
||||
async fn request_xvb_public_api(
|
||||
client: hyper::Client<HttpsConnector<HttpConnector>>,
|
||||
api_uri: &str,
|
||||
) -> std::result::Result<Self, anyhow::Error> {
|
||||
let request = hyper::Request::builder()
|
||||
.method("GET")
|
||||
.uri(api_uri)
|
||||
.body(hyper::Body::empty())?;
|
||||
let response =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(8), client.request(request))
|
||||
.await?;
|
||||
// let response = client.request(request).await;
|
||||
|
||||
let body = hyper::body::to_bytes(response?.body_mut()).await?;
|
||||
Ok(serde_json::from_slice::<Self>(&body)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn signal_interrupt(
|
||||
|
@ -323,8 +413,7 @@ fn signal_interrupt(
|
|||
mod test {
|
||||
use std::thread;
|
||||
|
||||
use super::PubXvbApi;
|
||||
use crate::utils::constants::XVB_URL_PUBLIC_API;
|
||||
use super::XvbPubStats;
|
||||
use hyper::Client;
|
||||
use hyper_tls::HttpsConnector;
|
||||
|
||||
|
@ -337,9 +426,7 @@ mod test {
|
|||
dbg!(new_data);
|
||||
}
|
||||
#[tokio::main]
|
||||
async fn corr(client: Client<HttpsConnector<hyper::client::HttpConnector>>) -> PubXvbApi {
|
||||
PubXvbApi::request_xvb_public_api(client, XVB_URL_PUBLIC_API)
|
||||
.await
|
||||
.unwrap()
|
||||
async fn corr(client: Client<HttpsConnector<hyper::client::HttpConnector>>) -> XvbPubStats {
|
||||
XvbPubStats::request_api(client).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -411,6 +411,12 @@ pub const XVB_HELP: &str = "You need to register an account by clicking on the l
|
|||
pub const XVB_URL: &str = "https://xmrvsbeast.com";
|
||||
pub const XVB_URL_PUBLIC_API: &str = "https://xmrvsbeast.com/p2pool/stats";
|
||||
pub const XVB_TOKEN_LEN: usize = 9;
|
||||
pub const XVB_HERO_SELECT: &str =
|
||||
"Donate all spared hashrate to the raffle, even if there is more than enough to be in the most highest round type possible";
|
||||
pub const XVB_TOKEN_FIELD: &str = "Token";
|
||||
pub const XVB_FAILURE_FIELD: &str = "Failures";
|
||||
pub const XVB_DONATED_1H_FIELD: &str = "Donated last hour";
|
||||
pub const XVB_DONATED_24H_FIELD: &str = "Donated last 24 hours";
|
||||
|
||||
// CLI argument messages
|
||||
pub const ARG_HELP: &str = r#"USAGE: ./gupax [--flag]
|
||||
|
|
Loading…
Reference in a new issue