Merge branch 'egui'

This commit is contained in:
hinto.janai 2023-12-28 16:03:00 -05:00
commit 3b7181e9c0
No known key found for this signature in database
GPG key ID: D47CE05FA175A499
10 changed files with 551 additions and 570 deletions

954
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -33,10 +33,12 @@ benri = "0.1.12"
bytes = "1.4.0" bytes = "1.4.0"
dirs = "5.0.1" dirs = "5.0.1"
#-------------------------------------------------------------------------------- #--------------------------------------------------------------------------------
egui = "0.19.0" egui = "0.24.1"
egui_extras = { version = "0.19.0", features = ["image"] } egui_extras = { version = "0.24.1", features = ["image"] }
## 2023-12-28: https://github.com/hinto-janai/gupax/issues/68
eframe = { version = "0.24.1", default-features = false, features = ["glow"] }
## Update 2023-Feb-06: The below gets fixed by using the [wgpu] backend instead of [glow] ## 2023-02-06: The below gets fixed by using the [wgpu] backend instead of [glow]
## It also fixes crashes on CPU-based graphics. Only used for Windows. ## It also fixes crashes on CPU-based graphics. Only used for Windows.
## Using [wgpu] actually crashes macOS (fixed in 0.20.x though). ## Using [wgpu] actually crashes macOS (fixed in 0.20.x though).
@ -79,8 +81,6 @@ strip-ansi-escapes = "0.2.0"
tar = "0.4.38" tar = "0.4.38"
flate2 = "1.0" flate2 = "1.0"
sudo = "0.6.0" sudo = "0.6.0"
## [glow] backend for macOS/Linux.
eframe = { version = "0.19.0", default-features = false, features = ["glow"] }
# macOS # macOS
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
@ -105,7 +105,6 @@ tls-api-native-tls = "0.9.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
zip = "0.6.6" zip = "0.6.6"
is_elevated = "0.1.2" is_elevated = "0.1.2"
eframe = { version = "0.19.0", default-features = false, features = ["glow"] }
# For Windows build (icon) # For Windows build (icon)
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]

2
external/egui vendored

@ -1 +1 @@
Subproject commit 62b4d427c01201898914594a9d00d1576bc23432 Subproject commit 9cf535bd50b2602b0bce45c718d164bae2b4ed77

View file

@ -425,6 +425,8 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
stroke: Stroke::new(1.0, Color32::from_gray(255)), stroke: Stroke::new(1.0, Color32::from_gray(255)),
}; };
// Based off default dark() mode.
// https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1210
let widgets = Widgets { let widgets = Widgets {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
bg_fill: BG, bg_fill: BG,
@ -432,6 +434,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
rounding: Rounding::same(10.0), rounding: Rounding::same(10.0),
expansion: 0.0, expansion: 0.0,
weak_bg_fill: BG,
}, },
inactive: WidgetVisuals { inactive: WidgetVisuals {
bg_fill: Color32::from_gray(50), bg_fill: Color32::from_gray(50),
@ -439,6 +442,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
rounding: Rounding::same(10.0), rounding: Rounding::same(10.0),
expansion: 0.0, expansion: 0.0,
weak_bg_fill: Color32::from_gray(50),
}, },
hovered: WidgetVisuals { hovered: WidgetVisuals {
bg_fill: Color32::from_gray(80), bg_fill: Color32::from_gray(80),
@ -446,6 +450,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
rounding: Rounding::same(10.0), rounding: Rounding::same(10.0),
expansion: 1.0, expansion: 1.0,
weak_bg_fill: Color32::from_gray(80),
}, },
active: WidgetVisuals { active: WidgetVisuals {
bg_fill: Color32::from_gray(55), bg_fill: Color32::from_gray(55),
@ -453,6 +458,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
fg_stroke: Stroke::new(2.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE),
rounding: Rounding::same(10.0), rounding: Rounding::same(10.0),
expansion: 1.0, expansion: 1.0,
weak_bg_fill: Color32::from_gray(120),
}, },
open: WidgetVisuals { open: WidgetVisuals {
bg_fill: Color32::from_gray(27), bg_fill: Color32::from_gray(27),
@ -460,12 +466,12 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
rounding: Rounding::same(10.0), rounding: Rounding::same(10.0),
expansion: 0.0, expansion: 0.0,
weak_bg_fill: Color32::from_gray(120),
}, },
}; };
// https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1113
Visuals { Visuals {
dark_mode: true,
override_text_color: None,
widgets, widgets,
selection, selection,
hyperlink_color: Color32::from_rgb(90, 170, 255), hyperlink_color: Color32::from_rgb(90, 170, 255),
@ -477,12 +483,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
window_rounding: Rounding::same(6.0), window_rounding: Rounding::same(6.0),
window_shadow: Shadow::big_dark(), window_shadow: Shadow::big_dark(),
popup_shadow: Shadow::small_dark(), popup_shadow: Shadow::small_dark(),
resize_corner_size: 12.0, ..Visuals::dark()
text_cursor_width: 2.0,
text_cursor_preview: false,
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true,
collapsing_header_frame: false,
} }
}); });

