From 339b8f302d8f5768b2080ef3c91903c39eaf98fa Mon Sep 17 00:00:00 2001
From: hinto-janaiyo <hinto.janaiyo@protonmail.com>
Date: Sun, 13 Nov 2022 21:56:25 -0500
Subject: [PATCH] p2pool: add [Advanced], add [node.toml] database, add char
 limit

---
 src/README.md    |   2 +-
 src/constants.rs |  45 +++-
 src/disk.rs      | 537 +++++++++++++++++++++++++++++++++++++++++++++++
 src/gupax.rs     |  71 +++++--
 src/main.rs      |  75 +++++--
 src/p2pool.rs    | 378 +++++++++++++++++++++------------
 src/state.rs     | 350 ------------------------------
 src/update.rs    |   2 +-
 src/xmrig.rs     |   2 +-
 9 files changed, 925 insertions(+), 537 deletions(-)
 create mode 100644 src/disk.rs
 delete mode 100644 src/state.rs

diff --git a/src/README.md b/src/README.md
index b9bf19d..52f8f2b 100644
--- a/src/README.md
+++ b/src/README.md
@@ -8,12 +8,12 @@
 | File/Folder    | Purpose |
 |----------------|---------|
 | `constants.rs` | General constants needed in Gupax
+| `disk.rs`      | Code for writing to disk: `state.toml`, `nodes.toml`; This holds the structs for mutable [State]
 | `ferris.rs`    | Cute crab `--ferris`
 | `gupax.rs`     | `Gupax` tab
 | `main.rs`      | `App/Tab/State` + misc functions
 | `node.rs`      | Community node feature
 | `p2pool.rs`    | `P2Pool` tab
-| `state.rs`     | `gupax.toml` config code. This holds the structs representing tabs with mutable state (Gupax/P2Pool/XMRig)
 | `status.rs`    | `Status` tab
 | `update.rs`    | Update code for the `Gupax` tab
 | `xmrig.rs`     | `XMRig` tab
diff --git a/src/constants.rs b/src/constants.rs
index 6be837d..d43877d 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -66,13 +66,13 @@ pub const GUPAX_UPDATE_VIA_TOR: &'static str = "Update through the Tor network.
 pub const GUPAX_AUTO_NODE: &'static str = "Automatically ping the community Monero nodes and select the fastest at startup for P2Pool";
 pub const GUPAX_ASK_BEFORE_QUIT: &'static str = "Ask before quitting if processes are still alive or if an update is in progress";
 pub const GUPAX_SAVE_BEFORE_QUIT: &'static str = "Automatically save any changed settings before quitting";
-pub const GUPAX_PATH_P2POOL: &'static str = "The location of the P2Pool binary, both absolute and relative paths are accepted";
-pub const GUPAX_PATH_XMRIG: &'static str = "The location of the XMRig binary, both absolute and relative paths are accepted";
+pub const GUPAX_PATH_P2POOL: &'static str = "The location of the P2Pool binary: Both absolute and relative paths are accepted; A red [X] will appear if there is no file found at the given path";
+pub const GUPAX_PATH_XMRIG: &'static str = "The location of the XMRig binary: Both absolute and relative paths are accepted; A red [X] will appear if there is no file found at the given path";
 // P2Pool
 pub const P2POOL_MAIN: &'static str = "Use the P2Pool main-chain. This P2Pool finds shares faster, but has a higher difficulty. Suitable for miners with more than 50kH/s";
 pub const P2POOL_MINI: &'static str = "Use the P2Pool mini-chain. This P2Pool finds shares slower, but has a lower difficulty. Suitable for miners with less than 50kH/s";
-pub const P2POOL_OUT: &'static str = "How many out-bound peers (you connecting to others) to connect to?";
-pub const P2POOL_IN: &'static str = "How many in-bound peers (others connecting to you) to connect to?";
+pub const P2POOL_OUT: &'static str = "How many out-bound peers to connect to? (you connecting to others)";
+pub const P2POOL_IN: &'static str = "How many in-bound peers to allow? (others connecting to you)";
 pub const P2POOL_LOG: &'static str = "Verbosity of the console log";
 pub const P2POOL_COMMUNITY: &'static str = "Connect to a community trusted Monero node: This is convenient because you don't have to download the Monero blockchain but it comes at the cost of privacy";
 pub const P2POOL_MANUAL: &'static str = "Manually specify your own Monero node settings";
@@ -81,6 +81,25 @@ pub const P2POOL_AUTO_SELECT: &'static str = "Automatically select the fastest c
 pub const P2POOL_SELECT_FASTEST: &'static str = "Select the fastest community Monero node";
 pub const P2POOL_PING: &'static str = "Ping the built-in community Monero nodes";
 pub const P2POOL_ADDRESS: &'static str = "You must use a primary Monero address to mine on P2Pool (starts with a 4). It is highly recommended to create a new wallet for P2Pool mining; wallet addresses are public on P2Pool!";
+pub const P2POOL_COMMAND: &'static str = "Start P2Pool with these arguments and override all below settings; If the [--data-api] flag is not given, Gupax will append it to the arguments automatically so that the [Status] tab can work";
+pub const P2POOL_SIMPLE: &'static str =
+r#"Use simple settings:
+    - Remote community Monero node
+    - Default P2Pool settings + Mini"#;
+pub const P2POOL_ADVANCED: &'static str =
+r#"Use advanced settings:
+    - Overriding command arguments
+    - Manual node selection
+    - P2Pool Main/Mini selection
+    - Out/In peer setting
+    - Log level setting"#;
+pub const P2POOL_NAME: &'static str = "Add a unique name to identify this node; Only [A-Za-z0-9-_] and spaces allowed; Max length = 30 characters";
+pub const P2POOL_NODE_IP: &'static str = "Specify the Monero Node IP to connect to with P2Pool; Max length = 255 characters";
+pub const P2POOL_RPC_PORT: &'static str = "Specify the RPC port of the Monero node; [0-65535]";
+pub const P2POOL_ZMQ_PORT: &'static str = "Specify the ZMQ port of the Monero node; [0-65535]";
+pub const P2POOL_ADD: &'static str = "Add the current values to the list";
+pub const P2POOL_DELETE: &'static str = "Delete the currently selected node";
+pub const P2POOL_CLEAR: &'static str = "Clear all current values";
 
 // XMRig
 pub const XMRIG_P2POOL: &'static str = "Mine to your own P2Pool instance (localhost:3333)";
@@ -94,13 +113,19 @@ pub const XMRIG_PRIORITY: &'static str = "Set process priority (0 idle, 2 normal
 
 // CLI argument messages
 pub const ARG_HELP: &'static str =
-r#"USAGE: gupax [--flags]
+r#"USAGE: ./gupax [--flags]
 
-    -h | --help              Print this help message
-    -v | --version           Print versions
-    -n | --no-startup        Disable auto-update/node connections at startup
-    -r | --reset             Reset all Gupax configuration/state
-    -f | --ferris            Print an extremely cute crab"#;
+    -h | --help         Print this help message
+    -v | --version      Print version and build info
+    -l | --node-list    Print the manual node list
+    -s | --state        Print Gupax state
+    -n | --no-startup   Disable all auto-startup settings for this instance
+    -r | --reset        Reset all Gupax state and the manual node list
+    -f | --ferris       Print an extremely cute crab
+
+To view more detailed console debug information, start Gupax with
+the environment variable [RUST_LOG] set to a log level like so:
+    RUST_LOG=(trace|debug|info|warn|error) ./gupax"#;
 pub const ARG_COPYRIGHT: &'static str =
 r#"Gupax is licensed under GPLv3.
 For more information, see link below:
