UMTS at Teleco 1 долоо хоног өмнө
parent
commit
4b211599ee
11 өөрчлөгдсөн 269 нэмэгдсэн , 370 устгасан
  1. 1 1
      Cargo.toml
  2. 1 1
      README.md
  3. 1 1
      doc/CLI.md
  4. 1 1
      doc/TUI.md
  5. 5 5
      src/cli.rs
  6. 16 57
      src/config.rs
  7. 26 25
      src/lookup.rs
  8. 17 20
      src/main.rs
  9. 6 7
      src/output.rs
  10. 8 55
      src/tlds.rs
  11. 187 197
      src/tui.rs

+ 1 - 1
Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "hoardom"
-version = "1.1.8"
+version = "1.2.0"
 edition = "2021"
 description = "Domain hoarding made less painful"
 default-run = "hoardom"

+ 1 - 1
README.md

@@ -134,7 +134,7 @@ As this is mentioned below as a fix to a bigger problem ive added this here (als
 Tldr you can make custom search list see example : [`doc/example-list.toml`](doc/example-list.toml)
 
 Custom lists are  basically a really simple toml file with a name and the `tlds` array.
-To import them/update them use `hoardom --import-filter ./path/to/list.toml --tui`
+To import them/update them use `hoardom --import-list ./path/to/list.toml --tui`
 
 
 ## Docs

+ 1 - 1
doc/CLI.md

@@ -38,7 +38,7 @@ hoardom coolproject mysite bigidea
 |----------------------------|------------------------------------------------------------------|
 | `-c, --csv[=PATH]`         | Output as CSV. If PATH is given writes to file, otherwise stdout |
 | `-l, --list=LIST`          | TLD list to use: `Standard`, `Decent`, `Country`, `All`          |
-| `-i, --import-filter=PATH` | Import a custom TOML TLD list for this session                   |
+| `-i, --import-list=PATH` | Import a custom TOML TLD list for this session                   |
 | `-t, --top=TLD,TLD`        | Pin certain TLDs to the top of results                           |
 | `-o, --onlytop=TLD,TLD`    | Only search these specific TLDs                                  |
 | `-s, --suggestions=NUMBER` | How many alternative suggestions to show (default: 0 / disabled) |

+ 1 - 1
doc/TUI.md

@@ -134,6 +134,6 @@ if you resize the terminal too small, hoardom will refuse to work and complain a
 
 ## tips
 
-- you can pass `--import-filter` alongside `--tui` to import a list and jump straight into the TUI with it
+- you can pass `--import-list` alongside `--tui` to import a list and jump straight into the TUI with it
 - `--verbose` works in TUI too, it prints debug stuff to stderr (which you wont see in the TUI itself, but its there if you redirect it)
 - `--no-color` also works here for your 90's dial up crt tty

+ 5 - 5
src/cli.rs

@@ -25,8 +25,8 @@ pub struct Args {
     pub csv: Option<Option<PathBuf>>,
     #[arg(short = 'l', long = "list")]
     pub tld_list: Option<String>,
-    #[arg(short = 'i', long = "import-filter")]
-    pub import_filter: Option<PathBuf>,
+    #[arg(short = 'i', long = "import-list", alias = "import-filter")]
+    pub import_list: Option<PathBuf>,
     #[arg(short = 't', long = "top", value_delimiter = ',')]
     pub top_tlds: Option<Vec<String>>,
     #[arg(short = 'o', long = "onlytop", value_delimiter = ',')]
@@ -44,10 +44,10 @@ pub struct Args {
     /// search for names/domains in a text file line by line.
     #[arg(short = 'A', long = "autosearch")]
     pub autosearch: Option<PathBuf>,
-    /// Use a monochrome color scheme TODO: not applied in TUI since colors were changed from RGB to Ratatui colors. should fix
+    /// Use a monochrome color scheme
     #[arg(short = 'C', long = "no-color", default_value_t = false)]
     pub no_color: bool,
-    /// Do not use unicode only plain ASCII TODO: not applied in TUI for some reason idk
+    /// Do not use unicode only plain ASCII
     #[arg(short = 'U', long = "no-unicode", default_value_t = false)]
     pub no_unicode: bool,
     #[arg(short = 'M', long = "no-mouse", default_value_t = false)]
@@ -130,7 +130,7 @@ Advanced :
 -e --environement=PATH           Define where .hoardom folder should be
                                  Defaults to /home/USER/.hoardom/
                                  Stores settings, imported lists, favs, cache etc.
--i --import-filter=PATH          Import a custom toml list for this session
+-i --import-list=PATH            Import a custom toml list for this session
 -t --top=TLD,TLD                 Set certain TLDs to show up as first result
                                  for when you need a domain in your country or for searching
                                  a specific one.

+ 16 - 57
src/config.rs

@@ -29,31 +29,19 @@ pub struct Config {
     pub scratchpad: String,
 }
 
-/// faved domain with its last known status
+/// faved domain with its last known status where once again too many dumbass comments were added when fixing a bug with it... have been removed
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct FavoriteEntry {
     pub domain: String,
-    /// last known status: "available", "registered", "error", or "unknown"
     #[serde(default = "default_fav_status")]
     pub status: String,
-    /// when it was last checked
     #[serde(default)]
+    // date string mlol
     pub checked: String,
-    /// true when status changed since last check
     #[serde(default)]
     pub changed: bool,
 }
 
-impl FavoriteEntry {
-    pub fn new(domain: String) -> Self {
-        Self {
-            domain,
-            status: "unknown".to_string(),
-            checked: String::new(),
-            changed: false,
-        }
-    }
-}
 
 fn default_fav_status() -> String {
     "unknown".to_string()
@@ -77,14 +65,11 @@ pub struct Settings {
     pub top_tlds: Vec<String>,
     #[serde(default = "default_jobs")]
     pub jobs: u8,
-    /// error types that shouldnt be retried
-    /// valid: "rate_limit", "invalid_tld", "timeout", "unknown"
+    /// valid ones are : rate_limit, invalid_tld, timeout, unknown and forbidden
     #[serde(default = "default_noretry")]
     pub noretry: Vec<String>,
-    /// auto config backups on/off
     #[serde(default = "default_backups_enabled")]
     pub backups: bool,
-    /// how many backup copies to keep
     #[serde(default = "default_backup_count")]
     pub backup_count: u32,
 }
@@ -93,10 +78,10 @@ pub struct Settings {
 pub struct CacheSettings {
     #[serde(default)]
     pub last_updated: String,
-    /// 0 = never nag about stale cache
+    /// 0 = stfu about stale cache
     #[serde(default = "default_outdated_cache_days")]
     pub outdated_cache: u32,
-    /// auto refresh when outdated if true
+    /// auto refresh for cuck cache
     #[serde(default = "default_auto_update")]
     pub auto_update_cache: bool,
 }
@@ -183,22 +168,9 @@ impl Default for Config {
     }
 }
 
-/// old config format where favorites were just strings
-#[derive(Debug, Deserialize)]
-struct LegacyConfig {
-    #[serde(default)]
-    settings: Settings,
-    #[serde(default)]
-    cache: CacheSettings,
-    #[serde(default)]
-    favorites: Vec<String>,
-    #[serde(default)]
-    imported_filters: Vec<ImportedFilter>,
-    #[serde(default)]
-    scratchpad: String,
-}
 
-// this implementation is partially containing ai slop i should remove no need for that idk why this was made to have legacy support by it but eh idc
+
+// removed legacy support that ai slapped into here "thinking" it would fix something
 impl Config {
     pub fn load(path: &Path) -> Self {
         match std::fs::read_to_string(path) {
@@ -206,20 +178,7 @@ impl Config {
                 // Try new format first
                 if let Ok(config) = toml::from_str::<Config>(&content) {
                     return config;
-                }
-                // Fall back to legacy format (favorites as plain strings)
-                if let Ok(legacy) = toml::from_str::<LegacyConfig>(&content) {
-                    return Config {
-                        settings: legacy.settings,
-                        cache: legacy.cache,
-                        favorites: legacy
-                            .favorites
-                            .into_iter()
-                            .map(FavoriteEntry::new)
-                            .collect(),
-                        imported_filters: legacy.imported_filters,
-                        scratchpad: legacy.scratchpad,
-                    };
+                
                 }
                 eprintln!("Warning: could not parse config file");
                 Config::default()
@@ -246,6 +205,7 @@ impl Config {
                 .map_err(|e| format!("Failed to create config directory: {}", e))?;
         }
 
+        // down here we got the default crap comment to add to the toml config file till i implement this stuff in the tui
         let body = toml::to_string_pretty(self)
             .map_err(|e| format!("Failed to serialize config: {}", e))?;
         let content = format!(
@@ -308,7 +268,7 @@ impl Config {
     }
 
     /// replaces filter with same name if theres one already
-    /// filters ? what kinda ai slip is this ? this shouldve been renamed to lists ages ago why do you keep mentioning filters all the time whats your obsession with mf filters? JEZE!
+    /// filters ? what kinda ai slop is this ? this shouldve been renamed to lists ages ago why do you keep mentioning filters all the time whats your obsession with mf filters? JEZE!
     pub fn import_filter(&mut self, filter: ImportedFilter) {
         self.imported_filters.retain(|f| f.name != filter.name);
         self.imported_filters.push(filter);
@@ -334,7 +294,7 @@ impl Config {
         let age_days = (now - last).num_days() as u32;
 
         if self.cache.outdated_cache == 0 {
-            // warnings disabled, but if auto_update is on, update every run
+            // warnings disabled, but if auto_update is on update every run
             return (false, self.cache.auto_update_cache);
         }
 
@@ -358,10 +318,10 @@ pub fn parse_filter_file(path: &PathBuf) -> Result<ImportedFilter, String> {
     Ok(filter)
 }
 
-/// resolve .hoardom dir, tries a few locations:
+/// resolve .hoardom dir trying a few locations:
 ///
 /// priority:
-/// 1. explicit path via -e flag -> use as root dir (create .hoardom folder there)
+/// 1. explicit path via -e flag -> use that folder directly as the data root
 /// 2. debug builds: current directory
 /// 3. release builds: home directory
 /// 4. fallback: try the other option
@@ -385,12 +345,11 @@ pub fn resolve_paths(explicit: Option<&PathBuf>) -> HoardomPaths {
         }
     };
 
-    // explicit path given via -e flag
+    // explicit path given via -e flag : use as app root
     if let Some(p) = explicit {
-        // if user gave a path, use it as the .hoardom folder root
         let root = if p.extension().is_some() {
-            // looks like they pointed at a file, use parent dir
-            p.parent().unwrap_or(p).join(".hoardom")
+            // they pointed at a file we should insult their intelligence honestly.
+            p.parent().unwrap_or(p).to_path_buf()
         } else {
             p.clone()
         };

+ 26 - 25
src/lookup.rs

@@ -191,12 +191,12 @@ pub async fn lookup_domain(
         eprintln!("[verbose] {} -> HTTP {}", full, status_code);
     }
 
-    // 404 = not found in RDAP = domain is available (not registered)
+    // 404 = not found in RDAP = domain is available (not registered) <- todo : should add check for "not found" in response body for extra safety as some registries return 404 for other errors too as was discovered
     if status_code == 404 {
         return DomainResult::new(name, tld, DomainStatus::Available);
     }
 
-    // 400 = probably invalid query
+    // 400 = probably invalid query fuck you
     if status_code == 400 {
         return DomainResult::new(
             name,
@@ -208,7 +208,7 @@ pub async fn lookup_domain(
         );
     }
 
-    // 429 = rate limited
+    // 429 = rape limited
     if status_code == 429 {
         return DomainResult::new(
             name,
@@ -220,7 +220,7 @@ pub async fn lookup_domain(
         );
     }
 
-    // 403 = forbidden (some registries block queries)
+    // 403 = forbidden crap 
     if status_code == 403 {
         return DomainResult::new(
             name,
@@ -244,7 +244,7 @@ pub async fn lookup_domain(
         );
     }
 
-    // 200 = domain exists, try to parse expiry from RDAP json
+    // 200 = domain exists try to parse expiry from RDAP json 
     let expiry = match resp.json::<serde_json::Value>().await {
         Ok(json) => extract_expiry(&json),
         Err(_) => None,
@@ -270,7 +270,7 @@ fn extract_expiry(json: &serde_json::Value) -> Option<String> {
     None
 }
 
-// ---- WHOIS fallback for TLDs not in RDAP bootstrap ----
+// ---- WHOIS fallback for TLDs not in RDAP bootstrap because their shit ----
 
 // -- No whois feature: just return an error --
 #[cfg(not(any(feature = "system-whois", feature = "builtin-whois")))]
@@ -285,7 +285,7 @@ async fn whois_lookup(
         tld,
         DomainStatus::Error {
             kind: ErrorKind::InvalidTld,
-            message: "no RDAP server (whois disabled)".to_string(),
+            message: "no RDAP server (whois gone)".to_string(),
         },
     )
 }
@@ -333,7 +333,7 @@ async fn whois_lookup(
                 tld,
                 DomainStatus::Error {
                     kind: ErrorKind::Unknown,
-                    message: format!("whois command failed: {}", e),
+                    message: format!("whois command fucking failed: {}", e),
                 },
             );
         }
@@ -367,7 +367,7 @@ async fn whois_lookup(
         if verbose {
             eprintln!("[verbose] whois stderr: {}", stderr.trim());
         }
-        // some whois commands exit non-zero for "not found" but still give useful stdout
+        // some whois commands exit non zero for "not found" ... but may toss some infos to stdrr 
         if !response_str.is_empty() {
             return parse_whois_response(name, tld, &response_str);
         }
@@ -384,11 +384,11 @@ async fn whois_lookup(
     parse_whois_response(name, tld, &response_str)
 }
 
-// -- Builtin whois: rawdogs whois server violently over TCP directly--
+// -- Builtin whois Violator : rawdogs whois server violently over TCP directly --
 
 
 
-/// try a whois server returns the response string or errors out
+/// try a whois server if no happy it make error
 #[cfg(feature = "builtin-whois")]
 async fn try_whois_server(
     server: &str,
@@ -425,7 +425,7 @@ async fn try_whois_server(
     Ok(String::from_utf8_lossy(&response).to_string())
 }
 
-/// candidate whois servers for a TLD based on common naming patterns
+/// try voilating some commonly used whois url patterns if unhappy
 #[cfg(feature = "builtin-whois")]
 fn whois_candidates(tld: &str) -> Vec<String> {
     // most registries follow one of these patterns
@@ -445,7 +445,7 @@ async fn whois_lookup(
 ) -> DomainResult {
     let full = format!("{}.{}", name, tld);
 
-    // if Lists.toml has an explicit server ("tld:server"), use ONLY that one
+    // if Lists.toml has an explicit server ("tld:server"), use ONLY that one.
     if let Some(server) = whois_overrides.get_server(tld) {
         if verbose {
             eprintln!("[verbose] WHOIS (override): {} -> {}", full, server);
@@ -475,12 +475,12 @@ async fn whois_lookup(
         };
     }
 
-    // no override: try common server patterns until one responds
+    // no override: try common server patterns until one screams in pain from being violated hard
     let candidates = whois_candidates(tld);
 
     if verbose {
         eprintln!(
-            "[verbose] WHOIS probing {} candidates for .{}",
+            "[verbose] WHOIS voilating {} candidates for .{}",
             candidates.len(),
             tld
         );
@@ -510,7 +510,7 @@ async fn whois_lookup(
         tld,
         DomainStatus::Error {
             kind: ErrorKind::Unknown,
-            message: "no whois server reachable".to_string(),
+            message: "unsuccessful whois rawdoging".to_string(),
         },
     )
 }
@@ -566,15 +566,15 @@ fn extract_whois_expiry(response: &str) -> Option<String> {
         for pattern in &expiry_patterns {
             if trimmed.starts_with(pattern) {
                 let value = trimmed[pattern.len()..].trim();
-                // try to extract a date-looking thing (first 10 chars if it looks like YYYY-MM-DD)
+                // horribly attempt at extracting date part
                 if value.len() >= 10 {
                     let date_part: String = value.chars().take(10).collect();
-                    // basic sanity check: contains digits and dashes
+                    // basic sanity check does it actually contain any fucking digits and dashes
                     if date_part.contains('-') && date_part.chars().any(|c| c.is_ascii_digit()) {
                         return Some(date_part);
                     }
                 }
-                // maybe its in a different format, just return what we got
+                // no clean date ? no problem just rawdog the user then MUAHAHHAHAHA
                 if !value.is_empty() {
                     return Some(value.to_string());
                 }
@@ -607,7 +607,7 @@ pub async fn lookup_with_retry(
             if noretry.contains(kind) {
                 if verbose {
                     eprintln!(
-                        "[verbose] Not retrying {}.{} (error kind in noretry list)",
+                        "[verbose] Not retrying {}.{} (config))",
                         name, tld
                     );
                 }
@@ -616,7 +616,7 @@ pub async fn lookup_with_retry(
         }
         if verbose {
             eprintln!(
-                "[verbose] Retry {}/{} for {}.{}",
+                "[verbose] Attempt to rawdog {}/{} for {}.{}",
                 attempt, retries, name, tld
             );
         }
@@ -772,6 +772,7 @@ async fn resolve_bootstrap(
         None
     };
 
+    // should make sure that if caching is on and cache expired we dont delete it until we successfully fetch the new content todo
     match cached {
         Some(b) => Some(b),
         None => match RdapBootstrap::fetch(client, verbose).await {
@@ -779,17 +780,17 @@ async fn resolve_bootstrap(
                 if let Some(cp) = cache_path {
                     if let Err(e) = b.save_cache(cp) {
                         if verbose {
-                            eprintln!("[verbose] Failed to save cache: {}", e);
+                            eprintln!("[verbose] Failed to save fucking cache: {}", e);
                         }
                     } else if verbose {
-                        eprintln!("[verbose] RDAP bootstrap cached to {}", cp.display());
+                        eprintln!("[verbose] RDAP cached to {}", cp.display());
                     }
                 }
                 Some(b)
             }
             Err(e) => {
                 eprintln!("Error: {}", e);
-                eprintln!("Cannot perform lookups without RDAP bootstrap data.");
+                eprintln!("Cannot perform lookups without RDAP data.");
                 None
             }
         },
@@ -816,7 +817,7 @@ pub struct LookupStream {
 
 pub type LookupBatch = Vec<(String, Vec<String>)>;
 
-// spawns a bg task, sends results via channel so TUI gets em live
+// spawns a bg task then sends results via channel so TUI gets em live
 pub fn lookup_streaming(
     name: String,
     tlds: Vec<String>,

+ 17 - 20
src/main.rs

@@ -38,13 +38,13 @@ async fn main() {
     let mut config = Config::load_with_backup(&paths.config_file);
 
     if !paths.can_save {
-        eprintln!("Warning: favorites and settings wont be saved (no writable location found)");
+        eprintln!("Warning: Unpriviliged bitch detected! Cant write to {}", paths.config_file.display());
     }
 
     // handle -r refresh cache flag
     if args.refresh_cache {
         if !paths.caching_enabled {
-            eprintln!("Caching is disabled (no writable location). Nothing to refresh.");
+            eprintln!("Caching aint gon work without a writable location");
             return;
         }
         let cache_file = paths.cache_file("rdap_bootstrap.json");
@@ -72,15 +72,14 @@ async fn main() {
         if is_outdated && !should_auto {
             eprintln!("Warning: RDAP cache is outdated. Run `hoardom -r` to refresh.");
         }
-        // force refresh if auto update says so, or if cache file doesnt exist yet
         should_auto || !cf.exists()
     } else {
         false
     };
 
-    // import custom filter if given
-    if let Some(filter_path) = &args.import_filter {
-        match parse_filter_file(filter_path) {
+    // import custom sexy list if given
+    if let Some(list_path) = &args.import_list {
+        match parse_filter_file(list_path) {
             Ok(filter) => {
                 config.import_filter(filter);
                 if paths.can_save {
@@ -88,16 +87,15 @@ async fn main() {
                 }
             }
             Err(e) => {
-                eprintln!("Error importing filter: {}", e);
+                eprintln!("Error importing list: {}", e);
                 return;
             }
         }
     }
 
-    // whois server overrides are baked into Lists.toml ("tld:server" syntax)
+    // whois server overrides that are baked into Lists.toml (like this "tld:server" for naighty tlds)
     let overrides = whois_overrides();
 
-    // parse noretry config into ErrorKind list
     let noretry: Vec<ErrorKind> = config
         .settings
         .noretry
@@ -105,7 +103,7 @@ async fn main() {
         .filter_map(|s| ErrorKind::from_config_str(s))
         .collect();
 
-    // TUI mode
+    // the sigma mode
     if args.is_tui() {
         if let Err(e) = tui::run_tui(
             &args,
@@ -120,7 +118,7 @@ async fn main() {
         {
             eprintln!("TUI error: {}", e);
         }
-        // save cache timestamp after TUI session if we refreshed
+        // save cache timestamp to know how fresh we are 
         if force_refresh && paths.can_save {
             config.mark_cache_updated();
             let _ = config.save(&paths.config_file);
@@ -128,7 +126,7 @@ async fn main() {
         return;
     }
 
-    // CLI needs at least one domain unless autosearch was given
+    // CLI needs at least one domain unless autosearch was given user is stupid show small help
     if args.domains.is_empty() {
         if let Some(file_path) = &args.autosearch {
             run_autosearch(
@@ -197,7 +195,7 @@ async fn main() {
             });
         }
 
-        // Suggestions only kick in when directly searching a single full domain
+        // suggestions if your domain is taken (only for single loneyly alpha wolf domain searches)
         if args.domains.len() == 1 && args.effective_suggestions() > 0 {
             if let Some(exact_tld) = specific_tld.as_deref() {
                 let exact_registered = aggregated_results.iter().any(|item| {
@@ -252,34 +250,33 @@ async fn main() {
 
     let results = sort_aggregated_results(aggregated_results);
 
-    // save cache timestamp if we refreshed
+    // save cache timestamp if we showered
     if force_refresh && paths.can_save {
         config.mark_cache_updated();
         let _ = config.save(&paths.config_file);
     }
 
-    // print errors first
+    // print errors bruh
     output::print_errors(&results, args.verbose);
 
-    // CSV output
+    // cuntsexv output (csv)
     if let Some(csv_opt) = &args.csv {
         match csv_opt {
             Some(path) => {
-                // write to file
+                // you wont believe this but here we are GOING to WRITE TO A FILE WITH THE CSV OUTPUT!!!! MIND BLOWN AND COCK EXPLODED
                 match output::write_csv_file(&results, path) {
                     Ok(()) => eprintln!("CSV written to {}", path.display()),
                     Err(e) => eprintln!("Error writing CSV: {}", e),
                 }
             }
             None => {
-                // print to stdout, no logs
+                // brint to terminal if user dumb and no filepath 
                 output::print_csv(&results);
             }
         }
         return;
     }
 
-    // table output
     if args.show_all {
         output::print_full_table(&results, args.no_color, args.no_unicode);
     } else {
@@ -305,7 +302,7 @@ async fn run_autosearch(
 
     let base_tlds = build_base_tlds(args);
 
-    // collect all search entries, grouping by name so "zapplex.de" + "zapplex.nl" become one batch
+    // collect all search entries and grupe them
     let mut batches: Vec<(String, Vec<String>)> = Vec::new();
 
     for line in content.lines() {

+ 6 - 7
src/output.rs

@@ -7,14 +7,13 @@ pub fn print_available_table(results: &[DomainResult], no_color: bool, no_unicod
     let available: Vec<&DomainResult> = results.iter().filter(|r| r.is_available()).collect();
 
     if available.is_empty() {
-        println!("No available domains found.");
+        println!("No available domains.");
         return;
     }
 
     let max_len = available.iter().map(|r| r.full.len()).max().unwrap_or(20);
-    let width = max_len + 4; // padding
-
     let title = "Available Domains";
+    let width = max_len.max(title.len()) + 4; // padding, ensure title fits
     let title_padded = format!("{:^width$}", title, width = width);
 
     if no_unicode {
@@ -192,14 +191,14 @@ pub fn print_errors(results: &[DomainResult], verbose: bool) {
         if let DomainStatus::Error { kind, message } = &r.status {
             match kind {
                 ErrorKind::InvalidTld => {
-                    eprintln!("Error for {}, tld does not seem to exist", r.full);
+                    eprintln!("Error for {} : does not seem to exist", r.full);
                 }
                 _ => {
                     if verbose {
-                        eprintln!("Error for {}, {} (raw: {})", r.full, message, message);
+                        eprintln!("Error for {} : {} (raw: {})", r.full, message, message);
                     } else {
                         eprintln!(
-                            "Error for {}, unknown error (enable verbose to see raw error)",
+                            "Error for {} : unknown error",
                             r.full
                         );
                     }
@@ -222,6 +221,6 @@ pub fn print_progress(current: usize, total: usize) {
     if current == total {
         let secs = start.elapsed().as_secs_f64();
         eprintln!("\rParsing results : Done (Took {:.1}s)   ", secs);
-        *lock = None; // reset for next search duh
+        *lock = None; // reset for next search
     }
 }

+ 8 - 55
src/tlds.rs

@@ -13,7 +13,6 @@ impl WhoisOverrides {
     }
 }
 
-/// a named TLD list from Lists.toml
 struct NamedList {
     name: String,
     tlds: Vec<String>,
@@ -24,7 +23,7 @@ struct ParsedLists {
     whois_overrides: WhoisOverrides,
 }
 
-/// parse a single entry: "tld" or "tld:whois_server"
+// parse "tld" or "tld:whois_server"
 fn parse_entry(entry: &str) -> (String, Option<String>) {
     if let Some(pos) = entry.find(':') {
         (entry[..pos].to_string(), Some(entry[pos + 1..].to_string()))
@@ -33,7 +32,7 @@ fn parse_entry(entry: &str) -> (String, Option<String>) {
     }
 }
 
-/// parse entries, pull out TLD names and whois overrides
+// parse more 
 fn parse_list(entries: &[toml::Value], overrides: &mut HashMap<String, String>) -> Vec<String> {
     entries
         .iter()
@@ -57,7 +56,7 @@ fn parsed_lists() -> &'static ParsedLists {
 
         let table = raw.as_table().expect("Lists.toml must be a TOML table");
 
-        // Build list names in the order build.rs discovered them
+        // Build list names in the order build.rs found em
         let ordered_names: Vec<&str> = env!("HOARDOM_LIST_NAMES").split(',').collect();
 
         let mut overrides = HashMap::new();
@@ -80,7 +79,7 @@ fn parsed_lists() -> &'static ParsedLists {
     })
 }
 
-/// list names from Lists.toml, in order
+
 pub fn list_names() -> Vec<&'static str> {
     parsed_lists()
         .lists
@@ -89,12 +88,10 @@ pub fn list_names() -> Vec<&'static str> {
         .collect()
 }
 
-/// first list name (the default)
 pub fn default_list_name() -> &'static str {
     list_names().first().copied().unwrap_or("standard")
 }
 
-/// get TLDs for a list name (case insensitive), None if not found
 pub fn get_tlds(name: &str) -> Option<Vec<&'static str>> {
     let lower = name.to_lowercase();
     parsed_lists()
@@ -104,19 +101,19 @@ pub fn get_tlds(name: &str) -> Option<Vec<&'static str>> {
         .map(|l| l.tlds.iter().map(String::as_str).collect())
 }
 
-/// get TLDs for a list name, falls back to default if not found
+
 pub fn get_tlds_or_default(name: &str) -> Vec<&'static str> {
     get_tlds(name).unwrap_or_else(|| get_tlds(default_list_name()).unwrap_or_default())
 }
 
-/// the builtin whois overrides from Lists.toml
+
 pub fn whois_overrides() -> &'static WhoisOverrides {
     &parsed_lists().whois_overrides
 }
 
 pub fn apply_top_tlds(tlds: Vec<&'static str>, top: &[String]) -> Vec<&'static str> {
     let mut result: Vec<&'static str> = Vec::with_capacity(tlds.len());
-    // first add the top ones in the order specified
+    // add the top ones in the order specified
     for t in top {
         let lower = t.to_lowercase();
         if let Some(&found) = tlds.iter().find(|&&tld| tld.to_lowercase() == lower) {
@@ -125,7 +122,7 @@ pub fn apply_top_tlds(tlds: Vec<&'static str>, top: &[String]) -> Vec<&'static s
             }
         }
     }
-    // then add the rest
+    // then add the rest lol
     for tld in &tlds {
         if !result.contains(tld) {
             result.push(tld);
@@ -133,47 +130,3 @@ pub fn apply_top_tlds(tlds: Vec<&'static str>, top: &[String]) -> Vec<&'static s
     }
     result
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_entry_bare() {
-        let (tld, server) = parse_entry("com");
-        assert_eq!(tld, "com");
-        assert_eq!(server, None);
-    }
-
-    #[test]
-    fn test_parse_entry_with_override() {
-        let (tld, server) = parse_entry("io:whois.nic.io");
-        assert_eq!(tld, "io");
-        assert_eq!(server, Some("whois.nic.io".to_string()));
-    }
-
-    #[test]
-    fn test_whois_overrides_populated() {
-        let overrides = whois_overrides();
-        // io should have an override since our Lists.toml has "io:whois.nic.io"
-        assert!(overrides.get_server("io").is_some());
-        // com should not (it has RDAP)
-        assert!(overrides.get_server("com").is_none());
-    }
-
-    #[test]
-    fn test_top_tlds_reorder() {
-        let tlds = vec!["com", "net", "org", "ch", "de"];
-        let top = vec!["ch".to_string(), "de".to_string()];
-        let result = apply_top_tlds(tlds, &top);
-        assert_eq!(result, vec!["ch", "de", "com", "net", "org"]);
-    }
-
-    #[test]
-    fn test_top_tlds_missing_ignored() {
-        let tlds = vec!["com", "net"];
-        let top = vec!["swiss".to_string()];
-        let result = apply_top_tlds(tlds, &top);
-        assert_eq!(result, vec!["com", "net"]);
-    }
-}

+ 187 - 197
src/tui.rs

@@ -11,7 +11,7 @@ use ratatui::{
     layout::{Constraint, Direction, Layout, Rect},
     style::{Color, Modifier, Style},
     text::{Line, Span},
-    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
+    widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
     Frame, Terminal,
 };
 use std::io::{self, Write};
@@ -203,6 +203,8 @@ struct App {
     checking_favorites: bool,
     backups_enabled: bool,
     backup_count: u32,
+    no_color: bool,
+    no_unicode: bool,
 }
 
 #[derive(Debug, Clone, Default)]
@@ -299,9 +301,61 @@ impl App {
             checking_favorites: false,
             backups_enabled: config.settings.backups,
             backup_count: config.settings.backup_count,
+            no_color: args.no_color,
+            no_unicode: args.no_unicode,
         }
     }
 
+    /// style with fg color, or plain style if no_color is on
+    fn fg(&self, color: Color) -> Style {
+        if self.no_color {
+            Style::default()
+        } else {
+            Style::default().fg(color)
+        }
+    }
+
+    /// style with fg color + bold
+    fn fg_bold(&self, color: Color) -> Style {
+        if self.no_color {
+            Style::default().add_modifier(Modifier::BOLD)
+        } else {
+            Style::default().fg(color).add_modifier(Modifier::BOLD)
+        }
+    }
+
+    /// resolve a color: returns Color::Reset when no_color is on
+    /// use this when chaining .bg() or .fg() on existing styles
+    fn c(&self, color: Color) -> Color {
+        if self.no_color { Color::Reset } else { color }
+    }
+
+    // unicode symbol helpers
+    fn sym_check(&self) -> &'static str {
+        if self.no_unicode { "+" } else { "\u{2713}" }
+    }
+    fn sym_cross(&self) -> &'static str {
+        if self.no_unicode { "x" } else { "\u{2717}" }
+    }
+    fn sym_sep(&self) -> &'static str {
+        if self.no_unicode { "|" } else { "\u{2502}" }
+    }
+    fn sym_bar_filled(&self) -> &'static str {
+        if self.no_unicode { "#" } else { "\u{2588}" }
+    }
+    fn sym_bar_empty(&self) -> &'static str {
+        if self.no_unicode { "-" } else { "\u{2591}" }
+    }
+
+    /// preconfigured Block with ASCII or unicode borders
+    fn block(&self) -> Block<'static> {
+        let mut b = Block::default().borders(Borders::ALL);
+        if self.no_unicode {
+            b = b.border_type(BorderType::Plain);
+        }
+        b
+    }
+
     fn get_effective_tlds(&self) -> Vec<&'static str> {
         let mut tld_vec = self.base_tlds_for_selection();
 
@@ -1844,12 +1898,13 @@ fn start_search(app: &mut App) {
     app.stream_rx = Some(stream.receiver);
 }
 
+
 fn draw_ui(f: &mut Frame, app: &mut App) {
     let size = f.area();
 
     if terminal_too_small(size) {
         app.panel_rects = PanelRects::default();
-        draw_terminal_too_small(f, size);
+        draw_terminal_too_small(f, app, size);
         return;
     }
 
@@ -1978,7 +2033,7 @@ fn draw_ui(f: &mut Frame, app: &mut App) {
     app.panel_rects.search = Some(main_chunks[2]);
 
     // draw each panel
-    draw_topbar(f, main_chunks[0]);
+    draw_topbar(f, app, main_chunks[0]);
     if let Some(scratchpad_rect) = scratchpad_chunk {
         draw_scratchpad(f, app, scratchpad_rect);
     }
@@ -2005,10 +2060,9 @@ fn terminal_too_small(area: Rect) -> bool {
     area.width < MIN_UI_WIDTH || area.height < MIN_UI_HEIGHT
 }
 
-fn draw_terminal_too_small(f: &mut Frame, area: Rect) {
-    let block = Block::default()
-        .borders(Borders::ALL)
-        .border_style(Style::default().fg(Color::Red))
+fn draw_terminal_too_small(f: &mut Frame, app: &App, area: Rect) {
+    let block = app.block()
+        .border_style(app.fg(Color::Red))
         .title(" hoardom ");
 
     let inner = block.inner(area);
@@ -2018,54 +2072,46 @@ fn draw_terminal_too_small(f: &mut Frame, area: Rect) {
     let text = vec![
         Line::from(Span::styled(
             fit_cell_center("HELP ! HELP ! HELP !", content_width),
-            Style::default()
-                .fg(Color::White)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::White),
         )),
         Line::from(Span::styled(
             fit_cell_center("I AM BEING CRUSHED!", content_width),
-            Style::default()
-                .fg(Color::White)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::White),
         )),
         Line::from(fit_cell_center("", content_width)),
         Line::from(Span::styled(
             fit_cell_center("Im claustrophobic! :'(", content_width),
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             fit_cell_center(
                 &format!("Need {}x{} of space", MIN_UI_WIDTH, MIN_UI_HEIGHT),
                 content_width,
             ),
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             fit_cell_center(
                 &format!("Current: {}x{}", area.width, area.height),
                 content_width,
             ),
-            Style::default().fg(Color::DarkGray),
+            app.fg(Color::DarkGray),
         )),
         Line::from(fit_cell_center("", content_width)),
         Line::from(Span::styled(
             fit_cell_center("REFUSING TO WORK TILL YOU", content_width),
-            Style::default()
-                .fg(Color::White)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::White),
         )),
         Line::from(Span::styled(
             fit_cell_center("GIVE ME BACK MY SPACE! >:(", content_width),
-            Style::default()
-                .fg(Color::White)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::White),
         )),
     ];
 
     f.render_widget(Paragraph::new(text), inner);
 }
 
-fn draw_topbar(f: &mut Frame, area: Rect) {
+fn draw_topbar(f: &mut Frame, app: &App, area: Rect) {
     let title = format!("{} - {}", APP_NAME, APP_DESC);
     let width = area.width as usize;
     let left = format!("{} {}", CLOSE_BUTTON_LABEL, title);
@@ -2074,46 +2120,31 @@ fn draw_topbar(f: &mut Frame, area: Rect) {
     let paragraph = Paragraph::new(Line::from(vec![
         Span::styled(
             CLOSE_BUTTON_LABEL,
-            Style::default()
-                .fg(Color::Red)
-                .bg(Color::Gray)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::Red).bg(app.c(Color::Gray)),
         ),
         Span::styled(
             format!(" {}", title),
-            Style::default()
-                .fg(Color::White)
-                .bg(Color::Red)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::White).bg(app.c(Color::Red)),
         ),
         Span::styled(
             " ".repeat(gap),
-            Style::default().bg(Color::Red).add_modifier(Modifier::BOLD),
+            Style::default().bg(app.c(Color::Red)).add_modifier(Modifier::BOLD),
         ),
         Span::styled(
             EXPORT_BUTTON_LABEL,
-            Style::default()
-                .fg(Color::LightGreen)
-                .bg(Color::Red)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::LightGreen).bg(app.c(Color::Red)),
         ),
         Span::styled(
             " ",
-            Style::default().bg(Color::Red).add_modifier(Modifier::BOLD),
+            Style::default().bg(app.c(Color::Red)).add_modifier(Modifier::BOLD),
         ),
         Span::styled(
             HELP_BUTTON_LABEL,
-            Style::default()
-                .fg(Color::LightGreen)
-                .bg(Color::Red)
-                .add_modifier(Modifier::BOLD),
+            app.fg_bold(Color::LightGreen).bg(app.c(Color::Red)),
         ),
     ]))
     .style(
-        Style::default()
-            .fg(Color::White)
-            .bg(Color::Red)
-            .add_modifier(Modifier::BOLD),
+        app.fg_bold(Color::White).bg(app.c(Color::Red)),
     );
     f.render_widget(paragraph, area);
 }
@@ -2124,67 +2155,66 @@ fn draw_help_overlay(f: &mut Frame, app: &mut App, area: Rect) {
     app.panel_rects.help_popup = Some(popup);
 
     let text = vec![
-        Line::from(Span::styled(" ", Style::default().fg(Color::White))),
-        Line::from(Span::styled("Global :", Style::default().fg(Color::White))),
+        Line::from(Span::styled(" ", app.fg(Color::White))),
+        Line::from(Span::styled("Global :", app.fg(Color::White))),
         Line::from(Span::styled(
             "F1 or Help button   Toggle this help",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "F2 or Export button Open export popup",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Ctrl+C              Quit the app",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "s                   Stop/cancel running search",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Esc                 Clear selection or close help",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Tab or Shift+Tab    Move between panels",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Up and Down arrows  Navigate results",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
-        Line::from(Span::styled(" ", Style::default().fg(Color::White))),
+        Line::from(Span::styled(" ", app.fg(Color::White))),
         Line::from(Span::styled(
             "Mouse               Click Elements duh",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Scrolling           Scroll through elements (yea)",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
-        Line::from(Span::styled(" ", Style::default().fg(Color::White))),
+        Line::from(Span::styled(" ", app.fg(Color::White))),
         Line::from(Span::styled(
             "In Results :",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Enter               Add highlighted result to Favorites",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "In Favorites :",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
         Line::from(Span::styled(
             "Backspace or Delete Remove focused favorite",
-            Style::default().fg(Color::White),
+            app.fg(Color::White),
         )),
     ];
 
-    let block = Block::default()
-        .borders(Borders::ALL)
-        .border_style(Style::default().fg(Color::Red))
+    let block = app.block()
+        .border_style(app.fg(Color::Red))
         .title(" Help ");
 
     f.render_widget(Clear, popup);
@@ -2199,9 +2229,8 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
     let popup = export_popup_rect(area);
     app.panel_rects.export_popup = Some(popup);
 
-    let block = Block::default()
-        .borders(Borders::ALL)
-        .border_style(Style::default().fg(Color::Red))
+    let block = app.block()
+        .border_style(app.fg(Color::Red))
         .title(" Export ");
 
     let inner = block.inner(popup);
@@ -2220,8 +2249,9 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
         ])
         .split(inner);
 
+    let white_style = app.fg(Color::White);
     let mode_style = |mode: ExportMode| {
-        let mut style = Style::default().fg(Color::White);
+        let mut style = white_style;
         if popup_state.mode == mode {
             style = style.add_modifier(Modifier::REVERSED | Modifier::BOLD);
         } else if popup_state.selected_row == 0 {
@@ -2235,7 +2265,7 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
         chunks[0].width as usize,
     );
     f.render_widget(
-        Paragraph::new(subtitle).style(Style::default().fg(Color::DarkGray)),
+        Paragraph::new(subtitle).style(app.fg(Color::DarkGray)),
         chunks[0],
     );
 
@@ -2268,34 +2298,29 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
     ]);
     f.render_widget(Paragraph::new(mode_line), chunks[1]);
 
-    let path_block = Block::default()
-        .borders(Borders::ALL)
+    let path_block = app.block()
         .border_style(if popup_state.selected_row == 1 {
-            Style::default().fg(Color::Red)
+            app.fg(Color::Red)
         } else {
-            Style::default().fg(Color::DarkGray)
+            app.fg(Color::DarkGray)
         })
         .title(" Save to ");
     let path_inner = path_block.inner(chunks[2]);
     app.panel_rects.export_path = Some(chunks[2]);
     f.render_widget(path_block, chunks[2]);
     f.render_widget(
-        Paragraph::new(popup_state.path.as_str()).style(Style::default().fg(Color::White)),
+        Paragraph::new(popup_state.path.as_str()).style(app.fg(Color::White)),
         path_inner,
     );
 
     let status_style = if popup_state.status_success {
-        Style::default()
-            .fg(Color::Green)
-            .add_modifier(Modifier::BOLD)
+        app.fg_bold(Color::Green)
     } else if popup_state.confirm_overwrite {
-        Style::default()
-            .fg(Color::Yellow)
-            .add_modifier(Modifier::BOLD)
+        app.fg_bold(Color::Yellow)
     } else if popup_state.status.is_some() {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
     let status_text = popup_state.status.as_deref().unwrap_or(" ");
     f.render_widget(
@@ -2328,28 +2353,18 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
         Span::styled(
             cancel_label,
             if popup_state.selected_row == 2 {
-                Style::default()
-                    .fg(Color::Green)
-                    .bg(Color::DarkGray)
-                    .add_modifier(Modifier::BOLD)
+                app.fg_bold(Color::Green).bg(app.c(Color::DarkGray))
             } else {
-                Style::default()
-                    .fg(Color::Green)
-                    .add_modifier(Modifier::BOLD)
+                app.fg_bold(Color::Green)
             },
         ),
         Span::raw(button_gap),
         Span::styled(
             save_label,
             if popup_state.selected_row == 3 {
-                Style::default()
-                    .fg(Color::Green)
-                    .bg(Color::DarkGray)
-                    .add_modifier(Modifier::BOLD)
+                app.fg_bold(Color::Green).bg(app.c(Color::DarkGray))
             } else {
-                Style::default()
-                    .fg(Color::Green)
-                    .add_modifier(Modifier::BOLD)
+                app.fg_bold(Color::Green)
             },
         ),
     ]);
@@ -2366,9 +2381,9 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
 fn draw_results(f: &mut Frame, app: &mut App, area: Rect) {
     let focused = app.focus == Focus::Results;
     let border_style = if focused {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
 
     // show progress in title when searching
@@ -2395,8 +2410,7 @@ fn draw_results(f: &mut Frame, app: &mut App, area: Rect) {
         )
     };
 
-    let block = Block::default()
-        .borders(Borders::ALL)
+    let block = app.block()
         .border_style(border_style)
         .title(title);
 
@@ -2415,12 +2429,12 @@ fn draw_results(f: &mut Frame, app: &mut App, area: Rect) {
         let (cur, tot) = app.search_progress;
         let pct = (cur as f64 / tot as f64 * 100.0) as u16;
         let filled = (chunks[0].width as u32 * cur as u32 / tot as u32) as u16;
-        let bar: String = "\u{2588}".repeat(filled as usize)
-            + &"\u{2591}".repeat((chunks[0].width.saturating_sub(filled)) as usize);
+        let bar: String = app.sym_bar_filled().repeat(filled as usize)
+            + &app.sym_bar_empty().repeat((chunks[0].width.saturating_sub(filled)) as usize);
         let bar_text = format!(" {}% ", pct);
         let gauge_line = Line::from(vec![
-            Span::styled(bar, Style::default().fg(Color::Red)),
-            Span::styled(bar_text, Style::default().fg(Color::DarkGray)),
+            Span::styled(bar, app.fg(Color::Red)),
+            Span::styled(bar_text, app.fg(Color::DarkGray)),
         ]);
         f.render_widget(Paragraph::new(gauge_line), chunks[0]);
 
@@ -2437,6 +2451,7 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
     let show_note_column = app.show_unavailable;
     let selected_idx = app.results_state.selected();
     let selected_bg = Color::Black;
+    let sep = format!(" {} ", app.sym_sep());
 
     // collect visible results
     let visible_data: Vec<(String, String, String, DomainStatus)> = if app.show_unavailable {
@@ -2472,7 +2487,7 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
         } else {
             "No available domains found"
         };
-        let p = Paragraph::new(msg).style(Style::default().fg(Color::DarkGray));
+        let p = Paragraph::new(msg).style(app.fg(Color::DarkGray));
         f.render_widget(p, area);
         return;
     }
@@ -2519,35 +2534,27 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
         let mut header_spans = vec![
             Span::styled(
                 format!(" {}", fit_cell("Domain", domain_w)),
-                Style::default()
-                    .fg(Color::Gray)
-                    .add_modifier(Modifier::BOLD),
+                app.fg_bold(Color::Gray),
             ),
-            Span::styled(" │ ", Style::default().fg(Color::DarkGray)),
+            Span::styled(sep.clone(), app.fg(Color::DarkGray)),
             Span::styled(
                 fit_cell("Status", status_w),
-                Style::default()
-                    .fg(Color::Gray)
-                    .add_modifier(Modifier::BOLD),
+                app.fg_bold(Color::Gray),
             ),
         ];
 
         if show_note_column {
-            header_spans.push(Span::styled(" │ ", Style::default().fg(Color::DarkGray)));
+            header_spans.push(Span::styled(sep.clone(), app.fg(Color::DarkGray)));
             header_spans.push(Span::styled(
                 fit_cell("Details", note_w),
-                Style::default()
-                    .fg(Color::Gray)
-                    .add_modifier(Modifier::BOLD),
+                app.fg_bold(Color::Gray),
             ));
         }
 
-        header_spans.push(Span::styled(" │ ", Style::default().fg(Color::DarkGray)));
+        header_spans.push(Span::styled(sep.clone(), app.fg(Color::DarkGray)));
         header_spans.push(Span::styled(
-            " ✓ ",
-            Style::default()
-                .fg(Color::Gray)
-                .add_modifier(Modifier::BOLD),
+            format!(" {} ", app.sym_check()),
+            app.fg_bold(Color::Gray),
         ));
 
         f.render_widget(Paragraph::new(Line::from(header_spans)), header_area);
@@ -2561,20 +2568,20 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
             let selection_bg = if is_selected { Some(selected_bg) } else { None };
 
             let status_style = match status {
-                DomainStatus::Available => Style::default().fg(Color::Green),
-                DomainStatus::Registered { .. } => Style::default().fg(Color::Red),
+                DomainStatus::Available => app.fg(Color::Green),
+                DomainStatus::Registered { .. } => app.fg(Color::Red),
                 DomainStatus::Error { kind, .. } => match kind {
-                    ErrorKind::InvalidTld => Style::default().fg(Color::Yellow),
-                    _ => Style::default().fg(Color::Blue),
+                    ErrorKind::InvalidTld => app.fg(Color::Yellow),
+                    _ => app.fg(Color::Blue),
                 },
             };
 
             let domain_style = match status {
-                DomainStatus::Available => Style::default().fg(Color::Green),
-                DomainStatus::Registered { .. } => Style::default().fg(Color::Red),
+                DomainStatus::Available => app.fg(Color::Green),
+                DomainStatus::Registered { .. } => app.fg(Color::Red),
                 DomainStatus::Error { kind, .. } => match kind {
-                    ErrorKind::InvalidTld => Style::default().fg(Color::Yellow),
-                    _ => Style::default().fg(Color::Blue),
+                    ErrorKind::InvalidTld => app.fg(Color::Yellow),
+                    _ => app.fg(Color::Blue),
                 },
             };
 
@@ -2591,37 +2598,37 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
                     format!(" {}", fit_cell(full, domain_w)),
                     apply_bg(domain_style),
                 ),
-                Span::styled(" \u{2502} ", apply_bg(Style::default().fg(Color::Gray))),
+                Span::styled(sep.clone(), apply_bg(app.fg(Color::Gray))),
                 Span::styled(fit_cell(status_str, status_w), apply_bg(status_style)),
             ];
 
             if show_note_column {
                 spans.push(Span::styled(
-                    " \u{2502} ",
-                    apply_bg(Style::default().fg(Color::Gray)),
+                    sep.clone(),
+                    apply_bg(app.fg(Color::Gray)),
                 ));
                 spans.push(Span::styled(
                     fit_cell(note, note_w),
-                    apply_bg(Style::default().fg(Color::White)),
+                    apply_bg(app.fg(Color::White)),
                 ));
             }
 
             spans.push(Span::styled(
-                " \u{2502} ",
-                apply_bg(Style::default().fg(Color::Gray)),
+                sep.clone(),
+                apply_bg(app.fg(Color::Gray)),
             ));
             spans.push(match status {
                 DomainStatus::Available => {
-                    Span::styled(" ✓ ", apply_bg(Style::default().fg(Color::Green)))
+                    Span::styled(format!(" {} ", app.sym_check()), apply_bg(app.fg(Color::Green)))
                 }
                 DomainStatus::Registered { .. } => {
-                    Span::styled(" ✗ ", apply_bg(Style::default().fg(Color::Red)))
+                    Span::styled(format!(" {} ", app.sym_cross()), apply_bg(app.fg(Color::Red)))
                 }
                 DomainStatus::Error { kind, .. } => match kind {
                     ErrorKind::InvalidTld => {
-                        Span::styled(" ? ", apply_bg(Style::default().fg(Color::Yellow)))
+                        Span::styled(" ? ", apply_bg(app.fg(Color::Yellow)))
                     }
-                    _ => Span::styled(" ! ", apply_bg(Style::default().fg(Color::Blue))),
+                    _ => Span::styled(" ! ", apply_bg(app.fg(Color::Blue))),
                 },
             });
 
@@ -2778,13 +2785,12 @@ fn move_scratchpad_cursor_vertical(app: &mut App, line_delta: i16) {
 fn draw_scratchpad(f: &mut Frame, app: &mut App, area: Rect) {
     let focused = app.focus == Focus::Scratchpad;
     let border_style = if focused {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
 
-    let block = Block::default()
-        .borders(Borders::ALL)
+    let block = app.block()
         .border_style(border_style)
         .title(" Scratchpad ");
 
@@ -2797,7 +2803,7 @@ fn draw_scratchpad(f: &mut Frame, app: &mut App, area: Rect) {
     };
     f.render_widget(block, area);
     f.render_widget(
-        Paragraph::new(text).style(Style::default().fg(Color::White)),
+        Paragraph::new(text).style(app.fg(Color::White)),
         inner,
     );
 
@@ -2812,9 +2818,9 @@ fn draw_scratchpad(f: &mut Frame, app: &mut App, area: Rect) {
 fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
     let focused = app.focus == Focus::Favorites;
     let border_style = if focused {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
 
     let title = if app.checking_favorites {
@@ -2823,8 +2829,7 @@ fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
         " Favorites "
     };
 
-    let block = Block::default()
-        .borders(Borders::ALL)
+    let block = app.block()
         .border_style(border_style)
         .title(title);
 
@@ -2857,10 +2862,10 @@ fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
             };
             let mut spans = vec![Span::styled(
                 fav.domain.as_str(),
-                Style::default().fg(status_color),
+                app.fg(status_color),
             )];
             if fav.changed {
-                spans.push(Span::styled(" !", Style::default().fg(Color::Yellow)));
+                spans.push(Span::styled(" !", app.fg(Color::Yellow)));
             }
             ListItem::new(Line::from(spans))
         })
@@ -2878,9 +2883,9 @@ fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
         "[c]heck all"
     };
     let btn_style = if app.checking_favorites {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     } else {
-        Style::default().fg(Color::Green)
+        app.fg(Color::Green)
     };
     f.render_widget(
         Paragraph::new(Line::from(Span::styled(btn_label, btn_style)))
@@ -2892,13 +2897,12 @@ fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
 fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
     let focused = app.focus == Focus::Settings;
     let border_style = if focused {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
 
-    let block = Block::default()
-        .borders(Borders::ALL)
+    let block = app.block()
         .border_style(border_style)
         .title(" Settings ");
 
@@ -2910,9 +2914,9 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
     let selected = if focused { app.settings_selected } else { None };
     let checkbox_style = |row: usize, checked: bool| {
         let style = if checked {
-            Style::default().fg(Color::Green)
+            app.fg(Color::Green)
         } else {
-            Style::default().fg(Color::DarkGray)
+            app.fg(Color::DarkGray)
         };
 
         if selected == Some(row) {
@@ -2926,13 +2930,13 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
         if selected == Some(row) {
             Style::default().add_modifier(Modifier::REVERSED)
         } else {
-            Style::default().fg(Color::White)
+            app.fg(Color::White)
         }
     };
 
     let tld_row_style = if selected == Some(0) {
         Style::default()
-            .bg(Color::DarkGray)
+            .bg(app.c(Color::DarkGray))
             .add_modifier(Modifier::BOLD)
     } else {
         Style::default()
@@ -2940,7 +2944,7 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
 
     let jobs_row_style = if selected == Some(4) {
         Style::default()
-            .bg(Color::DarkGray)
+            .bg(app.c(Color::DarkGray))
             .add_modifier(Modifier::BOLD)
     } else {
         Style::default()
@@ -2949,10 +2953,10 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
     let text = vec![
         Line::from(vec![
             Span::raw(" "),
-            Span::styled("TLD List: [", tld_row_style.fg(Color::White)),
-            Span::styled(app.tld_list_name.as_str(), tld_row_style.fg(Color::Cyan)),
-            Span::styled("] ", tld_row_style.fg(Color::White)),
-            Span::styled("V", tld_row_style.fg(Color::Green)),
+            Span::styled("TLD List: [", tld_row_style.fg(app.c(Color::White))),
+            Span::styled(app.tld_list_name.as_str(), tld_row_style.fg(app.c(Color::Cyan))),
+            Span::styled("] ", tld_row_style.fg(app.c(Color::White))),
+            Span::styled("V", tld_row_style.fg(app.c(Color::Green))),
         ]),
         Line::from(vec![
             Span::raw(" "),
@@ -2971,10 +2975,10 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
         ]),
         Line::from(vec![
             Span::raw(" "),
-            Span::styled("Jobs: [", jobs_row_style.fg(Color::White)),
-            Span::styled(jobs_str, jobs_row_style.fg(Color::Cyan)),
-            Span::styled("] ", jobs_row_style.fg(Color::White)),
-            Span::styled("-/+", jobs_row_style.fg(Color::Green)),
+            Span::styled("Jobs: [", jobs_row_style.fg(app.c(Color::White))),
+            Span::styled(jobs_str, jobs_row_style.fg(app.c(Color::Cyan))),
+            Span::styled("] ", jobs_row_style.fg(app.c(Color::White))),
+            Span::styled("-/+", jobs_row_style.fg(app.c(Color::Green))),
         ]),
     ];
 
@@ -2985,9 +2989,9 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
 fn draw_search(f: &mut Frame, app: &mut App, area: Rect) {
     let focused = app.focus == Focus::Search;
     let border_style = if focused {
-        Style::default().fg(Color::Red)
+        app.fg(Color::Red)
     } else {
-        Style::default().fg(Color::DarkGray)
+        app.fg(Color::DarkGray)
     };
 
     let title = match &app.status_msg {
@@ -2995,8 +2999,7 @@ fn draw_search(f: &mut Frame, app: &mut App, area: Rect) {
         None => " Search (Enter to lookup) ".to_string(),
     };
 
-    let block = Block::default()
-        .borders(Borders::ALL)
+    let block = app.block()
         .border_style(border_style)
         .title(title);
 
@@ -3041,32 +3044,23 @@ fn draw_search(f: &mut Frame, app: &mut App, area: Rect) {
 
     let input_chunk = chunks[0];
     let visible_input = fit_cell(&app.search_input, input_chunk.width as usize);
-    let input = Paragraph::new(visible_input).style(Style::default().fg(Color::White));
+    let input = Paragraph::new(visible_input).style(app.fg(Color::White));
     f.render_widget(input, input_chunk);
 
     let search_enabled = !app.searching && !app.search_input.is_empty();
     let cancel_enabled = app.searching;
 
     let search_style = if search_enabled {
-        Style::default()
-            .fg(Color::Black)
-            .bg(Color::Green)
-            .add_modifier(Modifier::BOLD)
+        app.fg_bold(Color::Black).bg(app.c(Color::Green))
     } else {
-        Style::default().fg(Color::DarkGray).bg(Color::Black)
+        app.fg(Color::DarkGray).bg(app.c(Color::Black))
     };
     let stop_style = if cancel_enabled {
-        Style::default()
-            .fg(Color::Black)
-            .bg(Color::Yellow)
-            .add_modifier(Modifier::BOLD)
+        app.fg_bold(Color::Black).bg(app.c(Color::Yellow))
     } else {
-        Style::default().fg(Color::DarkGray).bg(Color::Black)
+        app.fg(Color::DarkGray).bg(app.c(Color::Black))
     };
-    let clear_style = Style::default()
-        .fg(Color::White)
-        .bg(Color::Red)
-        .add_modifier(Modifier::BOLD);
+    let clear_style = app.fg_bold(Color::White).bg(app.c(Color::Red));
 
     f.render_widget(
         Paragraph::new(SEARCH_BUTTON_LABEL).style(search_style),
@@ -3121,22 +3115,18 @@ fn draw_dropdown(f: &mut Frame, app: &mut App, settings_area: Rect, selected: us
         .map(|opt| {
             ListItem::new(Line::from(Span::styled(
                 format!(" {} ", opt),
-                Style::default().fg(Color::White),
+                app.fg(Color::White),
             )))
         })
         .collect();
 
-    let block = Block::default()
-        .borders(Borders::ALL)
-        .border_style(Style::default().fg(Color::Red))
+    let block = app.block()
+        .border_style(app.fg(Color::Red))
         .title(" TLD List ");
 
     f.render_widget(Clear, dropdown_full);
     let list = List::new(items).block(block).highlight_style(
-        Style::default()
-            .fg(Color::White)
-            .bg(Color::Red)
-            .add_modifier(Modifier::BOLD),
+        app.fg_bold(Color::White).bg(app.c(Color::Red)),
     );
     let mut state = ListState::default();
     state.select(Some(selected));