diff options
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | README.md | 210 | ||||
| -rw-r--r-- | build.rs | 10 | ||||
| -rw-r--r-- | dist/NotoSansMono-Regular.ttf | bin | 0 -> 512836 bytes | |||
| -rw-r--r-- | dist/NotoSansSymbols2-subset.ttf | bin | 0 -> 5860 bytes | |||
| -rw-r--r-- | doc/CLI.md | 61 | ||||
| -rw-r--r-- | doc/TUI.md | 2 | ||||
| -rwxr-xr-x | scripts/fetch-tlds.sh | 2 | ||||
| -rw-r--r-- | src/app.rs | 66 | ||||
| -rw-r--r-- | src/cli.rs | 58 | ||||
| -rw-r--r-- | src/config.rs | 57 | ||||
| -rw-r--r-- | src/lookup.rs | 505 | ||||
| -rw-r--r-- | src/main.rs | 41 | ||||
| -rw-r--r-- | src/output.rs | 21 | ||||
| -rw-r--r-- | src/tlds.rs | 4 | ||||
| -rw-r--r-- | src/tui.rs | 534 | ||||
| -rw-r--r-- | src/types.rs | 2 |
18 files changed, 1070 insertions, 512 deletions
@@ -1,6 +1,6 @@ [package] name = "hoardom" -version = "1.1.3" +version = "1.1.8" edition = "2021" description = "Domain hoarding made less painful" default-run = "hoardom" @@ -77,13 +77,16 @@ endif # ---- debian .deb package ---- -deb: release +deb: app @echo "building deb package v$(VERSION)" rm -rf $(DEB_ROOT) - # binary + # binaries install -d $(DEB_ROOT)/usr/bin install -m 755 $(BINARY) $(DEB_ROOT)/usr/bin/$(NAME) + @if [ -f $(GUI_BINARY) ]; then \ + install -m 755 $(GUI_BINARY) $(DEB_ROOT)/usr/bin/$(NAME)-app; \ + fi # desktop file + icon install -d $(DEB_ROOT)/usr/share/applications @@ -4,13 +4,13 @@ Allows you to HOARd DOMains but with alot less pain associated with it. > "How to get my IP unbanned by Whois servers" <br> > -- Probably you after using this tool. -## Lates Update : tool now has a wrapper app for desktop* folks! +## Latest Update : tool now has a wrapper app for desktop* folks! (No windows support only unix.) Use the new make file to either `make pkg` for macass or `make deb` for debian systems. If you are running gentoo or anything else just the usual `make install`, to uninstall do `make uninstall` obv. (both install and uninstall will require le sudo or root) <div style="width:80%; margin: auto;"> - + </div> @@ -23,151 +23,157 @@ In short the threeish interfaces serve the following purposes : - TUI : Allows you to hunt and keep track of domain (name) ideas and quickly search up what is available and what isnt right in your unix terminal of choice - GUI : Is a wrapper for the TUI that gives it its own icon in your app menu and when opened in dock/taskbar incase you wish to not use it that way. -## How do I install `hoardom`? -`hoardom`'s tui and especially its gui interface officially only supports Unix based systems, Windows was net tested! <br> The regular cli interface should work with the built in main lookup and fallback whois implementations on Windows but that hasn't been tested. +## planed features +that will probably not come because I have no time. +- search within results -### Debian - -```bash -# Install RustUp Toolchain (Optional if already installed) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - -# clone this repo -https://git.teleco.ch/crt/hoardom.git/about/?h=master - -``` - -> <br> - -> **_Question:_** Whats the like main use for this tool?<br><hr> -> -**_Answer:_** To allow looking up whether or not domain names are available or already registered without scrolling for ages through a long and slow web ui by your domain registrar of choice or needing to go through like 3 sites to cover all domain registrars you might use. <br> -> - -> <br> - -> **_Question:_** I dont need GUI all I want is a simple command?<br><hr> - -> **_Answer:_** Ofcourse! That was the initial idea, simply type : <br> -> --> `hoardom somedomain.com` to see if a specific domain is free <br> -> --> `hoardom somedomain` to check which top 100ish tlds are free <br> -> --> `hoardom -l all -a somedomain` to check all 600ish registerable domain names and explicitly show which ones are already taken <br> <br> - -> **_Note:_** If you need stuff like csv outputs or other silly features check [the CLi docs](doc/CLI.md) <br> +## Further screenshots +Have some screenshots I took as to know what will expect you and mainly for me to flex a bit. +### Its fast : +Searching the default domain list returns results of the top 100ish tlds in just **_1-3 seconds!_** +<div style="width:80%; margin: auto;"> -Dont believe me on this being the best? Fair, but what if one of my test subjects* described to you how it felt to use this tool : + -> THIS IS THE BEST domain hoarding tool ever!!! Ive gone completely mental and now have loads and loads of debt because I bought a bunch of worthless domains I will do nothing with! +</div> -*(the subject was me and voices i got in my head making this) +*(list of top 100ish tlds is a bit biased by my preference obviously) +<hr> -Dont wanna use its TUI? Fine it can also run as a normal CLI tool if you just wanna search some stuff real quick and move on with your sad chud life before going insane and launching it in TUI mode and start HOARDING you favorites, scribble your schizophrenia into its scratchpad and lose all your money to domains you wont ever use <3 +Searching availability for all domains I was able to compiled which are purchasable through either : Porkbun, INWX or OVH (~637 Domains) will take only **_10-20 Seconds!_** -## what this thing does +<div style="width:80%; margin: auto;"> -- searches suffixes or full domains across TLD lists -- has a TUI mode with: - - results - - favorites - - settings - - scratchpad - - exports -- has a CLI mode for quick searches, scripts or sigma terminal maxxers. -- lets you import custom TLD lists from toml files because I wont bother keeping a list of "technically obtainable ones" up to date (Check and edit List.toml before compiling if the embedded ones I prepared dont satisfy you) -- keeps a `.hoardom` config folder with your saved stuff - - favorites - - imported lists - - em notes - - settings - - cach and stuff idk + -## known bugs -- missing basic ah tui features -- half of the app untested its 6am i havent slept leave me alone -- scrolling then selecting something sometimes takes a while until the button hitboxes catch up with scrolling (if using mouse or touchpad that is) -- will display fallse positives for some domains that have a minimum length that this tool doesnt know about (usually 4/5 lette domains where ur part is only 2) +</div> -## planed features -that will probably not come because I have no time. -- search within results +Btw theres even tracking of you your favorites! Showing a persistant " ! " for domains that changed from unavailable to available (and reverse). To make it go away you'll need highlighting it and press enter (its meant so that no change goes unnoticed) -## more screenshots +### Export popup -### speedy as fuck ? heck yea !!! +you can export stuff from tui wow!, tbh I dont know what else to show its pretty cool, check yourself tbh. It obviously can do full mouse support to and scrollwheel support is also there if you want -How long does it takes to search all domains purchasable through either : Porkbun, INWX or OVH (~637 Domains)? 15ish seconds on wifi at my home most time coming from retries at some uglier tlds - + -Oh and ofcourse you can save favorites and check their status here and there, if one changes it shows an ! next to it until you confirm it with enter. +## How do I install `hoardom`? +`hoardom`'s tui and especially its gui interface officially only supports Unix based systems, Windows was net tested! <br> The regular cli interface should work with the built in main lookup and fallback whois implementations on Windows but that hasn't been tested. -### export popup +### Debian +Not tested yet tbh. Probably wont build the GUI wrapper but should still add a desktop entry shortcut for opening it in terminal -you can export stuff from tui wow! +```bash +# Install RustUp Toolchain (Optional if already installed) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - +# clone and enter this repo +git clone https://git.teleco.ch/crt/hoardom.git && cd hoardom -### cli example +# make deb and install +make deb +sudo dpkg -i target/deb/hoardom*.deb +``` -Have a banger name for a website but dont know what domains for that name are free ? just type hoardom <domainsuffix> and it gives you the answer in two seconds! - +### Other Linuxes +Not tested yet tbh. Same as above. -## quick start +```bash +# Install RustUp Toolchain (Optional if already installed) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -if you just wanna run it from source real quick: +# clone and enter this repo +git clone https://git.teleco.ch/crt/hoardom.git && cd hoardom -```bash -cargo run -- --tui +# build and install +make +sudo make install ``` -some normal CLI examples: +### MacDonalds OS +Also known as MacOS ```bash -cargo run -- pissnelke -cargo run -- drowogen.network -cargo run -- --list Country hoardom -``` +# Install RustUp Toolchain (Optional if already installed) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -import a custom list and jump into the TUI: +# clone and enter this repo +git clone https://git.teleco.ch/crt/hoardom.git && cd hoardom -```bash -cargo run -- --import-filter ./doc/example-list.toml --tui +# make pkg installation file. +make pkg ``` +After that go to the target/release folder and install the PKG file. +### Uninstall +``` +sudo make uninstall +``` -## docs - -the proper usage docs live here cause i didnt want this readme to turn into a kilometer ah receipt paper +### Minimal Q&A on getting started +And potentially fun facts. -- [CLI usage](doc/CLI.md) -- [TUI usage](doc/TUI.md) +> <br> - +> **_Question:_** How do I open the TUI?<br><hr> - +> **_Answer:_** With : `hoardom` <br> -so yeah if you want the actual button for button docs go there, but expect part of it to be llm slop as i am not writing a manual describing each button or feature for what was supposed to be a 2 hour simple cli tool that turned into a 1.5 day project because i have too much spice. +> <br> - +> **_Question:_** I dont need GUI all I want is a simple command?<br><hr> - +> **_Answer:_** Ofcourse! That was the initial idea, simply type : <br> +> --> `hoardom somedomain.com` to see if a specific domain is free <br> +> --> `hoardom somedomain` to check which top 100ish tlds are free <br> +> --> `hoardom -l all -a somedomain` to check all 600ish registerable domain names and explicitly show which ones are already taken <br> <br> - +> **_Note:_** If you need stuff like csv outputs or other silly features check [the CLi docs](doc/CLI.md) <br> -## custom lists +#### Custom lists +As this is mentioned below as a fix to a bigger problem ive added this here (also partially because i forgot to rename import filter to import list, earlier typo that survived for too long) -there is an importable example file here: +Tldr you can make custom search list see example : [`doc/example-list.toml`](doc/example-list.toml) -- [`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` -custom lists are just simple toml files with a name and a `tlds` array. -example shape: +## Docs -```toml -name = "somecoollist" -tlds = ["com", "io", "dev", "sh"] -``` +the proper usage docs live here cause i didnt want this readme to become like 1km long (its already too long anyways) -then import it with: +- [CLI usage](doc/CLI.md) +- [TUI usage](doc/TUI.md) -```bash -cargo run -- --import-filter ./path/to/list.toml --tui -``` -which ads it to your config.toml environment filet -yes i know its supposed to be named import-list ... i forgot to change it but im not fixing that now, is from when i used a different list fetching method. +## Known Problems +Just so you know + how to get arround some of the bigger ones. + +- **_BIG ISSUES_** + - + - *Problem* : Not fully ready for microdomains, If searched for SLD is only 1-3 chars some false positives from TLDs that dont allow such short SLDs are possible if that domain isnt registered. + - Temp Fix : Make custom list that excludes the missbehaving + - *Problem* : No support for special characters like ö ä ü + - Temp Fix : Type prefix `xn--` and the punycode for ur special character by hand or use a converter. +- **_Meh Issues_** + - + - Problem : No real like settings pop up panel in TUI to control rest of configuration options in TUI + - Edit your settings manually, by default in ~/.hoardam/config.toml +- **_Tiny Issues_** + - scrolling then selecting something by mouse might select wrong thing as hitboxes for selection cant updated fast enough due to spasming touchpad scroll events. + - Temp Fix : Be Patient and give it like 1-5 seconds to catch up and dont scroll as fast + +## How much Slop can I expect in this repo? +Usually comments were left for things that had unusally high amounts of things not done by me. If code has like ... very minimal or barely any comments its probably my own spaghetti code. If its got a bunch of em that look overly gramatically correct it was done by an llm, heres infos as i believe projects should state their usage of ai so I can avoid pure slop projects on GH that only work halfways : + +- General Infos + - Basically all used agentic LLMs were ran on my own hardware at home. + - Extensive systemprompt was writen to fix some basic typical garbage behaviour by agents + - This project shouldve never taken more than a few hours... However i spiraled and went nuts making it too capable because I got a rush of hyperfocus for it for while +- Where and for what were they used : + - Bugfixing, Internet Research and Repetitive work : + - Project is a bit of a mess as I lacked time to neatly split up repetitive functions into modules and didnt trust ai doing it. Bugfixing occassionally was guided by models capable of browsing the internet via mcp as googles search results are becoming worse and worse by day and i cant find crap anymore thanks to all the sponsored and ai slop that google itself already shoves in my face. + - Documentation and Comments : + - All docs and code commends usually get looked over by Agentic LLMs and incase things were in German/Swiss-German they were translated to english and if they contained swearing it shouldve been removed. Places that have unusally high ammounts of comments might be bugfixes done by ai, didnt really tell it to make comments for me just sanetize mine. -## did i use ai for coding this ? -for helping me fix bugs yea obv. other than that only some markdown structures and basic crap i didnt wanna do or as guidance on how to go about stuff was done by llms and comments to the code were sanetized from very harsh swearing by an llm lol. @@ -41,7 +41,10 @@ fn main() { println!("cargo:rustc-env=HOARDOM_WHOIS_CMD={}", whois_cmd); println!("cargo:rustc-env=HOARDOM_WHOIS_FLAGS={}", whois_flags); - println!("cargo:rustc-env=HOARDOM_RDAP_BOOTSTRAP_URL={}", rdap_bootstrap_url); + println!( + "cargo:rustc-env=HOARDOM_RDAP_BOOTSTRAP_URL={}", + rdap_bootstrap_url + ); // Extract list names from Lists.toml (keys that have array values) let lists_toml = std::fs::read_to_string("Lists.toml").expect("Could not read Lists.toml"); @@ -56,7 +59,10 @@ fn main() { } } } - println!("cargo:rustc-env=HOARDOM_LIST_NAMES={}", list_names.join(",")); + println!( + "cargo:rustc-env=HOARDOM_LIST_NAMES={}", + list_names.join(",") + ); // rerun if Cargo.toml or Lists.toml changes println!("cargo:rerun-if-changed=Cargo.toml"); diff --git a/dist/NotoSansMono-Regular.ttf b/dist/NotoSansMono-Regular.ttf Binary files differnew file mode 100644 index 0000000..541efd8 --- /dev/null +++ b/dist/NotoSansMono-Regular.ttf diff --git a/dist/NotoSansSymbols2-subset.ttf b/dist/NotoSansSymbols2-subset.ttf Binary files differnew file mode 100644 index 0000000..d0978d4 --- /dev/null +++ b/dist/NotoSansSymbols2-subset.ttf @@ -1,8 +1,7 @@ # CLI Usage hoardom can run as a normal CLI tool for quick domain lookups. - -written before i went nuts making the tui tool +this file was partually made and kept up to date by agents as I suck at writing docs as one might notice from tui.md lmao. ## Basic Usage @@ -19,44 +18,44 @@ hoardom coolproject mysite bigidea ## Modes -| Flag | Description | -|---------|----------------------------------------------------| -| `--cli` | Default non-interactive mode (implied when no flag) | -| `--tui` | Launch the Terminal UI | +| Flag | Description | +|---------|---------------------------------------------------| +| `--cli` | Default non-interactive mode (kinda useless tbh ) | +| `--tui` | Launch the Terminal UI | ## Basics -| Flag | Description | -|-----------------------------|-------------------------------------------------------------------| -| `-e, --environement=PATH` | Where to store the `.hoardom` config folder | -| `-a, --all` | Show all results including unavailable domains | -| `-h, --help` | Basic help | -| `-H, --fullhelp` | Full help with all flags | +| Flag | Description | +|---------------------------|------------------------------------------------| +| `-e, --environement=PATH` | Where to store the `.hoardom` config folder | +| `-a, --all` | Show all results including unavailable domains | +| `-h, --help` | Basic help | +| `-H, --fullhelp` | Full help with all flags | ## Advanced -| Flag | Description | -|-----------------------------|-------------------------------------------------------------------| -| `-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 | -| `-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) | +| Flag | Description | +|----------------------------|------------------------------------------------------------------| +| `-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 | +| `-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) | ## Various -| Flag | Description | -|-----------------------------|-------------------------------------------------------------------| -| `-j, --jobs=NUMBER` | Number of concurrent lookup requests (default: 32). Controls how many TLDs are looked up at the same time. Higher values speed up searches but may trigger rate limiting from RDAP/WHOIS servers. Max 99. | -| `-D, --delay=SECONDS` | Delay in seconds between lookup requests | -| `-R, --retry=NUMBER` | Retry count on lookup errors (default: 1) | -| `-V, --verbose` | Verbose output for debugging | -| `-A, --autosearch=FILE` | Search domains from a text file (one per line) | -| `-C, --no-color` | Monochrome output | -| `-U, --no-unicode` | ASCII-only output (no unicode box drawing) | -| `-M, --no-mouse` | Disable mouse integration in TUI | -| `-r, --refresh-cache` | Force refresh the RDAP bootstrap cache | +| Flag | Description | +|-------------------------|-------------------------------------------------------------| +| `-j, --jobs=NUMBER` | Number of concurrent lookup requests (default: 32) Max 99. | +| `-D, --delay=SECONDS` | Delay in seconds between lookup requests | +| `-R, --retry=NUMBER` | Retry count on lookup errors (default: 1) | +| `-V, --verbose` | Verbose output for debugging | +| `-A, --autosearch=FILE` | Search domains from a text file (one per line) | +| `-C, --no-color` | Monochrome output | +| `-U, --no-unicode` | ASCII-only output (no unicode box drawing) | +| `-M, --no-mouse` | Disable mouse integration in TUI | +| `-r, --refresh-cache` | Force refresh the RDAP bootstrap cache | ## Examples @@ -28,7 +28,7 @@ the TUI has a few panels: - **settings** below favorites: toggle stuff duh - **scratchpad** on the left (if enabled in toggler settings): just a little text area for gathering inspiration and other stuff like amongus memes or the bee movie script (saves to config.toml btw so u dont loose your mommy asmr converted to base64 that you saved in ur notes) -and since version 2.0.1 theres also a top bar with an export button and help button. +and since version 1.0.1 theres also a top bar with an export button and help button. ## searching diff --git a/scripts/fetch-tlds.sh b/scripts/fetch-tlds.sh index 0892f42..f94450a 100755 --- a/scripts/fetch-tlds.sh +++ b/scripts/fetch-tlds.sh @@ -12,7 +12,7 @@ # ./scripts/fetch-tlds.sh --template # generate full Lists.toml with whois overrides if necessary # # Notes : yea this is ai slop, didnt make it myself oooo scary, but most of the rust i did myself just didnt feel like doing this at 4am and it somewhat works - +# Correction : The initial porkbun fetching was mostly me but porkbun lacked many domains so yea set -euo pipefail @@ -1,6 +1,6 @@ -// hoardom-app: native e gui emo wrapper for the hoardom tui +// e gui emo wrapper for the hoardom tui // spawns hoardom --tui in a pty and renders it in its own window -// so it shows up with its own icon in the dock (mac) or taskbar (linux) +// so it shows up with its own icon in the dock (mac) or taskbar (linux) for people that want that // // built with: cargo build --features gui @@ -15,7 +15,10 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -// ----- constants ----- +// thx gemma for the formated section comments i didnt want... thats not what I wanted when i said sanetize my comments from swearing and german language. +// i mean it is prettier than my usual way, will check otherfiles to see if can atleast make it somewhat consistant. + +// ---- constants ---- const FONT_SIZE: f32 = 14.0; const DEFAULT_COLS: u16 = 120; @@ -33,6 +36,8 @@ enum TermColor { Rgb(u8, u8, u8), } + +// ai made this fn because i gave up on trying. fn ansi_color(idx: u8) -> Color32 { match idx { 0 => Color32::from_rgb(0, 0, 0), @@ -58,7 +63,11 @@ fn ansi_color(idx: u8) -> Color32 { let gi = (idx % 36) / 6; let bi = idx % 6; let v = |i: u16| -> u8 { - if i == 0 { 0 } else { 55 + i as u8 * 40 } + if i == 0 { + 0 + } else { + 55 + i as u8 * 40 + } }; Color32::from_rgb(v(ri), v(gi), v(bi)) } @@ -73,7 +82,11 @@ fn ansi_color(idx: u8) -> Color32 { fn resolve_color(c: TermColor, is_fg: bool) -> Color32 { match c { TermColor::Default => { - if is_fg { DEFAULT_FG } else { DEFAULT_BG } + if is_fg { + DEFAULT_FG + } else { + DEFAULT_BG + } } TermColor::Indexed(i) => ansi_color(i), TermColor::Rgb(r, g, b) => Color32::from_rgb(r, g, b), @@ -135,6 +148,8 @@ impl Cell { // ----- terminal grid ----- + +// contains quiet a few ai solved bugfixes they seem ... fineish... to me and work struct TermGrid { cells: Vec<Vec<Cell>>, rows: usize, @@ -158,12 +173,13 @@ struct TermGrid { alt_saved: Option<(Vec<Vec<Cell>>, usize, usize)>, // mouse tracking modes - mouse_normal: bool, // ?1000 - normal tracking (clicks) - mouse_button: bool, // ?1002 - button-event tracking (drag) - mouse_any: bool, // ?1003 - any-event tracking (all motion) - mouse_sgr: bool, // ?1006 - SGR extended coordinates + mouse_normal: bool, // ?1000 - normal tracking (clicks) + mouse_button: bool, // ?1002 - button-event tracking (drag) + mouse_any: bool, // ?1003 - any-event tracking (all motion) + mouse_sgr: bool, // ?1006 - SGR extended coordinates } +// i partially stole this from somewhere i forgot from where though. impl TermGrid { fn new(rows: usize, cols: usize) -> Self { TermGrid { @@ -397,7 +413,7 @@ impl TermGrid { } } - // SGR - set graphics rendition (colors and attributes) + // slopfix : SGR , set graphics rendition (colors and attributes) fn sgr(&mut self, params: &[u16]) { if params.is_empty() { self.reset_attrs(); @@ -637,6 +653,7 @@ impl Perform for TermGrid { // ----- keyboard input mapping ----- // map egui keys to terminal escape sequences +// yk because maybe i want to recycle this file for some other project in the future hence why i implemented it fn special_key_bytes(key: &egui::Key, modifiers: &egui::Modifiers) -> Option<Vec<u8>> { use egui::Key; match key { @@ -721,7 +738,7 @@ struct HoardomApp { cell_height: f32, current_cols: u16, current_rows: u16, - last_mouse_button: Option<u8>, // track held mouse button for drag/release + last_mouse_button: Option<u8>, // track held mouse button for drag/release } impl eframe::App for HoardomApp { @@ -1155,12 +1172,37 @@ fn main() -> eframe::Result<()> { cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // font fallback chain for monospace: Hack (default) -> NotoSansMono -> NotoSansSymbols2 + // Hack is missing box drawing, block elements, ellipsis + // NotoSansMono covers those but is missing dingbats (symbols) + // NotoSansSymbols2-subset has just those two glyphs from which only the necessary ones are extracted afaik + let mut fonts = egui::FontDefinitions::default(); + fonts.font_data.insert( + "NotoSansMono".to_owned(), + std::sync::Arc::new(egui::FontData::from_static(include_bytes!( + "../dist/NotoSansMono-Regular.ttf" + ))), + ); + fonts.font_data.insert( + "NotoSansSymbols2".to_owned(), + std::sync::Arc::new(egui::FontData::from_static(include_bytes!( + "../dist/NotoSansSymbols2-subset.ttf" + ))), + ); + let mono = fonts + .families + .entry(egui::FontFamily::Monospace) + .or_default(); + mono.push("NotoSansMono".to_owned()); + mono.push("NotoSansSymbols2".to_owned()); + cc.egui_ctx.set_fonts(fonts); + Ok(Box::new(HoardomApp { grid, pty_writer: Mutex::new(writer), pty_master: pair.master, child_exited, - cell_width: 0.0, // measured on first frame + cell_width: 0.0, // measured on first frame cell_height: 0.0, current_cols: DEFAULT_COLS, current_rows: DEFAULT_ROWS, @@ -2,98 +2,60 @@ use clap::Parser; use std::path::PathBuf; #[derive(Parser, Debug)] -#[command(name = "hoardom", version = "0.0.1", about = "Domain hoarding made less painful")] +#[command( + name = "hoardom", + version = "0.0.1", + about = "Domain hoarding made less painful" +)] // static version infos ???? whoops #[command(disable_help_flag = true, disable_version_flag = true)] pub struct Args { - /// One or more domain names to search for + /// ffs why were a million comments added at some point !?!? basically all of these are self explanatory. removed dumb ass redundant ones. + #[arg(value_name = "DOMAIN")] pub domains: Vec<String>, - - // -- Mode -- - /// Default non interactive mode #[arg(long = "cli", default_value_t = false)] pub cli_mode: bool, - - /// Easy to use Terminal based Graphical user interface #[arg(long = "tui", default_value_t = false)] pub tui_mode: bool, - - // -- Basics -- - /// Define where environement file should be saved #[arg(short = 'e', long = "environement")] pub env_path: Option<PathBuf>, - - /// Show all in list even when unavailable #[arg(short = 'a', long = "all", default_value_t = false)] pub show_all: bool, - - // -- Advanced -- - /// Out in CSV, Path is optional. If path isnt given will be printed to terminal with no logs #[arg(short = 'c', long = "csv")] pub csv: Option<Option<PathBuf>>, - - /// Built in TLD list to use (from Lists.toml) #[arg(short = 'l', long = "list")] pub tld_list: Option<String>, - - /// Import a custom toml list for this session #[arg(short = 'i', long = "import-filter")] pub import_filter: Option<PathBuf>, - - /// Set certain TLDs to show up as first result (comma separated) #[arg(short = 't', long = "top", value_delimiter = ',')] pub top_tlds: Option<Vec<String>>, - - /// Only search these TLDs (comma separated) #[arg(short = 'o', long = "onlytop", value_delimiter = ',')] pub only_top: Option<Vec<String>>, - - /// How many suggestions to look up and try to show (defaults to 0 aka disabled) #[arg(short = 's', long = "suggestions")] pub suggestions: Option<usize>, - - // -- Various -- - /// Number of concurrent lookup requests (default: 1) #[arg(short = 'j', long = "jobs")] pub jobs: Option<u8>, - - /// Set the global delay in seconds between lookup requests #[arg(short = 'D', long = "delay")] pub delay: Option<f64>, - - /// Retry NUMBER amount of times if domain lookup errors out #[arg(short = 'R', long = "retry")] pub retry: Option<u32>, - - /// Verbose output for debugging #[arg(short = 'V', long = "verbose", default_value_t = false)] pub verbose: bool, - - /// Search for names/domains in text file, one domain per new line + /// 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 + /// Use a monochrome color scheme TODO: not applied in TUI since colors were changed from RGB to Ratatui colors. should fix #[arg(short = 'C', long = "no-color", default_value_t = false)] pub no_color: bool, - - /// Do not use unicode only plain ASCII + /// Do not use unicode only plain ASCII TODO: not applied in TUI for some reason idk #[arg(short = 'U', long = "no-unicode", default_value_t = false)] pub no_unicode: bool, - - /// Disable the mouse integration for TUI #[arg(short = 'M', long = "no-mouse", default_value_t = false)] pub no_mouse: bool, - - /// Force refresh the RDAP bootstrap cache #[arg(short = 'r', long = "refresh-cache", default_value_t = false)] pub refresh_cache: bool, - - /// Basic Help #[arg(short = 'h', long = "help", default_value_t = false)] pub help: bool, - - /// Show full help #[arg(short = 'H', long = "fullhelp", default_value_t = false)] pub fullhelp: bool, } diff --git a/src/config.rs b/src/config.rs index 1e2efeb..546dd16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,10 +36,10 @@ pub struct FavoriteEntry { /// last known status: "available", "registered", "error", or "unknown" #[serde(default = "default_fav_status")] pub status: String, - /// when it was last checked (RFC 3339) + /// when it was last checked #[serde(default)] pub checked: String, - /// true when status changed since last check (shows ! in TUI) + /// true when status changed since last check #[serde(default)] pub changed: bool, } @@ -128,7 +128,11 @@ fn default_jobs() -> u8 { } fn default_noretry() -> Vec<String> { - vec!["rate_limit".to_string(), "invalid_tld".to_string(), "forbidden".to_string()] + vec![ + "rate_limit".to_string(), + "invalid_tld".to_string(), + "forbidden".to_string(), + ] } fn default_backups_enabled() -> bool { @@ -208,7 +212,11 @@ impl Config { return Config { settings: legacy.settings, cache: legacy.cache, - favorites: legacy.favorites.into_iter().map(FavoriteEntry::new).collect(), + favorites: legacy + .favorites + .into_iter() + .map(FavoriteEntry::new) + .collect(), imported_filters: legacy.imported_filters, scratchpad: legacy.scratchpad, }; @@ -240,37 +248,35 @@ impl Config { let body = toml::to_string_pretty(self) .map_err(|e| format!("Failed to serialize config: {}", e))?; - let content = format!("\ + let content = format!( + "\ # hoardom config - auto saved, comments are preserved on the line theyre on # # [settings] -# noretry: error types that shouldnt be retried -# \u{201c}rate_limit\u{201d} - server said slow down, retrying immediately wont help -# \u{201c}invalid_tld\u{201d} - TLD is genuinely broken, no point retrying -# \u{201c}forbidden\u{201d} - server returned 403, access denied, retrying wont fix it -# \u{201c}timeout\u{201d} - uncomment if youd rather skip slow TLDs than wait -# \u{201c}unknown\u{201d} - uncomment to skip any unrecognized errors too -\n{}", body); - std::fs::write(path, content) - .map_err(|e| format!("Failed to write config file: {}", e))?; +# noretry: error types that shouldnt be retried has the following valid values btw: +# \u{201c}rate_limit\u{201d} +# \u{201c}invalid_tld\u{201d} +# \u{201c}forbidden\u{201d} +# \u{201c}timeout\u{201d} +# \u{201c}unknown\u{201d} +\n{}", + body + ); + std::fs::write(path, content).map_err(|e| format!("Failed to write config file: {}", e))?; Ok(()) } - /// copy current config into backups/ folder. - /// keeps at most `max_count` backups, tosses the oldest. - /// only call on startup and shutdown - NOT on every save. + /// only call on startup and shutdown NOT on every save bruh pub fn create_backup(config_path: &Path, max_count: u32) -> Result<(), String> { let parent = config_path.parent().ok_or("No parent directory")?; let backup_dir = parent.join("backups"); std::fs::create_dir_all(&backup_dir) .map_err(|e| format!("Failed to create backup dir: {}", e))?; - // Timestamp-based filename: config_20260308_143022.toml let ts = chrono::Local::now().format("%Y%m%d_%H%M%S"); let backup_name = format!("config_{}.toml", ts); let backup_path = backup_dir.join(&backup_name); - // dont backup if same-second backup already exists if backup_path.exists() { return Ok(()); } @@ -278,7 +284,6 @@ impl Config { std::fs::copy(config_path, &backup_path) .map_err(|e| format!("Failed to copy config to backup: {}", e))?; - // prune old backups: sort by name (timestamp order), keep newest N if max_count > 0 { let mut backups: Vec<_> = std::fs::read_dir(&backup_dir) .map_err(|e| format!("Failed to read backup dir: {}", e))? @@ -303,6 +308,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! pub fn import_filter(&mut self, filter: ImportedFilter) { self.imported_filters.retain(|f| f.name != filter.name); self.imported_filters.push(filter); @@ -339,10 +345,10 @@ impl Config { } pub fn parse_filter_file(path: &PathBuf) -> Result<ImportedFilter, String> { - let content = std::fs::read_to_string(path) - .map_err(|e| format!("Could not read filter file: {}", e))?; - let filter: ImportedFilter = toml::from_str(&content) - .map_err(|e| format!("Could not parse filter file: {}", e))?; + let content = + std::fs::read_to_string(path).map_err(|e| format!("Could not read filter file: {}", e))?; + let filter: ImportedFilter = + toml::from_str(&content).map_err(|e| format!("Could not parse filter file: {}", e))?; if filter.name.is_empty() { return Err("Filter file must have a name defined".to_string()); } @@ -425,7 +431,7 @@ pub fn resolve_paths(explicit: Option<&PathBuf>) -> HoardomPaths { } } - // nothing works disable caching, use a dummy path + // nothing works disable caching and cry about it (it will still work just no persistant sessions) eprintln!("Warning: could not create .hoardom directory anywhere, caching disabled"); HoardomPaths { config_file: PathBuf::from(".hoardom/config.toml"), @@ -434,4 +440,3 @@ pub fn resolve_paths(explicit: Option<&PathBuf>) -> HoardomPaths { caching_enabled: false, } } - diff --git a/src/lookup.rs b/src/lookup.rs index f5b3177..694f559 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -6,6 +6,10 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +// Dont ask me about the spaghetti code found in here god may know but I dont remember what unholy sins have been created at like 5am here. + + + #[cfg(feature = "builtin-whois")] use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[cfg(feature = "builtin-whois")] @@ -23,7 +27,10 @@ pub struct RdapBootstrap { impl RdapBootstrap { pub async fn fetch(client: &reqwest::Client, verbose: bool) -> Result<Self, String> { if verbose { - eprintln!("[verbose] Fetching RDAP bootstrap from {}", RDAP_BOOTSTRAP_URL); + eprintln!( + "[verbose] Fetching RDAP bootstrap from {}", + RDAP_BOOTSTRAP_URL + ); } let resp = client @@ -47,15 +54,24 @@ impl RdapBootstrap { let tld_map = Self::parse_bootstrap_json(&json); if verbose { - eprintln!("[verbose] RDAP bootstrap loaded, {} TLDs mapped", tld_map.len()); + eprintln!( + "[verbose] RDAP bootstrap loaded, {} TLDs mapped", + tld_map.len() + ); } - Ok(Self { tld_map, raw_json: Some(body) }) + Ok(Self { + tld_map, + raw_json: Some(body), + }) } pub fn load_cached(cache_path: &Path, verbose: bool) -> Result<Self, String> { if verbose { - eprintln!("[verbose] Loading cached RDAP bootstrap from {}", cache_path.display()); + eprintln!( + "[verbose] Loading cached RDAP bootstrap from {}", + cache_path.display() + ); } let body = std::fs::read_to_string(cache_path) .map_err(|e| format!("Could not read cached bootstrap: {}", e))?; @@ -63,9 +79,15 @@ impl RdapBootstrap { .map_err(|e| format!("Could not parse cached bootstrap: {}", e))?; let tld_map = Self::parse_bootstrap_json(&json); if verbose { - eprintln!("[verbose] Cached RDAP bootstrap loaded, {} TLDs mapped", tld_map.len()); + eprintln!( + "[verbose] Cached RDAP bootstrap loaded, {} TLDs mapped", + tld_map.len() + ); } - Ok(Self { tld_map, raw_json: Some(body) }) + Ok(Self { + tld_map, + raw_json: Some(body), + }) } pub fn save_cache(&self, cache_path: &Path) -> Result<(), String> { @@ -126,7 +148,10 @@ pub async fn lookup_domain( None => { // no RDAP server for this TLD, fall back to WHOIS if verbose { - eprintln!("[verbose] No RDAP server for {}, falling back to WHOIS", tld); + eprintln!( + "[verbose] No RDAP server for {}, falling back to WHOIS", + tld + ); } return whois_lookup(whois_overrides, name, tld, verbose).await; } @@ -149,10 +174,14 @@ pub async fn lookup_domain( } else { ErrorKind::Unknown }; - return DomainResult::new(name, tld, DomainStatus::Error { - kind, - message: "unknown error".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind, + message: "unknown error".to_string(), + }, + ); } }; @@ -169,34 +198,50 @@ pub async fn lookup_domain( // 400 = probably invalid query if status_code == 400 { - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::InvalidTld, - message: "invalid tld".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::InvalidTld, + message: "invalid tld".to_string(), + }, + ); } // 429 = rate limited if status_code == 429 { - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::RateLimit, - message: "rate limited".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::RateLimit, + message: "rate limited".to_string(), + }, + ); } // 403 = forbidden (some registries block queries) if status_code == 403 { - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Forbidden, - message: "forbidden".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Forbidden, + message: "forbidden".to_string(), + }, + ); } - // anything else thats not success + // anything else thats not success is le bad if !status_code.is_success() { - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Unknown, - message: format!("HTTP {}", status_code), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Unknown, + message: format!("HTTP {}", status_code), + }, + ); } // 200 = domain exists, try to parse expiry from RDAP json @@ -229,16 +274,30 @@ fn extract_expiry(json: &serde_json::Value) -> Option<String> { // -- No whois feature: just return an error -- #[cfg(not(any(feature = "system-whois", feature = "builtin-whois")))] -async fn whois_lookup(_whois_overrides: &WhoisOverrides, name: &str, tld: &str, _verbose: bool) -> DomainResult { - DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::InvalidTld, - message: "no RDAP server (whois disabled)".to_string(), - }) +async fn whois_lookup( + _whois_overrides: &WhoisOverrides, + name: &str, + tld: &str, + _verbose: bool, +) -> DomainResult { + DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::InvalidTld, + message: "no RDAP server (whois disabled)".to_string(), + }, + ) } // -- System whois: shells out to the systems whois binary -- #[cfg(feature = "system-whois")] -async fn whois_lookup(_whois_overrides: &WhoisOverrides, name: &str, tld: &str, verbose: bool) -> DomainResult { +async fn whois_lookup( + _whois_overrides: &WhoisOverrides, + name: &str, + tld: &str, + verbose: bool, +) -> DomainResult { let full = format!("{}.{}", name, tld); let whois_cmd = env!("HOARDOM_WHOIS_CMD"); let whois_flags = env!("HOARDOM_WHOIS_FLAGS"); @@ -247,7 +306,10 @@ async fn whois_lookup(_whois_overrides: &WhoisOverrides, name: &str, tld: &str, if whois_flags.is_empty() { eprintln!("[verbose] System WHOIS: {} {}", whois_cmd, full); } else { - eprintln!("[verbose] System WHOIS: {} {} {}", whois_cmd, whois_flags, full); + eprintln!( + "[verbose] System WHOIS: {} {} {}", + whois_cmd, whois_flags, full + ); } } @@ -260,35 +322,44 @@ async fn whois_lookup(_whois_overrides: &WhoisOverrides, name: &str, tld: &str, } cmd.arg(&full); - let output = match tokio::time::timeout( - Duration::from_secs(15), - cmd.output(), - ).await { + let output = match tokio::time::timeout(Duration::from_secs(15), cmd.output()).await { Ok(Ok(out)) => out, Ok(Err(e)) => { if verbose { eprintln!("[verbose] System whois error for {}: {}", full, e); } - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Unknown, - message: format!("whois command failed: {}", e), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Unknown, + message: format!("whois command failed: {}", e), + }, + ); } Err(_) => { if verbose { eprintln!("[verbose] System whois timeout for {}", full); } - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Timeout, - message: "whois timeout".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Timeout, + message: "whois timeout".to_string(), + }, + ); } }; let response_str = String::from_utf8_lossy(&output.stdout); if verbose { - eprintln!("[verbose] WHOIS response for {} ({} bytes)", full, output.stdout.len()); + eprintln!( + "[verbose] WHOIS response for {} ({} bytes)", + full, + output.stdout.len() + ); } if !output.status.success() { @@ -300,26 +371,34 @@ async fn whois_lookup(_whois_overrides: &WhoisOverrides, name: &str, tld: &str, if !response_str.is_empty() { return parse_whois_response(name, tld, &response_str); } - return DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Unknown, - message: "whois command returned error".to_string(), - }); + return DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Unknown, + message: "whois command returned error".to_string(), + }, + ); } parse_whois_response(name, tld, &response_str) } -// -- Builtin whois: raw TCP to whois servers directly -- +// -- Builtin whois: rawdogs whois server violently over TCP directly-- + -/// try a whois server, returns the response string or errors out + +/// try a whois server returns the response string or errors out #[cfg(feature = "builtin-whois")] -async fn try_whois_server(server: &str, domain: &str, verbose: bool) -> Result<String, &'static str> { +async fn try_whois_server( + server: &str, + domain: &str, + verbose: bool, +) -> Result<String, &'static str> { let addr = format!("{}:43", server); - let stream = match tokio::time::timeout( - Duration::from_secs(4), - TcpStream::connect(&addr), - ).await { + let stream = match tokio::time::timeout(Duration::from_secs(4), TcpStream::connect(&addr)).await + { Ok(Ok(s)) => s, Ok(Err(_)) => return Err("connect error"), Err(_) => return Err("connect timeout"), @@ -337,10 +416,7 @@ async fn try_whois_server(server: &str, domain: &str, verbose: bool) -> Result<S } let mut response = Vec::new(); - match tokio::time::timeout( - Duration::from_secs(8), - reader.read_to_end(&mut response), - ).await { + match tokio::time::timeout(Duration::from_secs(8), reader.read_to_end(&mut response)).await { Ok(Ok(_)) => {} Ok(Err(_)) => return Err("read error"), Err(_) => return Err("read timeout"), @@ -361,7 +437,12 @@ fn whois_candidates(tld: &str) -> Vec<String> { } #[cfg(feature = "builtin-whois")] -async fn whois_lookup(whois_overrides: &WhoisOverrides, name: &str, tld: &str, verbose: bool) -> DomainResult { +async fn whois_lookup( + whois_overrides: &WhoisOverrides, + name: &str, + tld: &str, + verbose: bool, +) -> DomainResult { let full = format!("{}.{}", name, tld); // if Lists.toml has an explicit server ("tld:server"), use ONLY that one @@ -371,14 +452,26 @@ async fn whois_lookup(whois_overrides: &WhoisOverrides, name: &str, tld: &str, v } return match try_whois_server(server, &full, verbose).await { Ok(resp) if !resp.is_empty() => parse_whois_response(name, tld, &resp), - Ok(_) => DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Unknown, - message: "empty whois response".to_string(), - }), - Err(e) => DomainResult::new(name, tld, DomainStatus::Error { - kind: if e.contains("timeout") { ErrorKind::Timeout } else { ErrorKind::Unknown }, - message: format!("whois {}: {}", server, e), - }), + Ok(_) => DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Unknown, + message: "empty whois response".to_string(), + }, + ), + Err(e) => DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: if e.contains("timeout") { + ErrorKind::Timeout + } else { + ErrorKind::Unknown + }, + message: format!("whois {}: {}", server, e), + }, + ), }; } @@ -386,7 +479,11 @@ async fn whois_lookup(whois_overrides: &WhoisOverrides, name: &str, tld: &str, v let candidates = whois_candidates(tld); if verbose { - eprintln!("[verbose] WHOIS probing {} candidates for .{}", candidates.len(), tld); + eprintln!( + "[verbose] WHOIS probing {} candidates for .{}", + candidates.len(), + tld + ); } for server in &candidates { @@ -408,10 +505,14 @@ async fn whois_lookup(whois_overrides: &WhoisOverrides, name: &str, tld: &str, v } // nothing worked - DomainResult::new(name, tld, DomainStatus::Error { - kind: ErrorKind::Unknown, - message: "no whois server reachable".to_string(), - }) + DomainResult::new( + name, + tld, + DomainStatus::Error { + kind: ErrorKind::Unknown, + message: "no whois server reachable".to_string(), + }, + ) } fn parse_whois_response(name: &str, tld: &str, response: &str) -> DomainResult { @@ -483,6 +584,8 @@ fn extract_whois_expiry(response: &str) -> Option<String> { None } + + pub async fn lookup_with_retry( client: &reqwest::Client, bootstrap: &RdapBootstrap, @@ -503,13 +606,19 @@ pub async fn lookup_with_retry( if let DomainStatus::Error { kind, .. } = &result.status { if noretry.contains(kind) { if verbose { - eprintln!("[verbose] Not retrying {}.{} (error kind in noretry list)", name, tld); + eprintln!( + "[verbose] Not retrying {}.{} (error kind in noretry list)", + name, tld + ); } break; } } if verbose { - eprintln!("[verbose] Retry {}/{} for {}.{}", attempt, retries, name, tld); + eprintln!( + "[verbose] Retry {}/{} for {}.{}", + attempt, retries, name, tld + ); } tokio::time::sleep(Duration::from_millis(500)).await; result = lookup_domain(client, bootstrap, whois_overrides, name, tld, verbose).await; @@ -554,7 +663,17 @@ pub async fn lookup_all( let mut results = Vec::with_capacity(total); let delay = Duration::from_secs_f64(delay_secs); for (i, tld) in tlds.iter().enumerate() { - let result = lookup_with_retry(&client, &bootstrap, whois_overrides, name, tld, retries, noretry, verbose).await; + let result = lookup_with_retry( + &client, + &bootstrap, + whois_overrides, + name, + tld, + retries, + noretry, + verbose, + ) + .await; results.push(result); on_progress(i + 1, total); if delay_secs > 0.0 && i + 1 < total { @@ -579,7 +698,17 @@ pub async fn lookup_all( let name = name_owned.clone(); let tld = tld.to_string(); async move { - let result = lookup_with_retry(&client, &bootstrap, &whois_overrides, &name, &tld, retries, &noretry, verbose).await; + let result = lookup_with_retry( + &client, + &bootstrap, + &whois_overrides, + &name, + &tld, + retries, + &noretry, + verbose, + ) + .await; (i, result) } }) @@ -604,7 +733,10 @@ pub async fn refresh_cache(cache_path: &Path, verbose: bool) -> Result<(), Strin let client = build_client(); let bootstrap = RdapBootstrap::fetch(&client, verbose).await?; bootstrap.save_cache(cache_path)?; - eprintln!("RDAP bootstrap cache refreshed ({} TLDs)", bootstrap.tld_map.len()); + eprintln!( + "RDAP bootstrap cache refreshed ({} TLDs)", + bootstrap.tld_map.len() + ); Ok(()) } @@ -642,33 +774,37 @@ async fn resolve_bootstrap( match cached { Some(b) => Some(b), - None => { - match RdapBootstrap::fetch(client, verbose).await { - Ok(b) => { - if let Some(cp) = cache_path { - if let Err(e) = b.save_cache(cp) { - if verbose { - eprintln!("[verbose] Failed to save cache: {}", e); - } - } else if verbose { - eprintln!("[verbose] RDAP bootstrap cached to {}", cp.display()); + None => match RdapBootstrap::fetch(client, verbose).await { + Ok(b) => { + if let Some(cp) = cache_path { + if let Err(e) = b.save_cache(cp) { + if verbose { + eprintln!("[verbose] Failed to save cache: {}", e); } + } else if verbose { + eprintln!("[verbose] RDAP bootstrap cached to {}", cp.display()); } - Some(b) - } - Err(e) => { - eprintln!("Error: {}", e); - eprintln!("Cannot perform lookups without RDAP bootstrap data."); - None } + Some(b) } - } + Err(e) => { + eprintln!("Error: {}", e); + eprintln!("Cannot perform lookups without RDAP bootstrap data."); + None + } + }, } } pub enum StreamMsg { - Result { result: DomainResult, sort_index: usize }, - Progress { current: usize, total: usize }, + Result { + result: DomainResult, + sort_index: usize, + }, + Progress { + current: usize, + total: usize, + }, Error(String), Done, } @@ -698,19 +834,19 @@ pub fn lookup_streaming( let handle = tokio::spawn(async move { let client = build_client(); - let bootstrap = match resolve_bootstrap( - &client, - cache_path.as_deref(), - force_refresh, - verbose, - ).await { - Some(b) => b, - None => { - let _ = tx.send(StreamMsg::Error("Failed to load RDAP bootstrap".to_string())).await; - let _ = tx.send(StreamMsg::Done).await; - return; - } - }; + let bootstrap = + match resolve_bootstrap(&client, cache_path.as_deref(), force_refresh, verbose).await { + Some(b) => b, + None => { + let _ = tx + .send(StreamMsg::Error( + "Failed to load RDAP bootstrap".to_string(), + )) + .await; + let _ = tx.send(StreamMsg::Done).await; + return; + } + }; let total = tlds.len(); let concurrent = (jobs as usize).max(1); @@ -718,9 +854,29 @@ pub fn lookup_streaming( if concurrent <= 1 { let delay = Duration::from_secs_f64(delay_secs); for (i, tld) in tlds.iter().enumerate() { - let result = lookup_with_retry(&client, &bootstrap, &whois_overrides, &name, tld, retries, &noretry, verbose).await; - let _ = tx.send(StreamMsg::Result { result, sort_index: i }).await; - let _ = tx.send(StreamMsg::Progress { current: i + 1, total }).await; + let result = lookup_with_retry( + &client, + &bootstrap, + &whois_overrides, + &name, + tld, + retries, + &noretry, + verbose, + ) + .await; + let _ = tx + .send(StreamMsg::Result { + result, + sort_index: i, + }) + .await; + let _ = tx + .send(StreamMsg::Progress { + current: i + 1, + total, + }) + .await; if delay_secs > 0.0 && i + 1 < total { tokio::time::sleep(delay).await; } @@ -740,7 +896,17 @@ pub fn lookup_streaming( let noretry = Arc::clone(&noretry); let name = name.clone(); async move { - let result = lookup_with_retry(&client, &bootstrap, &whois_overrides, &name, &tld, retries, &noretry, verbose).await; + let result = lookup_with_retry( + &client, + &bootstrap, + &whois_overrides, + &name, + &tld, + retries, + &noretry, + verbose, + ) + .await; (idx, result) } }) @@ -749,8 +915,18 @@ pub fn lookup_streaming( let mut done_count = 0usize; while let Some((idx, result)) = stream.next().await { done_count += 1; - let _ = tx2.send(StreamMsg::Result { result, sort_index: idx }).await; - let _ = tx2.send(StreamMsg::Progress { current: done_count, total }).await; + let _ = tx2 + .send(StreamMsg::Result { + result, + sort_index: idx, + }) + .await; + let _ = tx2 + .send(StreamMsg::Progress { + current: done_count, + total, + }) + .await; } } @@ -776,7 +952,18 @@ pub fn lookup_many_streaming( ) -> LookupStream { if batches.len() == 1 { let (name, tlds) = batches.into_iter().next().unwrap(); - return lookup_streaming(name, tlds, delay_secs, retries, verbose, cache_path, force_refresh, jobs, whois_overrides, noretry); + return lookup_streaming( + name, + tlds, + delay_secs, + retries, + verbose, + cache_path, + force_refresh, + jobs, + whois_overrides, + noretry, + ); } let (tx, rx) = tokio::sync::mpsc::channel(64); @@ -784,19 +971,19 @@ pub fn lookup_many_streaming( let handle = tokio::spawn(async move { let client = build_client(); - let bootstrap = match resolve_bootstrap( - &client, - cache_path.as_deref(), - force_refresh, - verbose, - ).await { - Some(b) => b, - None => { - let _ = tx.send(StreamMsg::Error("Failed to load RDAP bootstrap".to_string())).await; - let _ = tx.send(StreamMsg::Done).await; - return; - } - }; + let bootstrap = + match resolve_bootstrap(&client, cache_path.as_deref(), force_refresh, verbose).await { + Some(b) => b, + None => { + let _ = tx + .send(StreamMsg::Error( + "Failed to load RDAP bootstrap".to_string(), + )) + .await; + let _ = tx.send(StreamMsg::Done).await; + return; + } + }; let total: usize = batches.iter().map(|(_, tlds)| tlds.len()).sum(); let concurrent = (jobs as usize).max(1); @@ -807,9 +994,24 @@ pub fn lookup_many_streaming( let mut global_idx = 0usize; for (name, tlds) in batches { for tld in tlds { - let result = lookup_with_retry(&client, &bootstrap, &whois_overrides, &name, &tld, retries, &noretry, verbose).await; + let result = lookup_with_retry( + &client, + &bootstrap, + &whois_overrides, + &name, + &tld, + retries, + &noretry, + verbose, + ) + .await; current += 1; - let _ = tx.send(StreamMsg::Result { result, sort_index: global_idx }).await; + let _ = tx + .send(StreamMsg::Result { + result, + sort_index: global_idx, + }) + .await; let _ = tx.send(StreamMsg::Progress { current, total }).await; if delay_secs > 0.0 && current < total { tokio::time::sleep(delay).await; @@ -839,7 +1041,17 @@ pub fn lookup_many_streaming( let whois_overrides = Arc::clone(&whois_overrides); let noretry = Arc::clone(&noretry); async move { - let result = lookup_with_retry(&client, &bootstrap, &whois_overrides, &name, &tld, retries, &noretry, verbose).await; + let result = lookup_with_retry( + &client, + &bootstrap, + &whois_overrides, + &name, + &tld, + retries, + &noretry, + verbose, + ) + .await; (idx, result) } }) @@ -848,13 +1060,26 @@ pub fn lookup_many_streaming( let mut done_count = 0usize; while let Some((idx, result)) = stream.next().await { done_count += 1; - let _ = tx2.send(StreamMsg::Result { result, sort_index: idx }).await; - let _ = tx2.send(StreamMsg::Progress { current: done_count, total }).await; + let _ = tx2 + .send(StreamMsg::Result { + result, + sort_index: idx, + }) + .await; + let _ = tx2 + .send(StreamMsg::Progress { + current: done_count, + total, + }) + .await; } } let _ = tx.send(StreamMsg::Done).await; }); - LookupStream { receiver: rx, handle } + LookupStream { + receiver: rx, + handle, + } } diff --git a/src/main.rs b/src/main.rs index aa0b993..9625959 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,13 +98,26 @@ async fn main() { let overrides = whois_overrides(); // parse noretry config into ErrorKind list - let noretry: Vec<ErrorKind> = config.settings.noretry.iter() + let noretry: Vec<ErrorKind> = config + .settings + .noretry + .iter() .filter_map(|s| ErrorKind::from_config_str(s)) .collect(); // TUI mode if args.is_tui() { - if let Err(e) = tui::run_tui(&args, &config, paths.clone(), cache_file.clone(), force_refresh, overrides.clone(), noretry.clone()).await { + if let Err(e) = tui::run_tui( + &args, + &config, + paths.clone(), + cache_file.clone(), + force_refresh, + overrides.clone(), + noretry.clone(), + ) + .await + { eprintln!("TUI error: {}", e); } // save cache timestamp after TUI session if we refreshed @@ -118,7 +131,15 @@ async fn main() { // CLI needs at least one domain unless autosearch was given if args.domains.is_empty() { if let Some(file_path) = &args.autosearch { - run_autosearch(&args, file_path, cache_file.as_deref(), force_refresh, overrides, &noretry).await; + run_autosearch( + &args, + file_path, + cache_file.as_deref(), + force_refresh, + overrides, + &noretry, + ) + .await; if force_refresh && paths.can_save { config.mark_cache_updated(); let _ = config.save(&paths.config_file); @@ -392,7 +413,10 @@ fn parse_domain_input(raw_domain: &str) -> (String, Option<String>) { } } -fn build_effective_tlds(base_tlds: &[&'static str], specific_tld: Option<&str>) -> Vec<&'static str> { +fn build_effective_tlds( + base_tlds: &[&'static str], + specific_tld: Option<&str>, +) -> Vec<&'static str> { if let Some(tld) = specific_tld { vec![Box::leak(tld.to_string().into_boxed_str()) as &'static str] } else { @@ -403,7 +427,13 @@ fn build_effective_tlds(base_tlds: &[&'static str], specific_tld: Option<&str>) fn estimate_total_lookups(domains: &[String], base_tlds: &[&'static str]) -> usize { domains .iter() - .map(|domain| if domain.contains('.') { 1 } else { base_tlds.len() }) + .map(|domain| { + if domain.contains('.') { + 1 + } else { + base_tlds.len() + } + }) .sum() } @@ -415,4 +445,3 @@ fn sort_aggregated_results(mut aggregated: Vec<AggregatedResult>) -> Vec<DomainR }); aggregated.into_iter().map(|item| item.result).collect() } - diff --git a/src/output.rs b/src/output.rs index 6a02f32..32198f4 100644 --- a/src/output.rs +++ b/src/output.rs @@ -57,9 +57,19 @@ pub fn print_full_table(results: &[DomainResult], no_color: bool, no_unicode: bo } // calc column widths - let domain_w = results.iter().map(|r| r.full.len()).max().unwrap_or(10).max(7); + let domain_w = results + .iter() + .map(|r| r.full.len()) + .max() + .unwrap_or(10) + .max(7); let status_w = 10; // "registered" is the longest - let note_w = results.iter().map(|r| r.note_str().len()).max().unwrap_or(4).max(4); + let note_w = results + .iter() + .map(|r| r.note_str().len()) + .max() + .unwrap_or(4) + .max(4); let domain_col = domain_w + 2; let status_col = status_w + 2; @@ -167,10 +177,9 @@ pub fn print_csv(results: &[DomainResult]) { } pub fn write_csv_file(results: &[DomainResult], path: &PathBuf) -> Result<(), String> { - let mut file = std::fs::File::create(path) - .map_err(|e| format!("Could not create CSV file: {}", e))?; - writeln!(file, "Domains, Status, Note") - .map_err(|e| format!("Write error: {}", e))?; + let mut file = + std::fs::File::create(path).map_err(|e| format!("Could not create CSV file: {}", e))?; + writeln!(file, "Domains, Status, Note").map_err(|e| format!("Write error: {}", e))?; for r in results { writeln!(file, "{}, {}, {}", r.full, r.status_str(), r.note_str()) .map_err(|e| format!("Write error: {}", e))?; diff --git a/src/tlds.rs b/src/tlds.rs index 2835c24..95697d0 100644 --- a/src/tlds.rs +++ b/src/tlds.rs @@ -52,8 +52,8 @@ static PARSED_LISTS: OnceLock<ParsedLists> = OnceLock::new(); fn parsed_lists() -> &'static ParsedLists { PARSED_LISTS.get_or_init(|| { - let raw: toml::Value = toml::from_str(include_str!("../Lists.toml")) - .expect("Lists.toml must be valid TOML"); + let raw: toml::Value = + toml::from_str(include_str!("../Lists.toml")).expect("Lists.toml must be valid TOML"); let table = raw.as_table().expect("Lists.toml must be a TOML table"); @@ -1,5 +1,8 @@ use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers, + MouseButton, MouseEvent, MouseEventKind, + }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -19,12 +22,14 @@ use crate::cli::Args; use crate::config::Config; use crate::config::FavoriteEntry; use crate::lookup; -use crate::tlds::{apply_top_tlds, get_tlds_or_default, list_names, default_list_name}; +use crate::tlds::{apply_top_tlds, default_list_name, get_tlds_or_default, list_names}; use crate::types::{DomainResult, DomainStatus, ErrorKind}; // note : this will be the worst shitshow of code you will probably have looked at in youre entire life -// it works and is somewhat stable but i didnt feel like sorting it into nice modules and all. -// have fun +// it works and is somewhat stable but i didnt feel like sorting it into nice modules and all mostly just relying +// on copy pasting shit where i need it +// +// have fun and may you forgive me for this extremly large ahh file. // names and labels const APP_NAME: &str = "hoardom"; @@ -36,7 +41,6 @@ const SEARCH_BUTTON_LABEL: &str = "[Search]"; const STOP_BUTTON_LABEL: &str = "[Stop](s)"; const CLEAR_BUTTON_LABEL: &str = "[Clear](C)"; - // Layout tuning constants const TOPBAR_HEIGHT: u16 = 1; const SEARCH_PANEL_HEIGHT: u16 = 3; @@ -88,8 +92,7 @@ fn export_favorites_txt(path: &Path, favorites: &[FavoriteEntry]) -> Result<(), .map_err(|e| format!("Failed to create export directory: {}", e))?; } let text: Vec<&str> = favorites.iter().map(|f| f.domain.as_str()).collect(); - std::fs::write(path, text.join("\n")) - .map_err(|e| format!("Failed to export favorites: {}", e)) + std::fs::write(path, text.join("\n")).map_err(|e| format!("Failed to export favorites: {}", e)) } fn export_results_csv(path: &Path, results: &[&DomainResult]) -> Result<(), String> { @@ -108,8 +111,7 @@ fn export_results_csv(path: &Path, results: &[&DomainResult]) -> Result<(), Stri )); } - std::fs::write(path, lines.join("\n")) - .map_err(|e| format!("Failed to export results: {}", e)) + std::fs::write(path, lines.join("\n")).map_err(|e| format!("Failed to export results: {}", e)) } #[derive(Debug, Clone, PartialEq)] @@ -125,7 +127,6 @@ enum ExportMode { } impl ExportMode { - fn default_file_name(self) -> &'static str { match self { ExportMode::FavoritesTxt => "hoardom-favorites.txt", @@ -229,7 +230,16 @@ struct PanelRects { } impl App { - fn new(args: &Args, config: &Config, config_path: PathBuf, can_save: bool, cache_path: Option<PathBuf>, force_refresh: bool, whois_overrides: crate::tlds::WhoisOverrides, noretry: Vec<ErrorKind>) -> Self { + fn new( + args: &Args, + config: &Config, + config_path: PathBuf, + can_save: bool, + cache_path: Option<PathBuf>, + force_refresh: bool, + whois_overrides: crate::tlds::WhoisOverrides, + noretry: Vec<ErrorKind>, + ) -> Self { let tld_list_name = args .tld_list .as_ref() @@ -271,7 +281,11 @@ impl App { verbose: args.verbose, delay: args.effective_delay(), retries: args.effective_retry(), - jobs: if args.jobs.is_some() { args.effective_jobs() } else { config.settings.jobs.max(1) }, + jobs: if args.jobs.is_some() { + args.effective_jobs() + } else { + config.settings.jobs.max(1) + }, panel_rects: PanelRects::default(), stream_rx: None, stream_task: None, @@ -421,7 +435,11 @@ impl App { last_res_export_path: self.last_res_export_path.clone(), top_tlds: self.top_tlds.clone().unwrap_or_default(), jobs: self.jobs, - noretry: self.noretry.iter().map(|k| k.to_config_str().to_string()).collect(), + noretry: self + .noretry + .iter() + .map(|k| k.to_config_str().to_string()) + .collect(), backups: self.backups_enabled, backup_count: self.backup_count, }, @@ -437,7 +455,9 @@ impl App { let d = domain.to_lowercase(); if !self.favorites.iter().any(|f| f.domain == d) { // check if we just looked this domain up - inherit its status - let status = self.results.iter() + let status = self + .results + .iter() .find(|(_, r)| r.full.to_lowercase() == d) .map(|(_, r)| r.status_str().to_string()) .unwrap_or_else(|| "unknown".to_string()); @@ -477,7 +497,11 @@ impl App { if self.show_unavailable { self.results.iter().map(|(_, r)| r).collect() } else { - self.results.iter().filter(|(_, r)| r.is_available()).map(|(_, r)| r).collect() + self.results + .iter() + .filter(|(_, r)| r.is_available()) + .map(|(_, r)| r) + .collect() } } @@ -598,7 +622,16 @@ pub async fn run_tui( let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut app = App::new(args, config, paths.config_file.clone(), paths.can_save, cache_file, force_refresh, whois_overrides, noretry); + let mut app = App::new( + args, + config, + paths.config_file.clone(), + paths.can_save, + cache_file, + force_refresh, + whois_overrides, + noretry, + ); if !paths.can_save { app.status_msg = Some("Warning: favorites and settings wont be saved".to_string()); @@ -608,10 +641,7 @@ pub async fn run_tui( // put the terminal back to normal if !args.no_mouse { - execute!( - terminal.backend_mut(), - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), DisableMouseCapture)?; while event::poll(std::time::Duration::from_millis(0))? { let _ = event::read(); @@ -638,7 +668,10 @@ async fn run_app( } if let Some(popup) = app.export_popup.as_ref() { - if popup.close_at.is_some_and(|deadline| Instant::now() >= deadline) { + if popup + .close_at + .is_some_and(|deadline| Instant::now() >= deadline) + { app.export_popup = None; } } @@ -693,7 +726,9 @@ async fn run_app( Ok(lookup::StreamMsg::Result { result, .. }) => { // Update the matching favorite's status let domain_lower = result.full.to_lowercase(); - if let Some(fav) = app.favorites.iter_mut().find(|f| f.domain == domain_lower) { + if let Some(fav) = + app.favorites.iter_mut().find(|f| f.domain == domain_lower) + { let new_status = result.status_str().to_string(); if fav.status != new_status && fav.status != "unknown" { fav.changed = true; @@ -786,7 +821,11 @@ async fn run_app( app.dropdown = DropdownState::Closed; app.set_focus(match app.focus { Focus::Search => { - if app.show_notes_panel { Focus::Scratchpad } else { Focus::Results } + if app.show_notes_panel { + Focus::Scratchpad + } else { + Focus::Results + } } Focus::Scratchpad => Focus::Results, Focus::Results => Focus::Favorites, @@ -800,7 +839,11 @@ async fn run_app( Focus::Search => Focus::Settings, Focus::Scratchpad => Focus::Search, Focus::Results => { - if app.show_notes_panel { Focus::Scratchpad } else { Focus::Search } + if app.show_notes_panel { + Focus::Scratchpad + } else { + Focus::Search + } } Focus::Favorites => Focus::Results, Focus::Settings => Focus::Favorites, @@ -901,7 +944,11 @@ fn handle_export_popup_key(app: &mut App, key: KeyCode) { popup.selected_row = (popup.selected_row + 1) % 4; } KeyCode::BackTab | KeyCode::Up => { - popup.selected_row = if popup.selected_row == 0 { 3 } else { popup.selected_row - 1 }; + popup.selected_row = if popup.selected_row == 0 { + 3 + } else { + popup.selected_row - 1 + }; } KeyCode::Left => { if popup.selected_row == 0 { @@ -1057,7 +1104,9 @@ async fn handle_search_key(app: &mut App, key: KeyCode) { } } KeyCode::Delete => { - if app.cursor_pos < app.search_input.len() && app.search_input.is_char_boundary(app.cursor_pos) { + if app.cursor_pos < app.search_input.len() + && app.search_input.is_char_boundary(app.cursor_pos) + { app.search_input.remove(app.cursor_pos); } } @@ -1100,7 +1149,11 @@ fn handle_results_key(app: &mut App, key: KeyCode) { KeyCode::Up => { let i = match app.results_state.selected() { Some(i) => { - if i > 0 { i - 1 } else { 0 } + if i > 0 { + i - 1 + } else { + 0 + } } None => 0, }; @@ -1109,7 +1162,11 @@ fn handle_results_key(app: &mut App, key: KeyCode) { KeyCode::Down => { let i = match app.results_state.selected() { Some(i) => { - if i + 1 < len { i + 1 } else { i } + if i + 1 < len { + i + 1 + } else { + i + } } None => 0, }; @@ -1139,7 +1196,11 @@ fn handle_favorites_key(app: &mut App, key: KeyCode) { KeyCode::Up => { let i = match app.favorites_state.selected() { Some(i) => { - if i > 0 { i - 1 } else { 0 } + if i > 0 { + i - 1 + } else { + 0 + } } None => 0, }; @@ -1148,7 +1209,11 @@ fn handle_favorites_key(app: &mut App, key: KeyCode) { KeyCode::Down => { let i = match app.favorites_state.selected() { Some(i) => { - if i + 1 < len { i + 1 } else { i } + if i + 1 < len { + i + 1 + } else { + i + } } None => 0, }; @@ -1257,26 +1322,24 @@ fn handle_settings_key(app: &mut App, key: KeyCode) { _ => {} } } - KeyCode::Char(' ') => { - match app.settings_selected.unwrap_or(0) { - 1 => { - app.show_unavailable = !app.show_unavailable; - app.save_config(); - } - 2 => { - app.show_notes_panel = !app.show_notes_panel; - if !app.show_notes_panel && app.focus == Focus::Scratchpad { - app.set_focus(Focus::Results); - } - app.save_config(); - } - 3 => { - app.clear_on_search = !app.clear_on_search; - app.save_config(); + KeyCode::Char(' ') => match app.settings_selected.unwrap_or(0) { + 1 => { + app.show_unavailable = !app.show_unavailable; + app.save_config(); + } + 2 => { + app.show_notes_panel = !app.show_notes_panel; + if !app.show_notes_panel && app.focus == Focus::Scratchpad { + app.set_focus(Focus::Results); } - _ => {} + app.save_config(); } - } + 3 => { + app.clear_on_search = !app.clear_on_search; + app.save_config(); + } + _ => {} + }, KeyCode::Char('+') | KeyCode::Char('=') => { if app.settings_selected == Some(4) { app.jobs = if app.jobs >= 99 { 99 } else { app.jobs + 1 }; @@ -1502,7 +1565,11 @@ fn handle_mouse(app: &mut App, mouse: MouseEvent) { app.set_focus(Focus::Results); let visible_len = app.visible_results().len(); let content_start = results_rect.y + 1; - let progress_offset = if app.searching && app.search_progress.1 > 0 { 1 } else { 0 }; + let progress_offset = if app.searching && app.search_progress.1 > 0 { + 1 + } else { + 0 + }; let header_offset = if visible_len > 0 { 1 } else { 0 }; let list_start = content_start + progress_offset + header_offset; @@ -1653,14 +1720,18 @@ fn start_fav_check(app: &mut App) { app.checking_favorites = true; // Build a batch: each favorite is "name.tld" -> lookup (name, [tld]) - let batches: lookup::LookupBatch = app.favorites.iter().filter_map(|fav| { - let parts: Vec<&str> = fav.domain.splitn(2, '.').collect(); - if parts.len() == 2 { - Some((parts[0].to_string(), vec![parts[1].to_string()])) - } else { - None - } - }).collect(); + let batches: lookup::LookupBatch = app + .favorites + .iter() + .filter_map(|fav| { + let parts: Vec<&str> = fav.domain.splitn(2, '.').collect(); + if parts.len() == 2 { + Some((parts[0].to_string(), vec![parts[1].to_string()])) + } else { + None + } + }) + .collect(); if batches.is_empty() { app.checking_favorites = false; @@ -1793,7 +1864,10 @@ fn draw_ui(f: &mut Frame, app: &mut App) { .split(size); let content_area = main_chunks[1]; - let desired_sidebar = content_area.width.saturating_mul(SIDEBAR_TARGET_WIDTH_PERCENT) / 100; + let desired_sidebar = content_area + .width + .saturating_mul(SIDEBAR_TARGET_WIDTH_PERCENT) + / 100; let mut sidebar_width = clamp_panel_size(desired_sidebar, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH) .min(content_area.width.saturating_sub(RESULTS_MIN_WIDTH)); if sidebar_width == 0 { @@ -1809,7 +1883,10 @@ fn draw_ui(f: &mut Frame, app: &mut App) { let (scratchpad_chunk, results_chunk) = if app.show_notes_panel { let center_width = content_area.width.saturating_sub(sidebar_width); - let desired_scratchpad = content_area.width.saturating_mul(SCRATCHPAD_TARGET_WIDTH_PERCENT) / 100; + let desired_scratchpad = content_area + .width + .saturating_mul(SCRATCHPAD_TARGET_WIDTH_PERCENT) + / 100; let mut scratchpad_width = clamp_panel_size( desired_scratchpad, SCRATCHPAD_MIN_WIDTH, @@ -1941,11 +2018,15 @@ 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), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), )), Line::from(Span::styled( fit_cell_center("I AM BEING CRUSHED!", content_width), - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), )), Line::from(fit_cell_center("", content_width)), Line::from(Span::styled( @@ -1953,21 +2034,31 @@ fn draw_terminal_too_small(f: &mut Frame, area: Rect) { Style::default().fg(Color::White), )), Line::from(Span::styled( - fit_cell_center(&format!("Need {}x{} of space", MIN_UI_WIDTH, MIN_UI_HEIGHT), content_width), + fit_cell_center( + &format!("Need {}x{} of space", MIN_UI_WIDTH, MIN_UI_HEIGHT), + content_width, + ), Style::default().fg(Color::White), )), Line::from(Span::styled( - fit_cell_center(&format!("Current: {}x{}", area.width, area.height), content_width), + fit_cell_center( + &format!("Current: {}x{}", area.width, area.height), + content_width, + ), Style::default().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), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), )), Line::from(Span::styled( fit_cell_center("GIVE ME BACK MY SPACE! >:(", content_width), - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), )), ]; @@ -1981,14 +2072,49 @@ fn draw_topbar(f: &mut Frame, area: Rect) { let right = format!("{} {}", EXPORT_BUTTON_LABEL, HELP_BUTTON_LABEL); let gap = width.saturating_sub(left.chars().count() + right.chars().count()); let paragraph = Paragraph::new(Line::from(vec![ - Span::styled(CLOSE_BUTTON_LABEL, Style::default().fg(Color::Red).bg(Color::Gray).add_modifier(Modifier::BOLD)), - Span::styled(format!(" {}", title), Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(" ".repeat(gap), Style::default().bg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(EXPORT_BUTTON_LABEL, Style::default().fg(Color::LightGreen).bg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(" ", Style::default().bg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(HELP_BUTTON_LABEL, Style::default().fg(Color::LightGreen).bg(Color::Red).add_modifier(Modifier::BOLD)), + Span::styled( + CLOSE_BUTTON_LABEL, + Style::default() + .fg(Color::Red) + .bg(Color::Gray) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!(" {}", title), + Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + " ".repeat(gap), + Style::default().bg(Color::Red).add_modifier(Modifier::BOLD), + ), + Span::styled( + EXPORT_BUTTON_LABEL, + Style::default() + .fg(Color::LightGreen) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + " ", + Style::default().bg(Color::Red).add_modifier(Modifier::BOLD), + ), + Span::styled( + HELP_BUTTON_LABEL, + Style::default() + .fg(Color::LightGreen) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + ), ])) - .style(Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD)); + .style( + Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + ); f.render_widget(paragraph, area); } @@ -2000,22 +2126,60 @@ fn draw_help_overlay(f: &mut Frame, app: &mut App, area: Rect) { 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("F1 or Help button Toggle this help", Style::default().fg(Color::White))), - Line::from(Span::styled("F2 or Export button Open export popup", Style::default().fg(Color::White))), - Line::from(Span::styled("Ctrl+C Quit the app", Style::default().fg(Color::White))), - Line::from(Span::styled("s Stop/cancel running search", Style::default().fg(Color::White))), - Line::from(Span::styled("Esc Clear selection or close help", Style::default().fg(Color::White))), - Line::from(Span::styled("Tab or Shift+Tab Move between panels", Style::default().fg(Color::White))), - Line::from(Span::styled("Up and Down arrows Navigate results", Style::default().fg(Color::White))), + Line::from(Span::styled( + "F1 or Help button Toggle this help", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "F2 or Export button Open export popup", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Ctrl+C Quit the app", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "s Stop/cancel running search", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Esc Clear selection or close help", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Tab or Shift+Tab Move between panels", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Up and Down arrows Navigate results", + Style::default().fg(Color::White), + )), Line::from(Span::styled(" ", Style::default().fg(Color::White))), - Line::from(Span::styled("Mouse Click Elements duh", Style::default().fg(Color::White))), - Line::from(Span::styled("Scrolling Scroll through elements (yea)", Style::default().fg(Color::White))), - + Line::from(Span::styled( + "Mouse Click Elements duh", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Scrolling Scroll through elements (yea)", + Style::default().fg(Color::White), + )), Line::from(Span::styled(" ", Style::default().fg(Color::White))), - Line::from(Span::styled("In Results :", Style::default().fg(Color::White))), - Line::from(Span::styled("Enter Add highlighted result to Favorites", Style::default().fg(Color::White))), - Line::from(Span::styled("In Favorites :", Style::default().fg(Color::White))), - Line::from(Span::styled("Backspace or Delete Remove focused favorite", Style::default().fg(Color::White))), + Line::from(Span::styled( + "In Results :", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Enter Add highlighted result to Favorites", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "In Favorites :", + Style::default().fg(Color::White), + )), + Line::from(Span::styled( + "Backspace or Delete Remove focused favorite", + Style::default().fg(Color::White), + )), ]; let block = Block::default() @@ -2066,7 +2230,10 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) { style }; - let subtitle = fit_cell_center("Choose what to export and where to save it.", chunks[0].width as usize); + let subtitle = fit_cell_center( + "Choose what to export and where to save it.", + chunks[0].width as usize, + ); f.render_widget( Paragraph::new(subtitle).style(Style::default().fg(Color::DarkGray)), chunks[0], @@ -2118,9 +2285,13 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) { ); let status_style = if popup_state.status_success { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) } else if popup_state.confirm_overwrite { - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) } else if popup_state.status.is_some() { Style::default().fg(Color::Red) } else { @@ -2132,7 +2303,6 @@ fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) { chunks[3], ); - let cancel_label = "[Cancel]"; let button_gap = " "; let save_label = "[Save]"; @@ -2158,18 +2328,28 @@ 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) + Style::default() + .fg(Color::Green) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) }, ), 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) + Style::default() + .fg(Color::Green) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) }, ), ]); @@ -2207,7 +2387,12 @@ fn draw_results(f: &mut Frame, app: &mut App, area: Rect) { Some(d) => format!(" | Took: {:.1}s", d.as_secs_f64()), None => String::new(), }; - format!(" Results ({} available / {} total{}) ", avail, app.results.len(), duration_str) + format!( + " Results ({} available / {} total{}) ", + avail, + app.results.len(), + duration_str + ) }; let block = Block::default() @@ -2251,13 +2436,34 @@ fn draw_results(f: &mut Frame, app: &mut App, area: Rect) { 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 selected_bg = Color::Black; // collect visible results let visible_data: Vec<(String, String, String, DomainStatus)> = if app.show_unavailable { - app.results.iter().map(|(_, r)| (r.full.clone(), r.status_str().to_string(), r.note_str(), r.status.clone())).collect() + app.results + .iter() + .map(|(_, r)| { + ( + r.full.clone(), + r.status_str().to_string(), + r.note_str(), + r.status.clone(), + ) + }) + .collect() } else { - app.results.iter().filter(|(_, r)| r.is_available()).map(|(_, r)| (r.full.clone(), r.status_str().to_string(), r.note_str(), r.status.clone())).collect() + app.results + .iter() + .filter(|(_, r)| r.is_available()) + .map(|(_, r)| { + ( + r.full.clone(), + r.status_str().to_string(), + r.note_str(), + r.status.clone(), + ) + }) + .collect() }; if visible_data.is_empty() && !app.searching { @@ -2311,18 +2517,38 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) { if let Some(header_area) = header_area { let mut header_spans = vec![ - Span::styled(format!(" {}", fit_cell("Domain", domain_w)), Style::default().fg(Color::Gray).add_modifier(Modifier::BOLD)), + Span::styled( + format!(" {}", fit_cell("Domain", domain_w)), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + ), Span::styled(" │ ", Style::default().fg(Color::DarkGray)), - Span::styled(fit_cell("Status", status_w), Style::default().fg(Color::Gray).add_modifier(Modifier::BOLD)), + Span::styled( + fit_cell("Status", status_w), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + ), ]; if show_note_column { header_spans.push(Span::styled(" │ ", Style::default().fg(Color::DarkGray))); - header_spans.push(Span::styled(fit_cell("Details", note_w), Style::default().fg(Color::Gray).add_modifier(Modifier::BOLD))); + header_spans.push(Span::styled( + fit_cell("Details", note_w), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + )); } header_spans.push(Span::styled(" │ ", Style::default().fg(Color::DarkGray))); - header_spans.push(Span::styled(" ✓ ", Style::default().fg(Color::Gray).add_modifier(Modifier::BOLD))); + header_spans.push(Span::styled( + " ✓ ", + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + )); f.render_widget(Paragraph::new(Line::from(header_spans)), header_area); } @@ -2361,22 +2587,40 @@ fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) { }; let mut spans = vec![ - Span::styled(format!(" {}", fit_cell(full, domain_w)), apply_bg(domain_style)), + Span::styled( + format!(" {}", fit_cell(full, domain_w)), + apply_bg(domain_style), + ), Span::styled(" \u{2502} ", apply_bg(Style::default().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)))); - spans.push(Span::styled(fit_cell(note, note_w), apply_bg(Style::default().fg(Color::White)))); + spans.push(Span::styled( + " \u{2502} ", + apply_bg(Style::default().fg(Color::Gray)), + )); + spans.push(Span::styled( + fit_cell(note, note_w), + apply_bg(Style::default().fg(Color::White)), + )); } - spans.push(Span::styled(" \u{2502} ", apply_bg(Style::default().fg(Color::Gray)))); + spans.push(Span::styled( + " \u{2502} ", + apply_bg(Style::default().fg(Color::Gray)), + )); spans.push(match status { - DomainStatus::Available => Span::styled(" ✓ ", apply_bg(Style::default().fg(Color::Green))), - DomainStatus::Registered { .. } => Span::styled(" ✗ ", apply_bg(Style::default().fg(Color::Red))), + DomainStatus::Available => { + Span::styled(" ✓ ", apply_bg(Style::default().fg(Color::Green))) + } + DomainStatus::Registered { .. } => { + Span::styled(" ✗ ", apply_bg(Style::default().fg(Color::Red))) + } DomainStatus::Error { kind, .. } => match kind { - ErrorKind::InvalidTld => Span::styled(" ? ", apply_bg(Style::default().fg(Color::Yellow))), + ErrorKind::InvalidTld => { + Span::styled(" ? ", apply_bg(Style::default().fg(Color::Yellow))) + } _ => Span::styled(" ! ", apply_bg(Style::default().fg(Color::Blue))), }, }); @@ -2553,8 +2797,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(Style::default().fg(Color::White)), inner, ); @@ -2623,17 +2866,17 @@ fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) { }) .collect(); - let list = List::new(items) - .highlight_style( - Style::default() - .add_modifier(Modifier::REVERSED), - ); + let list = List::new(items).highlight_style(Style::default().add_modifier(Modifier::REVERSED)); f.render_widget(block, area); f.render_stateful_widget(list, list_area, &mut app.favorites_state); // Draw the check button at the bottom - let btn_label = if app.checking_favorites { "checking..." } else { "[c]heck all" }; + let btn_label = if app.checking_favorites { + "checking..." + } else { + "[c]heck all" + }; let btn_style = if app.checking_favorites { Style::default().fg(Color::DarkGray) } else { @@ -2688,13 +2931,17 @@ fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) { }; let tld_row_style = if selected == Some(0) { - Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD) + Style::default() + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD) } else { Style::default() }; let jobs_row_style = if selected == Some(4) { - Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD) + Style::default() + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD) } else { Style::default() }; @@ -2801,23 +3048,44 @@ fn draw_search(f: &mut Frame, app: &mut App, area: Rect) { let cancel_enabled = app.searching; let search_style = if search_enabled { - Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::DarkGray).bg(Color::Black) }; let stop_style = if cancel_enabled { - Style::default().fg(Color::Black).bg(Color::Yellow).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Black) + .bg(Color::Yellow) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::DarkGray).bg(Color::Black) }; - let clear_style = Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD); + let clear_style = Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD); - f.render_widget(Paragraph::new(SEARCH_BUTTON_LABEL).style(search_style), chunks[1]); + f.render_widget( + Paragraph::new(SEARCH_BUTTON_LABEL).style(search_style), + chunks[1], + ); if app.clear_on_search { - f.render_widget(Paragraph::new(STOP_BUTTON_LABEL).style(stop_style), chunks[3]); + f.render_widget( + Paragraph::new(STOP_BUTTON_LABEL).style(stop_style), + chunks[3], + ); } else { - f.render_widget(Paragraph::new(STOP_BUTTON_LABEL).style(stop_style), chunks[3]); - f.render_widget(Paragraph::new(CLEAR_BUTTON_LABEL).style(clear_style), chunks[5]); + f.render_widget( + Paragraph::new(STOP_BUTTON_LABEL).style(stop_style), + chunks[3], + ); + f.render_widget( + Paragraph::new(CLEAR_BUTTON_LABEL).style(clear_style), + chunks[5], + ); } // show cursor in search bar when focused @@ -2836,7 +3104,10 @@ fn draw_dropdown(f: &mut Frame, app: &mut App, settings_area: Rect, selected: us let dropdown_full = Rect { x: settings_area.x + 1, y: settings_area.y + 1, - width: settings_area.width.saturating_sub(2).min(DROPDOWN_MAX_WIDTH), + width: settings_area + .width + .saturating_sub(2) + .min(DROPDOWN_MAX_WIDTH), height: (options.len() as u16 + 2).min(DROPDOWN_MAX_HEIGHT), }; @@ -2861,9 +3132,12 @@ fn draw_dropdown(f: &mut Frame, app: &mut App, settings_area: Rect, selected: us .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)); + let list = List::new(items).block(block).highlight_style( + Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + ); let mut state = ListState::default(); state.select(Some(selected)); f.render_stateful_widget(list, dropdown_full, &mut state); diff --git a/src/types.rs b/src/types.rs index 9a496c2..cc3950b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -89,5 +89,3 @@ impl DomainResult { } } } - - |