diff --git a/src/disk.rs b/src/disk.rs
new file mode 100644
index 0000000..5ff3c7a
--- /dev/null
+++ b/src/disk.rs
@@ -0,0 +1,537 @@
+// Gupax - GUI Uniting P2Pool And XMRig
+//
+// Copyright (c) 2022 hinto-janaiyo
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+// This handles reading/writing the disk files:
+//     - [state.toml] -> [App] state
+//     - [nodes.toml] -> [Manual Nodes] list
+// The TOML format is used. This struct hierarchy
+// directly translates into the TOML parser:
+//   State/
+//   ├─ Gupax/
+//   │  ├─ ...
+//   ├─ P2pool/
+//   │  ├─ ...
+//   ├─ Xmrig/
+//   │  ├─ ...
+//   ├─ Version/
+//      ├─ ...
+
+use std::{fs,env};
+use std::fmt::Display;
+use std::path::{Path,PathBuf};
+use std::result::Result;
+use std::sync::{Arc,Mutex};
+use std::collections::HashMap;
+use std::fmt::Write;
+use serde::{Serialize,Deserialize};
+use figment::Figment;
+use figment::providers::{Format,Toml};
+use crate::constants::*;
+use anyhow::Error;
+use log::*;
+
+//---------------------------------------------------------------------------------------------------- General functions for all [File]'s
+// get_file_path()      | Return absolute path to OS data path + filename
+// read_to_string()     | Convert the file at a given path into a [String]
+// create_new()         | Write a default TOML Struct into the appropriate file (in OS data path)
+// into_absolute_path() | Convert relative -> absolute path
+
+pub fn get_file_path(file: File) -> Result<PathBuf, TomlError> {
+	// Get OS data folder
+	// Linux   | $XDG_DATA_HOME or $HOME/.local/share | /home/alice/.local/state
+	// macOS   | $HOME/Library/Application Support    | /Users/Alice/Library/Application Support
+	// Windows | {FOLDERID_RoamingAppData}            | C:\Users\Alice\AppData\Roaming
+	let name = File::name(&file);
+
+	let mut path = match dirs::data_dir() {
+		Some(mut path) => {
+			path.push(DIRECTORY);
+			info!("OS data path ... OK");
+			path
+		},
+		None => { error!("Couldn't get OS PATH for data"); return Err(TomlError::Path(PATH_ERROR.to_string())) },
+	};
+	// Create directory
+	fs::create_dir_all(&path)?;
+	path.push(name);
+	info!("{:?} path ... {}", file, path.display());
+	Ok(path)
+}
+
+// Convert a [File] path to a [String]
+pub fn read_to_string(file: File, path: &PathBuf) -> Result<String, TomlError> {
+	match fs::read_to_string(&path) {
+		Ok(string) => {
+			info!("{:?} | Read ... OK", file);
+			Ok(string)
+		},
+		Err(err) => {
+			warn!("{:?} | Read ... FAIL", file);
+			Err(TomlError::Io(err))
+		},
+	}
+}
+
+// Write [String] to console with [info!] surrounded by "---"
+pub fn print_toml(toml: &String) {
+	info!("{}", HORIZONTAL);
+	for i in toml.lines() { info!("{}", i); }
+	info!("{}", HORIZONTAL);
+}
+
+// Turn relative paths into absolute paths
+pub fn into_absolute_path(path: String) -> Result<PathBuf, TomlError> {
+	let path = PathBuf::from(path);
+	if path.is_relative() {
+		let mut dir = std::env::current_exe()?;
+		dir.pop();
+		dir.push(path);
+		Ok(dir)
+	} else {
+		Ok(path)
+	}
+}
+
+//---------------------------------------------------------------------------------------------------- [State] Impl
+impl State {
+	pub fn new() -> Self {
+		let max_threads = num_cpus::get();
+		let current_threads;
+		if max_threads == 1 { current_threads = 1; } else { current_threads = max_threads / 2; }
+		Self {
+			gupax: Gupax {
+				auto_update: true,
+				auto_node: true,
+				ask_before_quit: true,
+				save_before_quit: true,
+				update_via_tor: true,
+				p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
+				xmrig_path: DEFAULT_XMRIG_PATH.to_string(),
+				absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(),
+				absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(),
+			},
+			p2pool: P2pool {
+				simple: true,
+				mini: true,
+				auto_node: true,
+				auto_select: true,
+				out_peers: 10,
+				in_peers: 10,
+				log_level: 3,
+				node: crate::NodeEnum::C3pool,
+				arguments: String::new(),
+				address: String::with_capacity(95),
+				name: "Local Monero Node".to_string(),
+				ip: "localhost".to_string(),
+				rpc: "18081".to_string(),
+				zmq: "18083".to_string(),
+				selected_name: "Local Monero Node".to_string(),
+				selected_ip: "localhost".to_string(),
+				selected_rpc: "18081".to_string(),
+				selected_zmq: "18083".to_string(),
+			},
+			xmrig: Xmrig {
+				simple: true,
+				tls: false,
+				nicehash: false,
+				keepalive: false,
+				hugepages_jit: true,
+				current_threads,
+				max_threads,
+				priority: 2,
+				pool: "localhost:3333".to_string(),
+				address: String::with_capacity(95),
+			},
+			version: Arc::new(Mutex::new(Version {
+				p2pool: Arc::new(Mutex::new(P2POOL_VERSION.to_string())),
+				xmrig: Arc::new(Mutex::new(XMRIG_VERSION.to_string())),
+			})),
+		}
+	}
+
+	// Convert [String] to [State]
+	pub fn from_string(string: String) -> Result<Self, TomlError> {
+		match toml::de::from_str(&string) {
+			Ok(state) => {
+				info!("State parse ... OK");
+				print_toml(&string);
+				Ok(state)
+			}
+			Err(err) => {
+				error!("State | String -> State ... FAIL ... {}", err);
+				Err(TomlError::Deserialize(err))
+			},
+		}
+	}
+
+	// Combination of multiple functions:
+	//   1. Attempt to read file from path into [String]
+	//      |_ Create a default file if not found
+	//   2. Deserialize [String] into a proper [Struct]
+	//      |_ Attempt to merge if deserialization fails
+	pub fn get() -> Result<Self, TomlError> {
+		// Read
+		let file = File::State;
+		let path = get_file_path(file)?;
+		let string = match read_to_string(file, &path) {
+			Ok(string) => string,
+			// Create
+			_ => {
+				let new = Self::create_new()?;
+				read_to_string(file, &path)?
+			},
+		};
+		// Deserialize
+		Self::from_string(string)
+	}
+
+	// Completely overwrite current [state.toml]
+	// with a new default version, and return [Self].
+	pub fn create_new() -> Result<Self, TomlError> {
+		info!("State | Creating new default...");
+		let new = Self::new();
+		let path = get_file_path(File::State)?;
+		println!("{:#?}", new);
+		let string = match toml::ser::to_string(&new) {
+				Ok(o) => { info!("State | Serialization ... OK"); o },
+				Err(e) => { error!("State | Couldn't serialize default file: {}", e); return Err(TomlError::Serialize(e)) },
+		};
+		fs::write(&path, &string)?;
+		info!("State | Write ... OK");
+		Ok(new)
+	}
+
+	// Save [State] onto disk file [gupax.toml]
+	pub fn save(&mut self) -> Result<(), TomlError> {
+		info!("Saving {:?} to disk...", self);
+		let path = get_file_path(File::State)?;
+		// Convert path to absolute
+		self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?;
+		self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?;
+		let string = match toml::ser::to_string(&self) {
+			Ok(string) => {
+				info!("TOML parse ... OK");
+				print_toml(&string);
+				string
+			},
+			Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) },
+		};
+		match fs::write(path, string) {
+			Ok(_) => { info!("TOML save ... OK"); Ok(()) },
+			Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) },
+		}
+	}
+
+	// Take [Self] as input, merge it with whatever the current [default] is,
+	// leaving behind old keys+values and updating [default] with old valid ones.
+	// Automatically overwrite current file.
+	pub fn merge(old: &Self) -> Result<Self, TomlError> {
+		info!("Starting TOML merge...");
+		let old = match toml::ser::to_string(&old) {
+			Ok(string) => { info!("Old TOML parse ... OK"); string },
+			Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) },
+		};
+		let default = match toml::ser::to_string(&Self::new()) {
+			Ok(string) => { info!("Default TOML parse ... OK"); string },
+			Err(err) => { error!("Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) },
+		};
+		let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() {
+			Ok(new) => { info!("TOML merge ... OK"); new },
+			Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) },
+		};
+		// Attempt save
+		Self::save(&mut new)?;
+		Ok(new)
+	}
+}
+
+//---------------------------------------------------------------------------------------------------- [Node] Impl
+impl Node {
+	pub fn new() -> Self {
+		Self {
+			ip: String::new(),
+			rpc: "18081".to_string(),
+			zmq: "18083".to_string(),
+		}
+	}
+
+	pub fn localhost() -> Self {
+		Self {
+			ip: "localhost".to_string(),
+			rpc: "18081".to_string(),
+			zmq: "18083".to_string(),
+		}
+	}
+
+	pub fn new_vec() -> Vec<(String, Self)> {
+		let mut vec = Vec::new();
+		vec.push(("Local Monero Node".to_string(), Self::localhost()));
+		vec
+	}
+
+	// Convert [String] to [Node] Vec
+	pub fn from_string(string: String) -> Result<Vec<(String, Self)>, TomlError> {
+		let nodes: HashMap<String, Node> = match toml::de::from_str(&string) {
+			Ok(map) => {
+				info!("Node | Parse ... OK");
+				print_toml(&string);
+				map
+			}
+			Err(err) => {
+				error!("Node | String parse ... FAIL ... {}", err);
+				return Err(TomlError::Deserialize(err))
+			},
+		};
+		let size = nodes.keys().len();
+		let mut vec = Vec::with_capacity(size);
+		for (key, values) in nodes.iter() {
+			vec.push((key.clone(), values.clone()));
+		}
+		Ok(vec)
+	}
+
+	// Convert [Vec<(String, Self)>] into [String]
+	// that can be written as a proper TOML file
+	pub fn into_string(vec: Vec<(String, Self)>) -> String {
+		let mut toml = String::new();
+		for (key, value) in vec.iter() {
+			write!(
+				toml,
+				"[\'{}\']\nip = {:#?}\nrpc = {:#?}\nzmq = {:#?}\n",
+				key,
+				value.ip,
+				value.rpc,
+				value.zmq,
+			);
+		}
+		toml
+	}
+
+	// Combination of multiple functions:
+	//   1. Attempt to read file from path into [String]
+	//      |_ Create a default file if not found
+	//   2. Deserialize [String] into a proper [Struct]
+	//      |_ Attempt to merge if deserialization fails
+	pub fn get() -> Result<Vec<(String, Self)>, TomlError> {
+		// Read
+		let file = File::Node;
+		let path = get_file_path(file)?;
+		let string = match read_to_string(file, &path) {
+			Ok(string) => string,
+			// Create
+			_ => {
+				let new = Self::create_new()?;
+				read_to_string(file, &path)?
+			},
+		};
+		// Deserialize
+		Self::from_string(string)
+	}
+
+	// Completely overwrite current [node.toml]
+	// with a new default version, and return [Vec<String, Self>].
+	pub fn create_new() -> Result<Vec<(String, Self)>, TomlError> {
+		info!("Node | Creating new default...");
+		let new = Self::new_vec();
+		let path = get_file_path(File::Node)?;
+		let string = Self::into_string(Self::new_vec());
+		fs::write(&path, &string)?;
+		info!("Node | Write ... OK");
+		Ok(new)
+	}
+
+//	// Save [State] onto disk file [gupax.toml]
+//	pub fn save(&mut self) -> Result<(), TomlError> {
+//		info!("Saving {:?} to disk...", self);
+//		let path = get_file_path(File::State)?;
+//		// Convert path to absolute
+//		self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?;
+//		self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?;
+//		let string = match toml::ser::to_string(&self) {
+//			Ok(string) => {
+//				info!("TOML parse ... OK");
+//				print_toml(&string);
+//				string
+//			},
+//			Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) },
+//		};
+//		match fs::write(path, string) {
+//			Ok(_) => { info!("TOML save ... OK"); Ok(()) },
+//			Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) },
+//		}
+//	}
+//
+//	// Take [Self] as input, merge it with whatever the current [default] is,
+//	// leaving behind old keys+values and updating [default] with old valid ones.
+//	// Automatically overwrite current file.
+//	pub fn merge(old: &Self) -> Result<Self, TomlError> {
+//		info!("Starting TOML merge...");
+//		let old = match toml::ser::to_string(&old) {
+//			Ok(string) => { info!("Old TOML parse ... OK"); string },
+//			Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) },
+//		};
+//		let default = match toml::ser::to_string(&Self::new()) {
+//			Ok(string) => { info!("Default TOML parse ... OK"); string },
+//			Err(err) => { error!("Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) },
+//		};
+//		let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() {
+//			Ok(new) => { info!("TOML merge ... OK"); new },
+//			Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) },
+//		};
+//		// Attempt save
+//		Self::save(&mut new)?;
+//		Ok(new)
+//	}
+}
+
+//---------------------------------------------------------------------------------------------------- Custom Error [TomlError]
+impl Display for TomlError {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		use TomlError::*;
+		match self {
+			Io(err)          => write!(f, "{}: {} | {}", ERROR, self, err),
+			Path(err)        => write!(f, "{}: {} | {}", ERROR, self, err),
+			Serialize(err)   => write!(f, "{}: {} | {}", ERROR, self, err),
+			Deserialize(err) => write!(f, "{}: {} | {}", ERROR, self, err),
+			Merge(err)       => write!(f, "{}: {} | {}", ERROR, self, err),
+		}
+	}
+}
+
+impl From<std::io::Error> for TomlError {
+	fn from(err: std::io::Error) -> Self {
+		TomlError::Io(err)
+	}
+}
+
+//---------------------------------------------------------------------------------------------------- Const
+// State file
+const ERROR: &'static str = "Disk error";
+const PATH_ERROR: &'static str = "PATH for state directory could not be not found";
+#[cfg(target_os = "windows")]
+const DIRECTORY: &'static str = r#"Gupax\"#;
+#[cfg(target_os = "macos")]
+const DIRECTORY: &'static str = "com.github.hinto-janaiyo.gupax/";
+#[cfg(target_os = "linux")]
+const DIRECTORY: &'static str = "gupax/";
+
+#[cfg(target_os = "windows")]
+pub const DEFAULT_P2POOL_PATH: &'static str = r"P2Pool\p2pool.exe";
+#[cfg(target_family = "unix")]
+pub const DEFAULT_P2POOL_PATH: &'static str = "p2pool/p2pool";
+#[cfg(target_os = "windows")]
+pub const DEFAULT_XMRIG_PATH: &'static str = r"XMRig\xmrig.exe";
+#[cfg(target_family = "unix")]
+pub const DEFAULT_XMRIG_PATH: &'static str = "xmrig/xmrig";
+
+//---------------------------------------------------------------------------------------------------- Error Enum
+#[derive(Debug)]
+pub enum TomlError {
+	Io(std::io::Error),
+	Path(String),
+	Serialize(toml::ser::Error),
+	Deserialize(toml::de::Error),
+	Merge(figment::Error),
+}
+
+//---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file)
+#[derive(Clone,Copy,Eq,PartialEq,Debug,Deserialize,Serialize)]
+pub enum File {
+	State,
+	Node,
+}
+
+impl File {
+	fn name(&self) -> &'static str {
+		match *self {
+			Self::State => "state.toml",
+			Self::Node => "node.toml",
+		}
+	}
+}
+
+//---------------------------------------------------------------------------------------------------- [Node] Struct
+#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
+pub struct Node {
+	pub ip: String,
+	pub rpc: String,
+	pub zmq: String,
+}
+
+//---------------------------------------------------------------------------------------------------- [State] Struct
+#[derive(Clone,Debug,Deserialize,Serialize)]
+pub struct State {
+	pub gupax: Gupax,
+	pub p2pool: P2pool,
+	pub xmrig: Xmrig,
+	pub version: Arc<Mutex<Version>>,
+}
+
+#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
+pub struct Gupax {
+	pub auto_update: bool,
+	pub auto_node: bool,
+	pub ask_before_quit: bool,
+	pub save_before_quit: bool,
+	pub update_via_tor: bool,
+	pub p2pool_path: String,
+	pub xmrig_path: String,
+	pub absolute_p2pool_path: PathBuf,
+	pub absolute_xmrig_path: PathBuf,
+}
+
+#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
+pub struct P2pool {
+	pub simple: bool,
+	pub mini: bool,
+	pub auto_node: bool,
+	pub auto_select: bool,
+	pub out_peers: u16,
+	pub in_peers: u16,
+	pub log_level: u8,
+	pub node: crate::node::NodeEnum,
+	pub arguments: String,
+	pub address: String,
+	pub name: String,
+	pub ip: String,
+	pub rpc: String,
+	pub zmq: String,
+	pub selected_name: String,
+	pub selected_ip: String,
+	pub selected_rpc: String,
+	pub selected_zmq: String,
+}
+
+#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
+pub struct Xmrig {
+	pub simple: bool,
+	pub tls: bool,
+	pub nicehash: bool,
+	pub keepalive: bool,
+	pub hugepages_jit: bool,
+	pub max_threads: usize,
+	pub current_threads: usize,
+	pub priority: u8,
+	pub pool: String,
+	pub address: String,
+}
+
+#[derive(Clone,Debug,Deserialize,Serialize)]
+pub struct Version {
+	pub p2pool: Arc<Mutex<String>>,
+	pub xmrig: Arc<Mutex<String>>,
+}
diff --git a/src/gupax.rs b/src/gupax.rs
index 35043de..54d298f 100644
--- a/src/gupax.rs
+++ b/src/gupax.rs
@@ -17,24 +17,29 @@
 
 use std::path::Path;
 use crate::{App,State};