View file

@ -259,7 +259,8 @@ impl crate::disk::Gupax {
ui.separator(); ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = None; } if ui.add_sized([width, height], SelectableLabel::new(self.ratio == None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = None; }
if ui.add_sized([width, height], Button::new("Set")).on_hover_text(GUPAX_SET).clicked() { if ui.add_sized([width, height], Button::new("Set")).on_hover_text(GUPAX_SET).clicked() {
frame.set_window_size(Vec2::new(self.selected_width as f32, self.selected_height as f32)); let size = Vec2::new(self.selected_width as f32, self.selected_height as f32);
ui.ctx().send_viewport_cmd(egui::viewport::ViewportCommand::InnerSize(size));
} }
})}); })});
} }

View file

@ -33,7 +33,7 @@ compile_error!("gupax is only built for windows/macos/linux");
// egui/eframe // egui/eframe
use egui::{ use egui::{
TextStyle::*, TextStyle::*,
color::Color32, Color32,
FontFamily::Proportional, FontFamily::Proportional,
TextStyle,Spinner, TextStyle,Spinner,
Layout,Align, Layout,Align,
@ -174,8 +174,7 @@ pub struct App {
} }
impl App { impl App {
fn cc(cc: &eframe::CreationContext<'_>, app: Self) -> Self { fn cc(cc: &eframe::CreationContext<'_>, resolution: Vec2, app: Self) -> Self {
let resolution = cc.integration_info.window_info.size;
init_text_styles(&cc.egui_ctx, resolution[0], crate::free::clamp_scale(app.state.gupax.selected_scale)); init_text_styles(&cc.egui_ctx, resolution[0], crate::free::clamp_scale(app.state.gupax.selected_scale));
cc.egui_ctx.set_visuals(VISUALS.clone()); cc.egui_ctx.set_visuals(VISUALS.clone());
Self { Self {
@ -812,7 +811,7 @@ impl KeyPressed {
//---------------------------------------------------------------------------------------------------- Init functions //---------------------------------------------------------------------------------------------------- Init functions
#[inline(always)] #[inline(always)]
fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) { fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) {
let scale = width / 30.0; let scale = width / 35.5;
let mut style = (*ctx.style()).clone(); let mut style = (*ctx.style()).clone();
style.text_styles = [ style.text_styles = [
(Small, FontId::new(scale/3.0, egui::FontFamily::Monospace)), (Small, FontId::new(scale/3.0, egui::FontFamily::Monospace)),
@ -828,7 +827,10 @@ fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) {
style.spacing.icon_width_inner = width / 35.0; style.spacing.icon_width_inner = width / 35.0;
style.spacing.icon_width = width / 25.0; style.spacing.icon_width = width / 25.0;
style.spacing.icon_spacing = 20.0; style.spacing.icon_spacing = 20.0;
style.spacing.scroll_bar_width = width / 150.0; style.spacing.scroll = egui::style::ScrollStyle {
bar_width: width / 150.0,
..egui::style::ScrollStyle::solid()
};
ctx.set_style(style); ctx.set_style(style);
// Make sure scale f32 is a regular number. // Make sure scale f32 is a regular number.
let pixels_per_point = crate::free::clamp_scale(pixels_per_point); let pixels_per_point = crate::free::clamp_scale(pixels_per_point);
@ -875,18 +877,18 @@ fn init_logger(now: Instant) {
#[inline(always)] #[inline(always)]
fn init_options(initial_window_size: Option<Vec2>) -> NativeOptions { fn init_options(initial_window_size: Option<Vec2>) -> NativeOptions {
let mut options = eframe::NativeOptions::default(); let mut options = eframe::NativeOptions::default();
options.min_window_size = Some(Vec2::new(APP_MIN_WIDTH, APP_MIN_HEIGHT)); options.viewport.min_inner_size = Some(Vec2::new(APP_MIN_WIDTH, APP_MIN_HEIGHT));
options.max_window_size = Some(Vec2::new(APP_MAX_WIDTH, APP_MAX_HEIGHT)); options.viewport.max_inner_size = Some(Vec2::new(APP_MAX_WIDTH, APP_MAX_HEIGHT));
options.initial_window_size = initial_window_size; options.viewport.inner_size = initial_window_size;
options.follow_system_theme = false; options.follow_system_theme = false;
options.default_theme = eframe::Theme::Dark; options.default_theme = eframe::Theme::Dark;
let icon = image::load_from_memory(BYTES_ICON).expect("Failed to read icon bytes").to_rgba8(); let icon = image::load_from_memory(BYTES_ICON).expect("Failed to read icon bytes").to_rgba8();
let (icon_width, icon_height) = icon.dimensions(); let (icon_width, icon_height) = icon.dimensions();
options.icon_data = Some(eframe::IconData { options.viewport.icon = Some(Arc::new(egui::viewport::IconData {
rgba: icon.into_raw(), rgba: icon.into_raw(),
width: icon_width, width: icon_width,
height: icon_height, height: icon_height,
}); }));
info!("init_options() ... OK"); info!("init_options() ... OK");
options options
} }
@ -1172,42 +1174,55 @@ fn main() {
Err(e) => warn!("Could not cleanup [gupax_tmp] folders: {}", e), Err(e) => warn!("Could not cleanup [gupax_tmp] folders: {}", e),
} }
let resolution = Vec2::new(selected_width, selected_height);
// Run Gupax. // Run Gupax.
info!("/*************************************/ Init ... OK /*************************************/"); info!("/*************************************/ Init ... OK /*************************************/");
eframe::run_native(&app.name_version.clone(), options, Box::new(|cc| Box::new(App::cc(cc, app))),); eframe::run_native(&app.name_version.clone(), options, Box::new(move |cc| Box::new(App::cc(cc, resolution, app))),);
} }
impl eframe::App for App { impl eframe::App for App {
#[inline(always)] #[inline]
fn on_close_event(&mut self) -> bool {
if self.state.gupax.ask_before_quit {
// If we're already on the [ask_before_quit] screen and
// the user tried to exit again, exit.
if self.error_state.quit_twice {
if self.state.gupax.save_before_quit { self.save_before_quit(); }
return true
}
// Else, set the error
self.error_state.set("", ErrorFerris::Oops, ErrorButtons::StayQuit);
self.error_state.quit_twice = true;
false
// Else, just quit.
} else {
if self.state.gupax.save_before_quit { self.save_before_quit(); }
true
}
}
#[inline(always)]
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// *-------* // *-------*
// | DEBUG | // | DEBUG |
// *-------* // *-------*
debug!("App | ----------- Start of [update()] -----------"); debug!("App | ----------- Start of [update()] -----------");
// If closing.
// Used to be `eframe::App::on_close_event(&mut self) -> bool`.
let close_signal = ctx.input(|input| {
use egui::viewport::ViewportCommand;
if !input.viewport().close_requested() {
return None;
}
if self.state.gupax.ask_before_quit {
// If we're already on the [ask_before_quit] screen and
// the user tried to exit again, exit.
if self.error_state.quit_twice {
if self.state.gupax.save_before_quit { self.save_before_quit(); }
return Some(ViewportCommand::Close);
}
// Else, set the error
self.error_state.set("", ErrorFerris::Oops, ErrorButtons::StayQuit);
self.error_state.quit_twice = true;
Some(ViewportCommand::CancelClose)
// Else, just quit.
} else {
if self.state.gupax.save_before_quit { self.save_before_quit(); }
Some(ViewportCommand::Close)
}
});
// This will either:
// 1. Cancel a close signal
// 2. Close the program
if let Some(cmd) = close_signal {
ctx.send_viewport_cmd(cmd);
}
// If [F11] was pressed, reverse [fullscreen] bool // If [F11] was pressed, reverse [fullscreen] bool
let mut input = ctx.input_mut(); let key: KeyPressed = ctx.input_mut(|input| {
let key: KeyPressed = {
if input.consume_key(Modifiers::NONE, Key::F11) { if input.consume_key(Modifiers::NONE, Key::F11) {
KeyPressed::F11 KeyPressed::F11
} else if input.consume_key(Modifiers::NONE, Key::Z) { } else if input.consume_key(Modifiers::NONE, Key::Z) {
@ -1233,16 +1248,17 @@ impl eframe::App for App {
} else { } else {
KeyPressed::None KeyPressed::None
} }
}; });
drop(input);
// Check if egui wants keyboard input. // Check if egui wants keyboard input.
// This prevents keyboard shortcuts from clobbering TextEdits. // This prevents keyboard shortcuts from clobbering TextEdits.
// (Typing S in text would always [Save] instead) // (Typing S in text would always [Save] instead)
let wants_input = ctx.wants_keyboard_input(); let wants_input = ctx.wants_keyboard_input();
if key.is_f11() { if key.is_f11() {
let info = frame.info(); if ctx.input(|i| i.viewport().maximized == Some(true)) {
frame.set_fullscreen(!info.window_info.fullscreen); ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
}
// Change Tabs LEFT // Change Tabs LEFT
} else if key.is_z() && !wants_input { } else if key.is_z() && !wants_input {
match self.tab { match self.tab {
@ -1524,7 +1540,7 @@ impl eframe::App for App {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let response = ui.add_sized([sudo_width*8.0, height], TextEdit::hint_text(TextEdit::singleline(&mut sudo.pass).password(hide), PASSWORD_TEXT)); let response = ui.add_sized([sudo_width*8.0, height], TextEdit::hint_text(TextEdit::singleline(&mut sudo.pass).password(hide), PASSWORD_TEXT));
let box_width = (ui.available_width()/2.0)-5.0; let box_width = (ui.available_width()/2.0)-5.0;
if (response.lost_focus() && ui.input().key_pressed(Key::Enter)) || if (response.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter))) ||
ui.add_sized([box_width, height], Button::new("Enter")).on_hover_text(PASSWORD_ENTER).clicked() { ui.add_sized([box_width, height], Button::new("Enter")).on_hover_text(PASSWORD_ENTER).clicked() {
response.request_focus(); response.request_focus();
if !sudo.testing { if !sudo.testing {

View file

@ -64,7 +64,7 @@ impl crate::disk::P2pool {
ui.separator(); ui.separator();
let response = ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(buffer), r#"Type a command (e.g "help" or "status") and press Enter"#)).on_hover_text(P2POOL_INPUT); let response = ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(buffer), r#"Type a command (e.g "help" or "status") and press Enter"#)).on_hover_text(P2POOL_INPUT);
// If the user pressed enter, dump buffer contents into the process STDIN // If the user pressed enter, dump buffer contents into the process STDIN
if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) { if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
response.request_focus(); // Get focus back response.request_focus(); // Get focus back
let buffer = std::mem::take(buffer); // Take buffer let buffer = std::mem::take(buffer); // Take buffer
let mut process = lock!(process); // Lock let mut process = lock!(process); // Lock
@ -151,7 +151,7 @@ impl crate::disk::P2pool {
debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes"); debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes");
let ip_location = crate::node::format_ip_location(&self.node, false); let ip_location = crate::node::format_ip_location(&self.node, false);
let text = RichText::new(format!("{}ms | {}", ms, ip_location)).color(color); let text = RichText::new(format!("{}ms | {}", ms, ip_location)).color(color);
ComboBox::from_id_source("remote_nodes").selected_text(text).show_ui(ui, |ui| { ComboBox::from_id_source("remote_nodes").selected_text(text).width(width).show_ui(ui, |ui| {
for data in lock!(ping).nodes.iter() { for data in lock!(ping).nodes.iter() {
let ms = crate::node::format_ms(data.ms); let ms = crate::node::format_ms(data.ms);
let ip_location = crate::node::format_ip_location(data.ip, true); let ip_location = crate::node::format_ip_location(data.ip, true);
@ -338,7 +338,7 @@ impl crate::disk::P2pool {
// [Ping List] // [Ping List]
debug!("P2Pool Tab | Rendering [Node List]"); debug!("P2Pool Tab | Rendering [Node List]");
let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name)); let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name));
ComboBox::from_id_source("manual_nodes").selected_text(text).show_ui(ui, |ui| { ComboBox::from_id_source("manual_nodes").selected_text(text).width(width).show_ui(ui, |ui| {
let mut n = 0; let mut n = 0;
for (name, node) in node_vec.iter() { for (name, node) in node_vec.iter() {
let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq)); let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq));

View file

@ -327,7 +327,12 @@ pub fn show(&mut self, sys: &Arc<Mutex<Sys>>, p2pool_api: &Arc<Mutex<PubP2poolAp
ui.add_sized([width, text], Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark")).on_hover_text(STATUS_SUBMENU_OTHER_CPUS); ui.add_sized([width, text], Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark")).on_hover_text(STATUS_SUBMENU_OTHER_CPUS);
}); });
egui::ScrollArea::both().always_show_scroll(true).max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| { egui::ScrollArea::both()
.scroll_bar_visibility(egui::containers::scroll_area::ScrollBarVisibility::AlwaysVisible)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
let width = width / 20.0; let width = width / 20.0;
let (cpu, bar, high, average, low, rank, bench) = ( let (cpu, bar, high, average, low, rank, bench) = (
width*10.0, width*10.0,

View file

@ -65,6 +65,10 @@ impl AtomicUnit {
self.0 self.0
} }
#[allow(clippy::inherent_to_string_shadow_display)]
// This is terrible but it formats it in a different way
// than `Display`, but for backwards compat, changing it
// requires touching other code, so...
pub fn to_string(self) -> String { pub fn to_string(self) -> String {
self.0.to_string() self.0.to_string()
} }
@ -400,6 +404,7 @@ r#"2022-09-08 18:42:55.4636 | 0.001000000000 XMR | Block 2,654,321
]); ]);
println!("OG: {:#?}", payout_ord); println!("OG: {:#?}", payout_ord);
#[allow(clippy::never_loop)]
for (_, atomic_unit, _) in payout_ord.rev_iter() { for (_, atomic_unit, _) in payout_ord.rev_iter() {
if atomic_unit.to_u64() == 3000000000 { if atomic_unit.to_u64() == 3000000000 {
break break

View file

@ -65,7 +65,7 @@ impl crate::disk::Xmrig {
ui.separator(); ui.separator();
let response = ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(buffer), r#"Commands: [h]ashrate, [p]ause, [r]esume, re[s]ults, [c]onnection"#)).on_hover_text(XMRIG_INPUT); let response = ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(buffer), r#"Commands: [h]ashrate, [p]ause, [r]esume, re[s]ults, [c]onnection"#)).on_hover_text(XMRIG_INPUT);
// If the user pressed enter, dump buffer contents into the process STDIN // If the user pressed enter, dump buffer contents into the process STDIN
if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) { if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
response.request_focus(); // Get focus back response.request_focus(); // Get focus back
let buffer = std::mem::take(buffer); // Take buffer let buffer = std::mem::take(buffer); // Take buffer
let mut process = lock!(process); // Lock let mut process = lock!(process); // Lock
@ -228,7 +228,7 @@ impl crate::disk::Xmrig {
// [Node List] // [Node List]
debug!("XMRig Tab | Rendering [Node List] ComboBox"); debug!("XMRig Tab | Rendering [Node List] ComboBox");
let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name)); let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name));
ComboBox::from_id_source("manual_pool").selected_text(text).show_ui(ui, |ui| { ComboBox::from_id_source("manual_pool").selected_text(text).width(width).show_ui(ui, |ui| {
let mut n = 0; let mut n = 0;
for (name, pool) in pool_vec.iter() { for (name, pool) in pool_vec.iter() {
let text = format!("{}. {}\n IP: {}\n Port: {}\n Rig: {}", n+1, name, pool.ip, pool.port, pool.rig); let text = format!("{}. {}\n IP: {}\n Port: {}\n Rig: {}", n+1, name, pool.ip, pool.port, pool.rig);