-use egui::TextStyle::Monospace;
-use egui::RichText;
+use egui::{
+	TextStyle::Monospace,
+	Checkbox,
+	RichText,
+	Label,
+	Color32,
+};
 use crate::constants::*;
-use crate::state::{Gupax,Version};
+use crate::disk::{Gupax,Version};
 use crate::update::*;
 use std::thread;
 use std::sync::{Arc,Mutex};
 use log::*;
 
 impl Gupax {
-	pub fn show(state: &mut Gupax, og: &Arc<Mutex<State>>, state_ver: &Arc<Mutex<Version>>, update: &Arc<Mutex<Update>>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
+	pub fn show(&mut self, og: &Arc<Mutex<State>>, state_ver: &Arc<Mutex<Version>>, update: &Arc<Mutex<Update>>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
 		// Update button + Progress bar
 		ui.group(|ui| {
 				// These are in unnecessary [ui.vertical()]'s
 				// because I need to use [ui.set_enabled]s, but I can't
 				// find a way to use a [ui.xxx()] with [ui.add_sized()].
 				// I have to pick one. This one seperates them though.
-				let height = height/6.0;
+				let height = height/8.0;
 				let width = width - SPACE;
 				let updating = *update.lock().unwrap().updating.lock().unwrap();
 				ui.vertical(|ui| {
@@ -87,36 +92,62 @@ impl Gupax {
 
 		ui.horizontal(|ui| {
 			ui.group(|ui| {
-					let width = (width - SPACE*9.8)/5.0;
-					let height = height/2.5;
+					let width = (width - SPACE*7.5)/4.0;
+					let height = height/8.0;
 					let mut style = (*ctx.style()).clone();
-					style.spacing.icon_width_inner = width / 6.0;
-					style.spacing.icon_width = width / 4.0;
+					style.spacing.icon_width_inner = width / 8.0;
+					style.spacing.icon_width = width / 6.0;
 					style.spacing.icon_spacing = 20.0;
 					ctx.set_style(style);
-					let height = height/2.5;
-					ui.add_sized([width, height], egui::Checkbox::new(&mut state.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE);
+					ui.add_sized([width, height], egui::Checkbox::new(&mut self.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE);
 					ui.separator();
-					ui.add_sized([width, height], egui::Checkbox::new(&mut state.auto_node, "Auto-node")).on_hover_text(GUPAX_AUTO_NODE);
+					ui.add_sized([width, height], egui::Checkbox::new(&mut self.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR);
 					ui.separator();
-					ui.add_sized([width, height], egui::Checkbox::new(&mut state.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR);
+					ui.add_sized([width, height], egui::Checkbox::new(&mut self.ask_before_quit, "Ask before quit")).on_hover_text(GUPAX_ASK_BEFORE_QUIT);
 					ui.separator();
-					ui.add_sized([width, height], egui::Checkbox::new(&mut state.ask_before_quit, "Ask before quit")).on_hover_text(GUPAX_ASK_BEFORE_QUIT);
-					ui.separator();
-					ui.add_sized([width, height], egui::Checkbox::new(&mut state.save_before_quit, "Save before quit")).on_hover_text(GUPAX_SAVE_BEFORE_QUIT);
+					ui.add_sized([width, height], egui::Checkbox::new(&mut self.save_before_quit, "Save before quit")).on_hover_text(GUPAX_SAVE_BEFORE_QUIT);
 			});
 		});
 		ui.add_space(SPACE);
 
+		ui.style_mut().override_text_style = Some(Monospace);
+		let height = height/20.0;
+		let text_edit = (ui.available_width()/10.0)-SPACE;
 		ui.horizontal(|ui| {
-			ui.label("P2Pool binary path:");
+			if self.p2pool_path.is_empty() {
+				ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ➖").color(Color32::LIGHT_GRAY)));
+			} else {
+				match crate::disk::into_absolute_path(self.p2pool_path.clone()) {
+					Ok(path) => {
+						if path.is_file() {
+							ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ✔").color(Color32::from_rgb(100, 230, 100))))
+						} else {
+							ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(Color32::from_rgb(230, 50, 50))))
+						}
+					},
+					_ => ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))),
+				};
+			}
 			ui.spacing_mut().text_edit_width = ui.available_width() - SPACE;
-			ui.text_edit_singleline(&mut state.p2pool_path).on_hover_text(GUPAX_PATH_P2POOL);
+			ui.text_edit_singleline(&mut self.p2pool_path).on_hover_text(GUPAX_PATH_P2POOL);
 		});
 		ui.horizontal(|ui| {
-			ui.label("XMRig binary path: ");
+			if self.xmrig_path.is_empty() {
+				ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ➖").color(Color32::LIGHT_GRAY)));
+			} else {
+				match crate::disk::into_absolute_path(self.xmrig_path.clone()) {
+					Ok(path) => {
+						if path.is_file() {
+							ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ✔").color(Color32::from_rgb(100, 230, 100))))
+						} else {
+							ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(Color32::from_rgb(230, 50, 50))))
+						}
+					},
+					_ => ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))),
+				};
+			}
 			ui.spacing_mut().text_edit_width = ui.available_width() - SPACE;
-			ui.text_edit_singleline(&mut state.xmrig_path).on_hover_text(GUPAX_PATH_XMRIG);
+			ui.text_edit_singleline(&mut self.xmrig_path).on_hover_text(GUPAX_PATH_XMRIG);
 		});
 	}
 }
diff --git a/src/main.rs b/src/main.rs
index a86fa6f..7fcfdfd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -49,14 +49,14 @@ use std::path::PathBuf;
 mod ferris;
 mod constants;
 mod node;
-mod state;
+mod disk;
 mod about;
 mod status;
 mod gupax;
 mod p2pool;
 mod xmrig;
 mod update;
-use {ferris::*,constants::*,node::*,state::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*,update::*};
+use {ferris::*,constants::*,node::*,disk::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*,update::*};
 
 //---------------------------------------------------------------------------------------------------- Struct + Impl
 // The state of the outer main [App].
@@ -74,6 +74,7 @@ pub struct App {
 	og: Arc<Mutex<State>>, // og = Old state to compare against
 	state: State, // state = Working state (current settings)
 	update: Arc<Mutex<Update>>, // State for update data [update.rs]
+	node_vec: Vec<(String, Node)>, // Manual Node database
 	diff: bool, // This bool indicates state changes
 	// Process/update state:
 	// Doesn't make sense to save this on disk
@@ -92,7 +93,7 @@ pub struct App {
 	version: &'static str, // Gupax version
 	name_version: String, // [Gupax vX.X.X]
 	banner: RetainedImage, // Gupax banner image
-	addr_regex: Regex, // [4.*] Monero Address Regex
+	regex: Regexes, // Custom Struct holding pre-made [Regex]'s
 }
 
 impl App {
@@ -113,9 +114,10 @@ impl App {
 			ping: Arc::new(Mutex::new(Ping::new())),
 			width: 1280.0,
 			height: 720.0,
-			og: Arc::new(Mutex::new(State::default())),
-			state: State::default(),
+			og: Arc::new(Mutex::new(State::new())),
+			state: State::new(),
 			update: Arc::new(Mutex::new(Update::new(String::new(), PathBuf::new(), PathBuf::new(), true))),
+			node_vec: Node::new_vec(),
 			diff: false,
 			p2pool: false,
 			xmrig: false,
@@ -129,7 +131,7 @@ impl App {
 			version: GUPAX_VERSION,
 			name_version: format!("Gupax {}", GUPAX_VERSION),
 			banner: RetainedImage::from_image_bytes("banner.png", BYTES_BANNER).unwrap(),
-			addr_regex: Regex::new("^4[A-Za-z1-9]+$").unwrap(),
+			regex: Regexes::new(),
 		};
 		// Apply arg state
 		let mut app = parse_args(app);
@@ -151,6 +153,8 @@ impl App {
 			};
 		}
 		app.state = app.og.lock().unwrap().clone();
+		// Get node list
+		app.node_vec = Node::get().unwrap();
 		// Handle max threads
 		app.og.lock().unwrap().xmrig.max_threads = num_cpus::get();
 		let current = app.og.lock().unwrap().xmrig.current_threads;
@@ -167,7 +171,7 @@ impl App {
 	}
 }
 
-//---------------------------------------------------------------------------------------------------- Enum + Impl
+//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl
 // The tabs inside [App].
 #[derive(Clone, Copy, Debug, PartialEq)]
 enum Tab {
@@ -184,6 +188,28 @@ impl Default for Tab {
     }
 }
 
+//---------------------------------------------------------------------------------------------------- [Regexes] struct
+#[derive(Clone, Debug)]
+struct Regexes {
+	name: Regex,
+	address: Regex,
+	ipv4: Regex,
+	domain: Regex,
+	port: Regex,
+}
+
+impl Regexes {
+	fn new() -> Self {
+		Regexes {
+			name: Regex::new("^[A-Za-z0-9-_]+( [A-Za-z0-9-_]+)*$").unwrap(),
+			address: Regex::new("^4[A-Za-z1-9]+$").unwrap(), // This still needs to check for (l, I, o, 0)
+			ipv4: Regex::new(r#"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"#).unwrap(),
+			domain: Regex::new(r#"^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$"#).unwrap(),
+			port: Regex::new(r#"^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"#).unwrap(),
+		}
+	}
+}
+
 //---------------------------------------------------------------------------------------------------- Init functions
 fn init_text_styles(ctx: &egui::Context, width: f32) {
 	let scale = width / 26.666;
@@ -196,6 +222,7 @@ fn init_text_styles(ctx: &egui::Context, width: f32) {
 		(Heading, FontId::new(scale/1.5, Proportional)),
 		(Name("Tab".into()), FontId::new(scale*1.2, Proportional)),
 		(Name("Bottom".into()), FontId::new(scale/2.0, Proportional)),
+		(Name("MonospaceSmall".into()), FontId::new(scale/2.5, egui::FontFamily::Monospace)),
 	].into();
 //	style.visuals.selection.stroke = Stroke { width: 5.0, color: Color32::from_rgb(255, 255, 255) };
 //	style.spacing.slider_width = scale;
@@ -301,7 +328,9 @@ fn init_auto(app: &App) {
 	}
 
 	// [Auto-Ping]
-	if app.og.lock().unwrap().p2pool.auto_node {
+	let auto_node = app.og.lock().unwrap().p2pool.auto_node;
+	let simple = app.og.lock().unwrap().p2pool.simple;
+	if auto_node && simple {
 		let ping = Arc::clone(&app.ping);
 		let og = Arc::clone(&app.og);
 		thread::spawn(move|| {
@@ -323,7 +352,7 @@ fn parse_args(mut app: App) -> App {
 		match arg.as_str() {
 			"-h"|"--help"    => { println!("{}", ARG_HELP); exit(0); },
 			"-v"|"--version" => {
-				println!("Gupax {} (OS: {}, Commit: {})\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT);
+				println!("Gupax {} [OS: {}, Commit: {}]\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT);
 				exit(0);
 			},
 			"-f"|"--ferris" => { println!("{}", FERRIS); exit(0); },
@@ -333,9 +362,11 @@ fn parse_args(mut app: App) -> App {
 	// Everything else
 	for arg in args {
 		match arg.as_str() {
+			"-l"|"--node-list"  => { info!("Printing node list..."); print_disk_file(File::Node); }
+			"-s"|"--state"      => { info!("Printing state..."); print_disk_file(File::State); }
 			"-n"|"--no-startup" => { info!("Disabling startup..."); app.startup = false; }
-			"-r"|"--reset" => { info!("Resetting state..."); app.reset = true; }
-			_ => { eprintln!("[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", arg); exit(1); },
+			"-r"|"--reset"      => { info!("Resetting state..."); app.reset = true; }
+			_                   => { eprintln!("[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", arg); exit(1); },
 		}
 	}
 	app
@@ -373,6 +404,18 @@ pub fn clean_dir() -> Result<(), anyhow::Error> {
 	Ok(())
 }
 
+// Print disk files to console
+fn print_disk_file(file: File) {
+	let path = match get_file_path(file) {
+		Ok(path) => path,
+		Err(e) => { error!("{}", e); exit(1); },
+	};
+	match std::fs::read_to_string(&path) {
+		Ok(string) => { println!("{}", string); exit(0); },
+		Err(e) => { error!("{}", e); exit(1); },
+	}
+}
+
 //---------------------------------------------------------------------------------------------------- [App] frame for [Panic] situations
 fn panic_main(error: String) {
 	error!("{}", error);
@@ -560,7 +603,7 @@ impl eframe::App for App {
 		// Top: Tabs
 		egui::TopBottomPanel::top("top").show(ctx, |ui| {
 			let width = (self.width - (SPACE*10.0))/5.0;
-			let height = self.height/10.0;
+			let height = self.height/12.0;
 			ui.group(|ui| {
 			    ui.add_space(4.0);
 				ui.horizontal(|ui| {
@@ -584,7 +627,7 @@ impl eframe::App for App {
 
 		// Bottom: app info + state/process buttons
 		egui::TopBottomPanel::bottom("bottom").show(ctx, |ui| {
-			let height = self.height/18.0;
+			let height = self.height/20.0;
 			ui.style_mut().override_text_style = Some(Name("Bottom".into()));
 			ui.horizontal(|ui| {
 				ui.group(|ui| {
@@ -632,11 +675,11 @@ impl eframe::App for App {
 					Tab::P2pool => {
 						ui.group(|ui| {
 							let width = width / 1.5;
-							if ui.add_sized([width, height], egui::SelectableLabel::new(!self.state.p2pool.simple, "Advanced")).clicked() {
+							if ui.add_sized([width, height], egui::SelectableLabel::new(!self.state.p2pool.simple, "Advanced")).on_hover_text(P2POOL_ADVANCED).clicked() {
 								self.state.p2pool.simple = false;
 							}
 							ui.separator();
-							if ui.add_sized([width, height], egui::SelectableLabel::new(self.state.p2pool.simple, "Simple")).clicked() {
+							if ui.add_sized([width, height], egui::SelectableLabel::new(self.state.p2pool.simple, "Simple")).on_hover_text(P2POOL_SIMPLE).clicked() {
 								self.state.p2pool.simple = true;
 							}
 						});
@@ -725,7 +768,7 @@ impl eframe::App for App {
 					Gupax::show(&mut self.state.gupax, &self.og, &self.state.version, &self.update, self.width, self.height, ctx, ui);
 				}
 				Tab::P2pool => {
-					P2pool::show(&mut self.state.p2pool, &self.og, self.p2pool, &self.ping, &self.addr_regex, self.width, self.height, ctx, ui);
+					P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, self.p2pool, &self.ping, &self.regex, self.width, self.height, ctx, ui);
 				}
 				Tab::Xmrig => {
 					Xmrig::show(&mut self.state.xmrig, self.width, self.height, ctx, ui);
diff --git a/src/p2pool.rs b/src/p2pool.rs
index eb3c966..0fededb 100644
--- a/src/p2pool.rs
+++ b/src/p2pool.rs
@@ -15,33 +15,73 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-use crate::App;
-use crate::constants::*;
-use crate::state::*;
-use crate::node::*;
-use crate::node::NodeEnum::*;
+use crate::{
+	App,
+	Regexes,
+	constants::*,
+	disk::*,
+	node::*
+};
+use egui::{
+	TextEdit,SelectableLabel,ComboBox,Label,FontId,Button,Color32,RichText,Slider,Checkbox,
+	TextStyle::*,
+	FontFamily::Proportional,
+	TextBuffer,
+};
 use std::sync::{Arc,Mutex};
 use std::thread;
-use log::*;
-use egui::{TextEdit,SelectableLabel,ComboBox,Label};
-use egui::TextStyle::*;
-use egui::FontFamily::Proportional;
-use egui::{FontId,Button,Color32,RichText};
 use regex::Regex;
+use log::*;
 
 impl P2pool {
-	pub fn show(&mut self, og: &Arc<Mutex<State>>, online: bool, ping: &Arc<Mutex<Ping>>, addr_regex: &Regex, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
-	let text_edit = height / 20.0;
-	// Console
+	pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc<Mutex<State>>, online: bool, ping: &Arc<Mutex<Ping>>, regex: &Regexes, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
+	let text_edit = height / 22.0;
+	//---------------------------------------------------------------------------------------------------- Console
 	ui.group(|ui| {
 		let height = height / SPACE;
 		let width = width - SPACE;
+		ui.style_mut().override_text_style = Some(Monospace);
 		ui.add_sized([width, height*3.0], TextEdit::multiline(&mut "".to_string()));
 		ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut "".to_string()), r#"Type a command (e.g "help" or "status") and press Enter"#));
 	});
 
+	//---------------------------------------------------------------------------------------------------- Args
+	if ! self.simple {
+	ui.group(|ui| { ui.horizontal(|ui| {
+		let width = (width/10.0) - SPACE;
+		ui.style_mut().override_text_style = Some(Monospace);
+		ui.add_sized([width, text_edit], Label::new("Command arguments:"));
+		ui.add_sized([ui.available_width(), text_edit], TextEdit::hint_text(TextEdit::singleline(&mut self.arguments), r#"--wallet <...> --host <...>"#)).on_hover_text(P2POOL_COMMAND);
+		self.arguments.truncate(1024);
+	})});
+	ui.set_enabled(self.arguments.is_empty());
+	}
+
+	//---------------------------------------------------------------------------------------------------- Address
+	ui.group(|ui| {
+		let width = width - SPACE;
+		ui.spacing_mut().text_edit_width = (width)-(SPACE*3.0);
+		ui.style_mut().override_text_style = Some(Monospace);
+		let text;
+		let color;
+		let len = format!("{:02}", self.address.len());
+		if self.address.is_empty() {
+			text = format!("Monero Address [{}/95] ➖", len);
+			color = Color32::LIGHT_GRAY;
+		} else if self.address.len() == 95 && Regex::is_match(&regex.address, &self.address) && ! self.address.contains("0") && ! self.address.contains("O") && ! self.address.contains("l") {
+			text = format!("Monero Address [{}/95] ✔", len);
+			color = Color32::from_rgb(100, 230, 100);
+		} else {
+			text = format!("Monero Address [{}/95] ❌", len);
+			color = Color32::from_rgb(230, 50, 50);
+		}
+		ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
+		ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4...")).on_hover_text(P2POOL_ADDRESS);
+		self.address.truncate(95);
+	});
+
+	//---------------------------------------------------------------------------------------------------- Simple
 	let height = ui.available_height();
-	// [Simple]
 	if self.simple {
 		// [Node]
 		let height = height / 6.0;
@@ -50,7 +90,7 @@ impl P2pool {
 		ui.vertical(|ui| {
 		ui.horizontal(|ui| {
 			// [Ping List]
-			let id = og.lock().unwrap().p2pool.node;
+			let id = self.node;
 			let ip = enum_to_ip(id);
 			let mut ms = 0;
 			let mut color = Color32::LIGHT_GRAY;
@@ -61,13 +101,13 @@ impl P2pool {
 					break
 				}
 			}
-			let text = RichText::new(format!("⏺ {}ms | {} | {}", ms, id, ip)).color(color);
+			let text = RichText::new(format!(" ⏺ {}ms | {} | {}", ms, id, ip)).color(color);
 			ComboBox::from_id_source("nodes").selected_text(RichText::text_style(text, Monospace)).show_ui(ui, |ui| {
 				for data in ping.lock().unwrap().nodes.iter() {
 					let ms = crate::node::format_ms(data.ms);
 					let id = crate::node::format_enum(data.id);
-					let text = RichText::text_style(RichText::new(format!("⏺ {} | {} | {}", ms, id, data.ip)).color(data.color), Monospace);
-					ui.selectable_value(&mut og.lock().unwrap().p2pool.node, data.id, text);
+					let text = RichText::text_style(RichText::new(format!(" ⏺ {} | {} | {}", ms, id, data.ip)).color(data.color), Monospace);
+					ui.selectable_value(&mut self.node, data.id, text);
 				}
 			});
 		});
@@ -130,131 +170,193 @@ impl P2pool {
 			ui.add_sized([width, height], egui::Checkbox::new(&mut self.auto_node, "Auto-node")).on_hover_text(P2POOL_AUTO_NODE);
 		})});
 
-		// [Address]
-		let height = ui.available_height();
-		ui.horizontal(|ui| {
-			if self.address.is_empty() {
-				ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ➖").color(Color32::LIGHT_GRAY)));
-			} else if self.address.len() == 95 && Regex::is_match(addr_regex, &self.address) {
-				ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ✔").color(Color32::from_rgb(100, 230, 100))));
-			} else {
-				ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ❌").color(Color32::from_rgb(230, 50, 50))));
-			}
-		});
-		ui.spacing_mut().text_edit_width = (width)-(SPACE*3.0);
-		ui.style_mut().override_text_style = Some(Monospace);
-		ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4...")).on_hover_text(P2POOL_ADDRESS);
-
-	// [Advanced]
+	//---------------------------------------------------------------------------------------------------- Advanced
 	} else {
-		// TODO:
-		// ping code
-		// If ping was pressed, start thread
-//		if self.ping {
-//			self.ping = false;
-//			self.pinging = Arc::new(Mutex::new(true));
-//			let node_clone = Arc::clone(&self.node);
-//			let pinging_clone = Arc::clone(&self.pinging);
-//			thread::spawn(move|| {
-//				let result = NodeStruct::ping();
-//				*node_clone.lock().unwrap() = result.nodes;
-//				*pinging_clone.lock().unwrap() = false;
-//			});
-//		}
-
-		// If ping-ING, display stats
-//		if *self.pinging.lock().unwrap() {
-//			egui::CentralPanel::default().show(ctx, |ui| {
-//				let width = ui.available_width();
-//				let width = width - 10.0;
-//				let height = ui.available_height();
-//				init_text_styles(ctx, width);
-//				ui.add_sized([width, height/2.0], Label::new(format!("In progress: {}", *self.pinging.lock().unwrap())));
-//				ui.group(|ui| {
-//					if ui.add_sized([width, height/10.0], egui::Button::new("Yes")).clicked() {
-//						info!("Quit confirmation = yes ... goodbye!");
-//						exit(0);
-//					} else if ui.add_sized([width, height/10.0], egui::Button::new("No")).clicked() {
-//						info!("Quit confirmation = no ... returning!");
-//						self.show_confirmation_dialog = false;
-//					}
-//				});
-//			});
-//			return
-//		}
-
-		let width = width - 30.0;
-		let mut style = (*ctx.style()).clone();
-		let height = ui.available_height()/1.2;
+		let mut incorrect_input = false; // This will disable [Add/Delete] on bad input
+		// [Monero node IP/RPC/ZMQ]
 		ui.horizontal(|ui| {
-			ui.group(|ui| { ui.vertical(|ui| {
-				ui.group(|ui| { ui.horizontal(|ui| {
-					if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.mini == false, "P2Pool Main")).on_hover_text(P2POOL_MAIN).clicked() { self.mini = false; };
-					if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.mini == true, "P2Pool Mini")).on_hover_text(P2POOL_MINI).clicked() { self.mini = true; };
-				})});
-
-				let width = width/4.0;
-				style.spacing.slider_width = width*1.25;
-				ctx.set_style(style);
-				ui.horizontal(|ui| {
-					ui.add_sized([width/8.0, height/5.0], egui::Label::new("Out peers [10-450]:"));
-					ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.out_peers, 10..=450)).on_hover_text(P2POOL_OUT);
-				});
-
-				ui.horizontal(|ui| {
-					ui.add_sized([width/8.0, height/5.0], egui::Label::new("    In peers [10-450]:"));
-					ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.in_peers, 10..=450)).on_hover_text(P2POOL_IN);
-				});
-
-				ui.horizontal(|ui| {
-					ui.add_sized([width/8.0, height/5.0], egui::Label::new("          Log level [0-6]:"));
-					ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.log_level, 0..=6)).on_hover_text(P2POOL_LOG);
-				});
-			})});
-
-		ui.group(|ui| { ui.vertical(|ui| {
-			ui.group(|ui| { ui.horizontal(|ui| {
-				if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.simple == false, "Community Monero Node")).on_hover_text(P2POOL_COMMUNITY).clicked() { self.simple = false; };
-				if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.simple == true, "Manual Monero Node")).on_hover_text(P2POOL_MANUAL).clicked() { self.simple = true; };
-			})});
-			ui.add_space(8.0);
-			ui.horizontal(|ui| {
-//			ui.add_sized([width/8.0, height/5.0],
-			egui::ComboBox::from_label(self.node.to_string()).selected_text(RINO).show_ui(ui, |ui| {
-					ui.selectable_value(&mut self.node, NodeEnum::Rino, RINO);
-					ui.selectable_value(&mut self.node, NodeEnum::Seth, SETH);
-					ui.selectable_value(&mut self.node, NodeEnum::Selsta1, SELSTA_1);
-			});
-//			);
-			});
-
-			if self.simple == false { ui.set_enabled(false); }
-			let width = (width/4.0);
-			ui.horizontal(|ui| {
-				ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node IP:"));
-				ui.spacing_mut().text_edit_width = ui.available_width() - 35.0;
-				ui.text_edit_singleline(&mut self.monerod);
-			});
-			ui.horizontal(|ui| {
-				ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node RPC Port:"));
-				ui.spacing_mut().text_edit_width = ui.available_width() - 35.0;
-				ui.text_edit_singleline(&mut self.rpc.to_string());
-			});
-			ui.horizontal(|ui| {
-				ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node ZMQ Port:"));
-				ui.spacing_mut().text_edit_width = ui.available_width() - 35.0;
-				ui.text_edit_singleline(&mut self.zmq.to_string());
-			});
-
-		})});
-
-		});
 		ui.group(|ui| {
+			let width = width/10.0;
+			ui.vertical(|ui| {
+			ui.style_mut().override_text_style = Some(Monospace);
+			ui.spacing_mut().text_edit_width = width*3.32;
+			ui.horizontal(|ui| {
+				let text;
+				let color;
+				let len = format!("{:02}", self.name.len());
+				if self.name.is_empty() {
+					text = format!("Name [ {}/30 ]➖", len);
+					color = Color32::LIGHT_GRAY;
+					incorrect_input = true;
+				} else if Regex::is_match(&regex.name, &self.name) {
+					text = format!("Name [ {}/30 ]✔", len);
+					color = Color32::from_rgb(100, 230, 100);
+				} else {
+					text = format!("Name [ {}/30 ]❌", len);
+					color = Color32::from_rgb(230, 50, 50);
+					incorrect_input = true;
+				}
+				ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
+				ui.text_edit_singleline(&mut self.name).on_hover_text(P2POOL_NAME);
+				self.name.truncate(30);
+			});
+			ui.horizontal(|ui| {
+				let text;
+				let color;
+				let len = format!("{:03}", self.ip.len());
+				if self.ip.is_empty() {
+					text = format!("  IP [{}/255]➖", len);
+					color = Color32::LIGHT_GRAY;
+					incorrect_input = true;
+				} else if self.ip == "localhost" || Regex::is_match(&regex.ipv4, &self.ip) || Regex::is_match(&regex.domain, &self.ip) {
+					text = format!("  IP [{}/255]✔", len);
+					color = Color32::from_rgb(100, 230, 100);
+				} else {
+					text = format!("  IP [{}/255]❌", len);
+					color = Color32::from_rgb(230, 50, 50);
+					incorrect_input = true;
+				}
+				ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
+				ui.text_edit_singleline(&mut self.ip).on_hover_text(P2POOL_NODE_IP);
+				self.ip.truncate(255);
+			});
+			ui.horizontal(|ui| {
+				let text;
+				let color;
+				let len = self.rpc.len();
+				if self.rpc.is_empty() {
+					text = format!(" RPC [  {}/5  ]➖", len);
+					color = Color32::LIGHT_GRAY;
+					incorrect_input = true;
+				} else if Regex::is_match(&regex.port, &self.rpc) {
+					text = format!(" RPC [  {}/5  ]✔", len);
+					color = Color32::from_rgb(100, 230, 100);
+				} else {
+					text = format!(" RPC [  {}/5  ]❌", len);
+					color = Color32::from_rgb(230, 50, 50);
+					incorrect_input = true;
+				}
+				ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
+				ui.text_edit_singleline(&mut self.rpc).on_hover_text(P2POOL_RPC_PORT);
+				self.rpc.truncate(5);
+			});
+			ui.horizontal(|ui| {
+				let text;
+				let color;
+				let len = self.zmq.len();
+				if self.zmq.is_empty() {
+					text = format!(" ZMQ [  {}/5  ]➖", len);
+					color = Color32::LIGHT_GRAY;
+					incorrect_input = true;
+				} else if Regex::is_match(&regex.port, &self.zmq) {
+					text = format!(" ZMQ [  {}/5  ]✔", len);
+					color = Color32::from_rgb(100, 230, 100);
+				} else {
+					text = format!(" ZMQ [  {}/5  ]❌", len);
+					color = Color32::from_rgb(230, 50, 50);
+					incorrect_input = true;
+				}
+				ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
+				ui.text_edit_singleline(&mut self.zmq).on_hover_text(P2POOL_ZMQ_PORT);
+				self.zmq.truncate(5);
+			});
+		});
+
+		ui.vertical(|ui| {
+			let width = ui.available_width();
+			ui.add_space(1.0);
+			// [Manual node selection]
+			ui.spacing_mut().slider_width = width - 8.0;
+			ui.spacing_mut().icon_width = width / 25.0;
+			// [Ping List]
+			let text = RichText::new(format!("{} | {}", self.selected_name, self.selected_ip));
+			ComboBox::from_id_source("nodes").selected_text(RichText::text_style(text, Monospace)).show_ui(ui, |ui| {
+				for (name, node) in node_vec.iter() {
+					let text = RichText::text_style(RichText::new(format!("{}\n     IP: {}\n    RPC: {}\n    ZMQ: {}", name, node.ip, node.rpc, node.zmq)), Monospace);
+					ui.selectable_value(&mut self.selected_name, name.clone(), text);
+//					println!("{}", value.ip);
+				}
+			});
+			// [Add] + [Delete]
+			let node_vec_len = node_vec.len();
+			ui.horizontal(|ui| {
+				let mut exists = false;
+				for (name, _) in node_vec.iter() {
+					if *name == self.name { exists = true; }
+				}
+				ui.set_enabled(!incorrect_input && !exists && node_vec_len < 100);
+				let text = format!("{}\n      Max amount of nodes: 100\n      Current amount: [{}/100]", P2POOL_ADD, node_vec_len);
+				if ui.add_sized([width, text_edit], Button::new("Add")).on_hover_text(text).clicked() {
+					let node = Node {
+						ip: self.ip.clone(),
+						rpc: self.rpc.clone(),
+						zmq: self.zmq.clone(),
+					};
+					node_vec.push((self.name.clone(), node));
+				}
+			});
+			ui.horizontal(|ui| {
+				ui.set_enabled(node_vec_len > 1);
+				let text = format!("{}\n      Max amount of nodes: 100\n      Current amount: [{}/100]", P2POOL_DELETE, node_vec_len);
+				if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() {
+					let mut n = 0;
+					for (name, _) in node_vec.iter() {
+						if *name == self.selected_name {
+							self.selected_name = node_vec[n-1].0.clone();
+							node_vec.remove(n);
+							break
+						}
+						n += 1;
+					}
+				}
+			});
+			ui.horizontal(|ui| {
+				ui.set_enabled(!self.name.is_empty() || !self.ip.is_empty() || !self.rpc.is_empty() || !self.zmq.is_empty());
+				if ui.add_sized([width, text_edit], Button::new("Clear")).on_hover_text(P2POOL_CLEAR).clicked() {
+					self.name.clear();
+					self.ip.clear();
+					self.rpc.clear();
+					self.zmq.clear();
+				}
+			});
+		});
+		});
+		});
+		ui.add_space(5.0);
+
+		// [Main/Mini]
 		ui.horizontal(|ui| {
-			ui.spacing_mut().text_edit_width = ui.available_width();
-			ui.label("Address:");
-			ui.text_edit_singleline(&mut self.address);
+		let height = height/3.0;
+		ui.group(|ui| { ui.horizontal(|ui| {
+			let width = (width/4.0)-SPACE;
+			let height = height + 6.0;
+			if ui.add_sized([width, height], SelectableLabel::new(self.mini == false, "P2Pool Main")).on_hover_text(P2POOL_MAIN).clicked() { self.mini = false; }
+			if ui.add_sized([width, height], SelectableLabel::new(self.mini == true, "P2Pool Mini")).on_hover_text(P2POOL_MINI).clicked() { self.mini = true; }
 		})});
+		// [Out/In Peers] + [Log Level]
+		ui.group(|ui| { ui.vertical(|ui| {
+			let text = (ui.available_width()/10.0)-SPACE;
+			let width = (text*8.0)-SPACE;
+			let height = height/3.0;
+			ui.style_mut().spacing.slider_width = width/1.2;
+			ui.style_mut().spacing.interact_size.y = height;
+			ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
+//			ui.style_mut().override_text_style = Some(Monospace);
+			ui.horizontal(|ui| {
+				ui.add_sized([text, height], Label::new("Out peers [10-450]:"));
+				ui.add_sized([width, height], Slider::new(&mut self.out_peers, 10..=450)).on_hover_text(P2POOL_OUT);
+				ui.add_space(ui.available_width()-4.0);
+			});
+			ui.horizontal(|ui| {
+				ui.add_sized([text, height], Label::new(" In peers [10-450]:"));
+				ui.add_sized([width, height], Slider::new(&mut self.in_peers, 10..=450)).on_hover_text(P2POOL_IN);
+			});
+			ui.horizontal(|ui| {
+				ui.add_sized([text, height], Label::new("   Log level [0-6]:"));
+				ui.add_sized([width, height], Slider::new(&mut self.log_level, 0..=6)).on_hover_text(P2POOL_LOG);
+			});
+		})});
+		});
 	}
 	}
 }
diff --git a/src/state.rs b/src/state.rs
deleted file mode 100644
index 9fd367d..0000000
--- a/src/state.rs
+++ /dev/null
@@ -1,350 +0,0 @@
-// Gupax - GUI Uniting P2Pool And XMRig
-//
-// Copyright (c) 2022 hinto-janaiyo
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-// This handles reading/parsing the state file: [gupax.toml]
-// The TOML format is used. This struct hierarchy directly
-// translates into the TOML parser:
-//   State/
-//   ├─ Gupax/
-//   │  ├─ ...
-//   ├─ P2pool/
-//   │  ├─ ...
-//   ├─ Xmrig/
-//   │  ├─ ...
-//   ├─ Version/
-//      ├─ ...
-
-use std::{fs,env};
-use std::fmt::Display;
-use std::path::{Path,PathBuf};
-use std::result::Result;
-use std::sync::{Arc,Mutex};
-use serde::{Serialize,Deserialize};
-use figment::Figment;
-use figment::providers::{Format,Toml};
-use crate::constants::HORIZONTAL;
-use log::*;
-
-//---------------------------------------------------------------------------------------------------- Impl
-impl State {
-	pub fn default() -> Self {
-		use crate::constants::{P2POOL_VERSION,XMRIG_VERSION};
-		let max_threads = num_cpus::get();
-		let current_threads;
-		if max_threads == 1 { current_threads = 1; } else { current_threads = max_threads / 2; }
-		Self {
-			gupax: Gupax {
-				auto_update: true,
-				auto_node: true,
-				ask_before_quit: true,
-				save_before_quit: true,
-				update_via_tor: true,
-				p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
-				xmrig_path: DEFAULT_XMRIG_PATH.to_string(),
-				absolute_p2pool_path: Self::into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(),
-				absolute_xmrig_path: Self::into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(),
-			},
-			p2pool: P2pool {
-				simple: true,
-				mini: true,
-				auto_node: true,
-				auto_select: true,
-				out_peers: 10,
-				in_peers: 10,
-				log_level: 3,
-				node: crate::NodeEnum::C3pool,
-				monerod: "localhost".to_string(),
-				rpc: 18081,
-				zmq: 18083,
-				address: "".to_string(),
-			},
-			xmrig: Xmrig {
-				simple: true,
-				tls: false,
-				nicehash: false,
-				keepalive: false,
-				hugepages_jit: true,
-				current_threads,
-				max_threads,
-				priority: 2,
-				pool: "localhost:3333".to_string(),
-				address: "".to_string(),
-			},
-			version: Arc::new(Mutex::new(Version {
-				p2pool: Arc::new(Mutex::new(P2POOL_VERSION.to_string())),
-				xmrig: Arc::new(Mutex::new(XMRIG_VERSION.to_string())),
-			})),
-		}
-	}
-
-	pub fn get_path() -> Result<PathBuf, TomlError> {
-		// Get OS data folder
-		// Linux   | $XDG_DATA_HOME or $HOME/.local/share | /home/alice/.local/state
-		// macOS   | $HOME/Library/Application Support    | /Users/Alice/Library/Application Support
-		// Windows | {FOLDERID_RoamingAppData}            | C:\Users\Alice\AppData\Roaming
-		let mut path = match dirs::data_dir() {
-			Some(mut path) => {
-				path.push(STATE_DIRECTORY);
-				info!("OS data path ... OK");
-				path
-			},
-			None => { error!("Couldn't get OS PATH for data"); return Err(TomlError::Path(PATH_ERROR.to_string())) },
-		};
-		// Create directory
-		fs::create_dir_all(&path)?;
-		path.push(STATE_FILE);
-		info!("TOML path ... {}", path.display());
-		Ok(path)
-	}
-
-	// Attempts to read [gupax.toml] or
-	// attempts to create if not found.
-	pub fn read_or_create(path: PathBuf) -> Result<String, TomlError> {
-		// Attempt to read file, create default if not found
-		match fs::read_to_string(&path) {
-			Ok(string) => {
-				info!("TOML read ... OK");
-				Ok(string)
-			},
-			Err(err) => {
-				warn!("TOML not found, attempting to create default");
-				let default = match toml::ser::to_string(&Self::default()) {
-						Ok(o) => { info!("TOML serialization ... OK"); o },
-						Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) },
-				};
-				fs::write(&path, &default)?;
-				info!("TOML write ... OK");
-				Ok(default)
-			},
-		}
-	}
-
-	// Attempt to parse from String
-	// If failed, assume we're working with an old [State]
-	// and attempt to merge it with a new [State::default()].
-	pub fn parse(string: String) -> Result<Self, TomlError> {
-		match toml::de::from_str(&string) {
-			Ok(toml) => {
-				info!("TOML parse ... OK");
-				Self::info(&toml);
-				Ok(toml)
-			},
-			Err(err) => {
-				warn!("Couldn't parse TOML, assuming old [State], attempting merge...");
-				Self::merge(&Self::default())
-			},
-		}
-	}
-
-	// Last three functions combined
-	// get_path() -> read_or_create() -> parse()
-	pub fn get() -> Result<Self, TomlError> {
-		Self::parse(Self::read_or_create(Self::get_path()?)?)
-	}
-
-	// Completely overwrite current [gupax.toml]
-	// with a new default version, and return [Self].
-	pub fn new_default() -> Result<Self, TomlError> {
-		info!("Creating new default TOML...");
-		let default = Self::default();
-		let path = Self::get_path()?;
-		let string = match toml::ser::to_string(&default) {
-				Ok(o) => { info!("TOML serialization ... OK"); o },
-				Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) },
-		};
-		fs::write(&path, &string)?;
-		info!("TOML write ... OK");
-		Ok(default)
-	}
-
-	// Turn relative paths into absolute paths
-	fn into_absolute_path(path: String) -> Result<PathBuf, TomlError> {
-		let path = PathBuf::from(path);
-		if path.is_relative() {
-			let mut dir = std::env::current_exe()?;
-			dir.pop();
-			dir.push(path);
-			Ok(dir)
-		} else {
-			Ok(path)
-		}
-	}
-
-	// Save [State] onto disk file [gupax.toml]
-	pub fn save(&mut self) -> Result<(), TomlError> {
-		info!("Saving TOML to disk...");
-		let path = Self::get_path()?;
-		// Convert path to absolute
-		self.gupax.absolute_p2pool_path = Self::into_absolute_path(self.gupax.p2pool_path.clone())?;
-		self.gupax.absolute_xmrig_path = Self::into_absolute_path(self.gupax.xmrig_path.clone())?;
-		let string = match toml::ser::to_string(&self) {
-			Ok(string) => {
-				info!("TOML parse ... OK");
-				Self::info(&self);
-				string
-			},
-			Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) },
-		};
-		match fs::write(path, string) {
-			Ok(_) => { info!("TOML save ... OK"); Ok(()) },
-			Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) },
-		}
-	}
-
-	// Take [Self] as input, merge it with whatever the current [default] is,
-	// leaving behind old keys+values and updating [default] with old valid ones.
-	// Automatically overwrite current file.
-	pub fn merge(old: &Self) -> Result<Self, TomlError> {
-		info!("Starting TOML merge...");
-		let old = match toml::ser::to_string(&old) {
-			Ok(string) => { info!("Old TOML parse ... OK"); string },
-			Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) },
-		};
-		let default = match toml::ser::to_string(&Self::default()) {
-			Ok(string) => { info!("Default TOML parse ... OK"); string },
-			Err(err) => { error!("Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) },
-		};
-		let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() {
-			Ok(new) => { info!("TOML merge ... OK"); new },
-			Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) },
-		};
-		// Attempt save
-		Self::save(&mut new)?;
-		Ok(new)
-	}
-
-	// Write [Self] to console with
-	// [info!] surrounded by "---"
-	pub fn info(&self) -> Result<(), toml::ser::Error> {
-		info!("{}", HORIZONTAL);
-		for i in toml::ser::to_string(&self)?.lines() { info!("{}", i); }
-		info!("{}", HORIZONTAL);
-		Ok(())
-	}
-}
-
-impl Display for TomlError {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-		use TomlError::*;
-		match self {
-			Io(err) => write!(f, "{}: Io | {}", ERROR, err),
-			Path(err) => write!(f, "{}: Path | {}", ERROR, err),
-			Serialize(err) => write!(f, "{}: Serialize | {}", ERROR, err),
-			Deserialize(err) => write!(f, "{}: Deserialize | {}", ERROR, err),
-			Merge(err) => write!(f, "{}: Merge | {}", ERROR, err),
-		}
-	}
-}
-
-impl From<std::io::Error> for TomlError {
-	fn from(err: std::io::Error) -> Self {
-		TomlError::Io(err)
-	}
-}
-
-//---------------------------------------------------------------------------------------------------- Const
-// State file
-const STATE_FILE: &'static str = "gupax.toml";
-const ERROR: &'static str = "TOML Error";
-const PATH_ERROR: &'static str = "PATH for state directory could not be not found";
-#[cfg(target_os = "windows")]
-const STATE_DIRECTORY: &'static str = "Gupax";
-#[cfg(target_os = "macos")]
-const STATE_DIRECTORY: &'static str = "com.github.hinto-janaiyo.gupax";
-#[cfg(target_os = "linux")]
-const STATE_DIRECTORY: &'static str = "gupax";
-
-#[cfg(target_os = "windows")]
-pub const DEFAULT_P2POOL_PATH: &'static str = r"P2Pool\p2pool.exe";
-#[cfg(target_family = "unix")]
-pub const DEFAULT_P2POOL_PATH: &'static str = "p2pool/p2pool";
-#[cfg(target_os = "windows")]
-pub const DEFAULT_XMRIG_PATH: &'static str = r"XMRig\xmrig.exe";
-#[cfg(target_family = "unix")]
-pub const DEFAULT_XMRIG_PATH: &'static str = "xmrig/xmrig";
-
-//---------------------------------------------------------------------------------------------------- Error Enum
-#[derive(Debug)]
-pub enum TomlError {
-	Io(std::io::Error),
-	Path(String),
-	Serialize(toml::ser::Error),
-	Deserialize(toml::de::Error),
-	Merge(figment::Error),
-}
-
-//---------------------------------------------------------------------------------------------------- Structs
-#[derive(Clone,Debug,Deserialize,Serialize)]
-pub struct State {
-	pub gupax: Gupax,
-	pub p2pool: P2pool,
-	pub xmrig: Xmrig,
-	pub version: Arc<Mutex<Version>>,
-}
-
-#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
-pub struct Gupax {
-	pub auto_update: bool,
-	pub auto_node: bool,
-	pub ask_before_quit: bool,
-	pub save_before_quit: bool,
-	pub update_via_tor: bool,
-	pub p2pool_path: String,
-	pub xmrig_path: String,
-	pub absolute_p2pool_path: PathBuf,
-	pub absolute_xmrig_path: PathBuf,
-}
-
-#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
-pub struct P2pool {
-	pub simple: bool,
-	pub mini: bool,
-	pub auto_node: bool,
-	pub auto_select: bool,
-	pub out_peers: u16,
-	pub in_peers: u16,
-	pub log_level: u8,
-	pub node: crate::node::NodeEnum,
-	pub monerod: String,
-	pub rpc: u16,
-	pub zmq: u16,
-	pub address: String,
-//	pub config: String,
-//	pub args: String,
-}
-
-#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
-pub struct Xmrig {
-	pub simple: bool,
-	pub tls: bool,
-	pub nicehash: bool,
-	pub keepalive: bool,
-	pub hugepages_jit: bool,
-	pub max_threads: usize,
-	pub current_threads: usize,
-	pub priority: u8,
-	pub pool: String,
-	pub address: String,
-//	pub config: String,
-//	pub args: String,
-}
-
-#[derive(Clone,Debug,Deserialize,Serialize)]
-pub struct Version {
-	pub p2pool: Arc<Mutex<String>>,
-	pub xmrig: Arc<Mutex<String>>,
-}
diff --git a/src/update.rs b/src/update.rs
index ba05425..ca30856 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -30,7 +30,7 @@ use arti_hyper::*;
 use arti_hyper::*;
 use crate::constants::GUPAX_VERSION;
 //use crate::{Name::*,State};
-use crate::state::*;
+use crate::disk::*;
 use crate::update::Name::*;
 use hyper::{Client,Body,Request};
 use hyper::header::HeaderValue;
diff --git a/src/xmrig.rs b/src/xmrig.rs
index ec12d74..f2389fe 100644
--- a/src/xmrig.rs
+++ b/src/xmrig.rs
@@ -21,7 +21,7 @@ use std::str::FromStr;
 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 use num_cpus;
 use crate::constants::*;
-use crate::state::Xmrig;
+use crate::disk::Xmrig;
 
 impl Xmrig {
 	pub fn show(&mut self, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {