From 4cc3e2bf861ba04e3924e337b9ac5d3e8d21eb02 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sun, 8 Mar 2026 15:09:27 +0100 Subject: sexy --- Cargo.toml | 23 +- Makefile | 220 ++++++++++ README.md | 4 + dist/AppIcon.icns | Bin 0 -> 640554 bytes dist/AppIcon.ico | Bin 0 -> 1143198 bytes dist/hoardom.desktop | 10 + dist/mac-launcher.sh | 19 + doc/CLI.md | 6 +- doc/TUI.md | 49 +-- src/app.rs | 1155 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/cli.rs | 33 +- 11 files changed, 1473 insertions(+), 46 deletions(-) create mode 100644 Makefile create mode 100644 dist/AppIcon.icns create mode 100644 dist/AppIcon.ico create mode 100644 dist/hoardom.desktop create mode 100644 dist/mac-launcher.sh create mode 100644 src/app.rs diff --git a/Cargo.toml b/Cargo.toml index 91f6d15..b7cd5bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "hoardom" -version = "1.0.4" +version = "1.1.2" edition = "2021" description = "Domain hoarding made less painful" +default-run = "hoardom" + [features] default = ["builtin-whois"] @@ -17,6 +19,11 @@ system-whois = [] # Cannot be used together with system-whois builtin-whois = [] +# native gui wrapper binary (hoardom-app) +# spawns hoardom --tui in a pty and renders it in its own window +# so it shows up as its own app with its own dock icon +gui = ["dep:eframe", "dep:portable-pty", "dep:vte"] + # Configurable values baked into the binary at compile time via build.rs [package.metadata.hoardom] whois-command = "whois" @@ -37,3 +44,17 @@ crossterm = "0.28" indicatif = "0.17" chrono = "0.4" futures = "0.3" + +# gui wrapper deps (only built with --features gui) +eframe = { version = "0.30", optional = true } +portable-pty = { version = "0.8", optional = true } +vte = { version = "0.14", optional = true } + +[[bin]] +name = "hoardom" +path = "src/main.rs" + +[[bin]] +name = "hoardom-app" +path = "src/app.rs" +required-features = ["gui"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f023892 --- /dev/null +++ b/Makefile @@ -0,0 +1,220 @@ +# hoardom makefile +# supports: make, make install, make deb, make pkg, make clean + +NAME := hoardom +VERSION := $(shell grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') +DESCRIPTION := Domain hoarding made less painful +MAINTAINER := crt + +PREFIX ?= /usr/local +BINDIR := $(PREFIX)/bin +DATADIR := $(PREFIX)/share +APPDIR := $(DATADIR)/applications +ICONDIR := $(DATADIR)/icons/hicolor/256x256/apps +ICON_FILE := dist/AppIcon.ico + +CARGO ?= cargo +CARGO_FLAGS ?= + +BUILDDIR := target/release +BINARY := $(BUILDDIR)/$(NAME) +GUI_BINARY := $(BUILDDIR)/$(NAME)-app + +# packaging scratch dirs +PKG_ROOT := target/pkg-root +DEB_ROOT := target/deb-root +MAC_ROOT := target/mac-root +MAC_APP := target/mac-root/$(NAME).app + +# ---- build ---- + +.PHONY: all build release debug clean install uninstall deb pkg app + +all: release + +release: + $(CARGO) build --release $(CARGO_FLAGS) + +debug: + $(CARGO) build $(CARGO_FLAGS) + +# build the gui wrapper (requires gui feature) +app: release + $(CARGO) build --release --features gui $(CARGO_FLAGS) + +# ---- install (linux / mac) ---- + +install: release + @echo "installing $(NAME) to $(DESTDIR)$(BINDIR)" + install -d $(DESTDIR)$(BINDIR) + install -m 755 $(BINARY) $(DESTDIR)$(BINDIR)/$(NAME) + @# install gui wrapper too if it was built + @if [ -f $(GUI_BINARY) ]; then \ + install -m 755 $(GUI_BINARY) $(DESTDIR)$(BINDIR)/$(NAME)-app; \ + fi +ifeq ($(shell uname), Darwin) + @echo "installing macOS app bundle" + install -d $(DESTDIR)/Applications + $(MAKE) _mac_app APP_DEST=$(DESTDIR)/Applications +else + @echo "installing desktop file" + install -d $(DESTDIR)$(APPDIR) + install -m 644 dist/$(NAME).desktop $(DESTDIR)$(APPDIR)/$(NAME).desktop + install -d $(DESTDIR)$(ICONDIR) + install -m 644 $(ICON_FILE) $(DESTDIR)$(ICONDIR)/$(NAME).ico +endif + @echo "done, $(NAME) is ready to go" + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/$(NAME) + rm -f $(DESTDIR)$(BINDIR)/$(NAME)-app + rm -f $(DESTDIR)$(APPDIR)/$(NAME).desktop + rm -f $(DESTDIR)$(ICONDIR)/$(NAME).ico +ifeq ($(shell uname), Darwin) + rm -rf $(DESTDIR)/Applications/$(NAME).app +endif + @echo "uninstalled" + +# ---- debian .deb package ---- + +deb: release + @echo "building deb package v$(VERSION)" + rm -rf $(DEB_ROOT) + + # binary + install -d $(DEB_ROOT)/usr/bin + install -m 755 $(BINARY) $(DEB_ROOT)/usr/bin/$(NAME) + + # desktop file + icon + install -d $(DEB_ROOT)/usr/share/applications + install -m 644 dist/$(NAME).desktop $(DEB_ROOT)/usr/share/applications/$(NAME).desktop + install -d $(DEB_ROOT)/usr/share/icons/hicolor/256x256/apps + install -m 644 $(ICON_FILE) $(DEB_ROOT)/usr/share/icons/hicolor/256x256/apps/$(NAME).ico + + # control file + install -d $(DEB_ROOT)/DEBIAN + printf 'Package: $(NAME)\n\ +Version: $(VERSION)\n\ +Section: utils\n\ +Priority: optional\n\ +Architecture: $(shell dpkg --print-architecture 2>/dev/null || echo amd64)\n\ +Maintainer: $(MAINTAINER)\n\ +Description: $(DESCRIPTION)\n\ + TUI and CLI tool for searching domain availability across TLD lists.\n\ + Includes favorites, scratchpad, export, and custom list support.\n' > $(DEB_ROOT)/DEBIAN/control + + dpkg-deb --build --root-owner-group $(DEB_ROOT) target/$(NAME)_$(VERSION)_$(shell dpkg --print-architecture 2>/dev/null || echo amd64).deb + @echo "deb built: target/$(NAME)_$(VERSION)_*.deb" + +# ---- macOS .pkg package ---- + +pkg: app + @echo "building macOS pkg v$(VERSION)" + rm -rf $(PKG_ROOT) $(MAC_ROOT) + + # cli binary package + install -d $(PKG_ROOT)/cli$(PREFIX)/bin + install -m 755 $(BINARY) $(PKG_ROOT)/cli$(PREFIX)/bin/$(NAME) + + pkgbuild \ + --root $(PKG_ROOT)/cli \ + --identifier ch.teleco.$(NAME).cli \ + --version $(VERSION) \ + --install-location / \ + target/$(NAME)-cli.pkg + + # app bundle package + install -d $(PKG_ROOT)/app + $(MAKE) _mac_app APP_DEST=$(PKG_ROOT)/app + + pkgbuild \ + --component $(PKG_ROOT)/app/$(NAME).app \ + --identifier ch.teleco.$(NAME).app \ + --version $(VERSION) \ + --install-location /Applications \ + target/$(NAME)-app.pkg + + # distribution xml so the installer actually has permission to write to /Applications + printf '\n\ +\n\ + $(NAME) $(VERSION)\n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + #$(NAME)-cli.pkg\n\ + #$(NAME)-app.pkg\n\ +\n' > $(PKG_ROOT)/distribution.xml + + productbuild \ + --distribution $(PKG_ROOT)/distribution.xml \ + --package-path target \ + target/$(NAME)-$(VERSION).pkg + rm -f target/$(NAME)-cli.pkg target/$(NAME)-app.pkg + rm -rf $(PKG_ROOT) + @echo "pkg built: target/$(NAME)-$(VERSION).pkg" + +# ---- internal: macOS .app bundle ---- + +_mac_app: + @test -n "$(APP_DEST)" || (echo "APP_DEST not set" && exit 1) + install -d $(APP_DEST)/$(NAME).app/Contents/MacOS + install -d $(APP_DEST)/$(NAME).app/Contents/Resources + + # gui wrapper as the executable (or shell launcher as fallback) + @if [ -f $(GUI_BINARY) ]; then \ + echo "using native gui wrapper"; \ + install -m 755 $(GUI_BINARY) $(APP_DEST)/$(NAME).app/Contents/MacOS/$(NAME)-app; \ + else \ + echo "gui wrapper not built, using shell launcher fallback"; \ + install -m 755 dist/mac-launcher.sh $(APP_DEST)/$(NAME).app/Contents/MacOS/$(NAME)-app; \ + fi + + # the actual tui binary (gui wrapper spawns this, shell launcher also needs it) + install -m 755 $(BINARY) $(APP_DEST)/$(NAME).app/Contents/MacOS/$(NAME) + + # Info.plist + printf '\n\ +\n\ +\n\ +\n\ + CFBundleExecutable\n\ + $(NAME)-app\n\ + CFBundleIdentifier\n\ + ch.teleco.$(NAME)\n\ + CFBundleName\n\ + $(NAME)\n\ + CFBundleDisplayName\n\ + hoardom\n\ + CFBundleVersion\n\ + $(VERSION)\n\ + CFBundleShortVersionString\n\ + $(VERSION)\n\ + CFBundlePackageType\n\ + APPL\n\ + CFBundleIconFile\n\ + icon.icns\n\ + LSMinimumSystemVersion\n\ + 10.12\n\ + NSHighResolutionCapable\n\ + \n\ +\n\ +\n' > $(APP_DEST)/$(NAME).app/Contents/Info.plist + + # app icon + cp dist/AppIcon.icns $(APP_DEST)/$(NAME).app/Contents/Resources/icon.icns + +# ---- clean ---- + +clean: + $(CARGO) clean + rm -rf $(PKG_ROOT) $(DEB_ROOT) $(MAC_ROOT) + rm -f target/$(NAME)_*.deb target/$(NAME)-*.pkg diff --git a/README.md b/README.md index 572f330..27579bb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ for example when gambling online just isnt doing it for you anymore) Seriously for once : It is meant to atleast make the journey you'll have loosing your mind finding a banger domain more tolerable than siting on your computer until 5am scrolling through domain registrars crappy ass domain look up webpanel just to know whats not taken yet. +## Lates 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) + ![CUCKKOM](https://git.teleco.ch/crt/hoardom.git/plain/doc/pics/image.png?h=main) diff --git a/dist/AppIcon.icns b/dist/AppIcon.icns new file mode 100644 index 0000000..ee6dafc Binary files /dev/null and b/dist/AppIcon.icns differ diff --git a/dist/AppIcon.ico b/dist/AppIcon.ico new file mode 100644 index 0000000..dc96ce9 Binary files /dev/null and b/dist/AppIcon.ico differ diff --git a/dist/hoardom.desktop b/dist/hoardom.desktop new file mode 100644 index 0000000..1c03702 --- /dev/null +++ b/dist/hoardom.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=hoardom +Comment=Domain hoarding made less painful +Exec=hoardom-app +Icon=hoardom +Terminal=false +Type=Application +Categories=Utility;Network; +Keywords=domain;dns;whois;rdap;tld; +StartupNotify=false diff --git a/dist/mac-launcher.sh b/dist/mac-launcher.sh new file mode 100644 index 0000000..c384cae --- /dev/null +++ b/dist/mac-launcher.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# hoardom app launcher - opens Terminal with TUI +SELF_DIR="$(cd "$(dirname "$0")" && pwd)" +HOARDOM="/usr/local/bin/hoardom" + +# try installed binary first, fall back to bundled copy +if [ ! -x "$HOARDOM" ]; then + HOARDOM="$SELF_DIR/hoardom-bin" +fi + +if [ -x "$HOARDOM" ]; then + osascript \ + -e 'tell application "Terminal"' \ + -e ' activate' \ + -e " do script \"'$HOARDOM' --tui\"" \ + -e 'end tell' +else + osascript -e 'display dialog "hoardom binary not found" buttons {"OK"} default button "OK"' +fi diff --git a/doc/CLI.md b/doc/CLI.md index 63ca92f..4f53102 100644 --- a/doc/CLI.md +++ b/doc/CLI.md @@ -48,7 +48,7 @@ hoardom coolproject mysite bigidea | Flag | Description | |-----------------------------|-------------------------------------------------------------------| -| `-j, --jobs=NUMBER` | Number of concurrent lookup requests (default: 1). 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. | +| `-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 | @@ -61,8 +61,8 @@ hoardom coolproject mysite bigidea ## Examples ```bash -# fast search with 8 concurrent requests -hoardom -j 8 coolproject +# fast search with 64 concurrent requests +hoardom -j 64 coolproject # search with the Country TLD list hoardom -l Country mysite diff --git a/doc/TUI.md b/doc/TUI.md index ff1a501..ce3ce3d 100644 --- a/doc/TUI.md +++ b/doc/TUI.md @@ -53,39 +53,39 @@ if `Clear on Search` is off in settings, results accumulate across searches. pre ## keyboard shortcuts ### global -| key | what | -|-----|------| -| `F1` | toggle help overlay | -| `F2` | open/close export popup | -| `Ctrl+C` | quit | -| `s` | cancel running search | -| `Tab` / `Shift+Tab` | cycle between panels | -| `Esc` | close help/dropdown, or clear selection in current panel | +| key | what | +|---------------------|----------------------------------------------------------| +| `F1` | toggle help overlay | +| `F2` | open/close export popup | +| `Ctrl+C` | quit | +| `s` | cancel running search | +| `Tab` / `Shift+Tab` | cycle between panels | +| `Esc` | close help/dropdown, or clear selection in current panel | ### search bar -| key | what | -|-----|------| -| `Enter` | start the search | -| typing | works normally when no search is running | -| `Home` / `End` | jump to start/end of input | +| key | what | +|----------------|------------------------------------------| +| `Enter` | start the search | +| typing | works normally when no search is running | +| `Home` / `End` | jump to start/end of input | ### results -| key | what | -|-----|------| -| `Up` / `Down` | navigate the list | -| `Enter` | add highlighted domain to favorites | -| mouse scroll | scroll through results | +| key | what | +|---------------|-------------------------------------| +| `Up` / `Down` | navigate the list | +| `Enter` | add highlighted domain to favorites | +| mouse scroll | scroll through results | ### favorites -| key | what | -|-----|------| -| `Up` / `Down` | navigate | +| key | what | +|------------------------|-----------------------------| +| `Up` / `Down` | navigate | | `Backspace` / `Delete` | remove the focused favorite | ### settings -| key | what | -|-----|------| -| `Up` / `Down` | move between settings rows | +| key | what | +|-------------------|-------------------------------------------------| +| `Up` / `Down` | move between settings rows | | `Enter` / `Space` | toggle checkboxes or open the TLD list dropdown | ### scratchpad @@ -108,6 +108,7 @@ theres 4 things in there: - **Show Unavailable** checkbox: toggles whether taken domains show with premium details in results - **Show Notes Panel** checkbox: toggles the scratchpad panel on the left - **Clear on Search** checkbox: if on, results get cleared before each new search. if off they pile up for the true hoarding feeling. +// todo add the job amount selector here too oh and settings auto save to config diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..4f3aefb --- /dev/null +++ b/src/app.rs @@ -0,0 +1,1155 @@ +// hoardom-app: native 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) +// +// built with: cargo build --features gui + +use eframe::egui::{self, Color32, FontId, Rect, Sense}; +use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use vte::{Params, Perform}; + +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +// ----- constants ----- + +const FONT_SIZE: f32 = 14.0; +const DEFAULT_COLS: u16 = 120; +const DEFAULT_ROWS: u16 = 35; + +const DEFAULT_FG: Color32 = Color32::from_rgb(204, 204, 204); +const DEFAULT_BG: Color32 = Color32::from_rgb(24, 24, 24); + +// ----- terminal colors ----- + +#[derive(Clone, Copy, PartialEq)] +enum TermColor { + Default, + Indexed(u8), + Rgb(u8, u8, u8), +} + +fn ansi_color(idx: u8) -> Color32 { + match idx { + 0 => Color32::from_rgb(0, 0, 0), + 1 => Color32::from_rgb(170, 0, 0), + 2 => Color32::from_rgb(0, 170, 0), + 3 => Color32::from_rgb(170, 85, 0), + 4 => Color32::from_rgb(0, 0, 170), + 5 => Color32::from_rgb(170, 0, 170), + 6 => Color32::from_rgb(0, 170, 170), + 7 => Color32::from_rgb(170, 170, 170), + 8 => Color32::from_rgb(85, 85, 85), + 9 => Color32::from_rgb(255, 85, 85), + 10 => Color32::from_rgb(85, 255, 85), + 11 => Color32::from_rgb(255, 255, 85), + 12 => Color32::from_rgb(85, 85, 255), + 13 => Color32::from_rgb(255, 85, 255), + 14 => Color32::from_rgb(85, 255, 255), + 15 => Color32::from_rgb(255, 255, 255), + // 6x6x6 color cube + 16..=231 => { + let idx = (idx - 16) as u16; + let ri = idx / 36; + let gi = (idx % 36) / 6; + let bi = idx % 6; + let v = |i: u16| -> u8 { + if i == 0 { 0 } else { 55 + i as u8 * 40 } + }; + Color32::from_rgb(v(ri), v(gi), v(bi)) + } + // grayscale ramp + 232..=255 => { + let g = 8 + (idx - 232) * 10; + Color32::from_rgb(g, g, g) + } + } +} + +fn resolve_color(c: TermColor, is_fg: bool) -> Color32 { + match c { + TermColor::Default => { + 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), + } +} + +// ----- terminal cell ----- + +#[derive(Clone, Copy)] +struct Cell { + ch: char, + fg: TermColor, + bg: TermColor, + bold: bool, + reverse: bool, +} + +impl Default for Cell { + fn default() -> Self { + Cell { + ch: ' ', + fg: TermColor::Default, + bg: TermColor::Default, + bold: false, + reverse: false, + } + } +} + +impl Cell { + fn resolved_fg(&self) -> Color32 { + if self.reverse { + resolve_color(self.bg, false) + } else { + let c = resolve_color(self.fg, true); + if self.bold { + // brighten bold text a bit + let [r, g, b, a] = c.to_array(); + Color32::from_rgba_premultiplied( + r.saturating_add(40), + g.saturating_add(40), + b.saturating_add(40), + a, + ) + } else { + c + } + } + } + + fn resolved_bg(&self) -> Color32 { + if self.reverse { + resolve_color(self.fg, true) + } else { + resolve_color(self.bg, false) + } + } +} + +// ----- terminal grid ----- + +struct TermGrid { + cells: Vec>, + rows: usize, + cols: usize, + cursor_row: usize, + cursor_col: usize, + cursor_visible: bool, + scroll_top: usize, + scroll_bottom: usize, + + // current drawing attributes + attr_fg: TermColor, + attr_bg: TermColor, + attr_bold: bool, + attr_reverse: bool, + + // saved cursor + saved_cursor: Option<(usize, usize)>, + + // alternate screen buffer + alt_saved: Option<(Vec>, 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 +} + +impl TermGrid { + fn new(rows: usize, cols: usize) -> Self { + TermGrid { + cells: vec![vec![Cell::default(); cols]; rows], + rows, + cols, + cursor_row: 0, + cursor_col: 0, + cursor_visible: true, + scroll_top: 0, + scroll_bottom: rows, + attr_fg: TermColor::Default, + attr_bg: TermColor::Default, + attr_bold: false, + attr_reverse: false, + saved_cursor: None, + alt_saved: None, + mouse_normal: false, + mouse_button: false, + mouse_any: false, + mouse_sgr: false, + } + } + + fn mouse_enabled(&self) -> bool { + self.mouse_normal || self.mouse_button || self.mouse_any + } + + fn resize(&mut self, new_rows: usize, new_cols: usize) { + if new_rows == self.rows && new_cols == self.cols { + return; + } + for row in &mut self.cells { + row.resize(new_cols, Cell::default()); + } + while self.cells.len() < new_rows { + self.cells.push(vec![Cell::default(); new_cols]); + } + self.cells.truncate(new_rows); + self.rows = new_rows; + self.cols = new_cols; + self.scroll_top = 0; + self.scroll_bottom = new_rows; + self.cursor_row = self.cursor_row.min(new_rows.saturating_sub(1)); + self.cursor_col = self.cursor_col.min(new_cols.saturating_sub(1)); + } + + fn reset_attrs(&mut self) { + self.attr_fg = TermColor::Default; + self.attr_bg = TermColor::Default; + self.attr_bold = false; + self.attr_reverse = false; + } + + fn put_char(&mut self, c: char) { + if self.cursor_col >= self.cols { + self.cursor_col = 0; + self.line_feed(); + } + if self.cursor_row < self.rows && self.cursor_col < self.cols { + self.cells[self.cursor_row][self.cursor_col] = Cell { + ch: c, + fg: self.attr_fg, + bg: self.attr_bg, + bold: self.attr_bold, + reverse: self.attr_reverse, + }; + } + self.cursor_col += 1; + } + + fn line_feed(&mut self) { + if self.cursor_row + 1 >= self.scroll_bottom { + self.scroll_up(); + } else { + self.cursor_row += 1; + } + } + + fn scroll_up(&mut self) { + if self.scroll_top < self.scroll_bottom && self.scroll_bottom <= self.rows { + self.cells.remove(self.scroll_top); + self.cells + .insert(self.scroll_bottom - 1, vec![Cell::default(); self.cols]); + } + } + + fn scroll_down(&mut self) { + if self.scroll_top < self.scroll_bottom && self.scroll_bottom <= self.rows { + self.cells.remove(self.scroll_bottom - 1); + self.cells + .insert(self.scroll_top, vec![Cell::default(); self.cols]); + } + } + + fn erase_display(&mut self, mode: u16) { + match mode { + 0 => { + // cursor to end + for c in self.cursor_col..self.cols { + self.cells[self.cursor_row][c] = Cell::default(); + } + for r in (self.cursor_row + 1)..self.rows { + for c in 0..self.cols { + self.cells[r][c] = Cell::default(); + } + } + } + 1 => { + // start to cursor + for r in 0..self.cursor_row { + for c in 0..self.cols { + self.cells[r][c] = Cell::default(); + } + } + for c in 0..=self.cursor_col.min(self.cols.saturating_sub(1)) { + self.cells[self.cursor_row][c] = Cell::default(); + } + } + 2 | 3 => { + // whole screen + for r in 0..self.rows { + for c in 0..self.cols { + self.cells[r][c] = Cell::default(); + } + } + } + _ => {} + } + } + + fn erase_line(&mut self, mode: u16) { + let row = self.cursor_row; + if row >= self.rows { + return; + } + match mode { + 0 => { + for c in self.cursor_col..self.cols { + self.cells[row][c] = Cell::default(); + } + } + 1 => { + for c in 0..=self.cursor_col.min(self.cols.saturating_sub(1)) { + self.cells[row][c] = Cell::default(); + } + } + 2 => { + for c in 0..self.cols { + self.cells[row][c] = Cell::default(); + } + } + _ => {} + } + } + + fn erase_chars(&mut self, n: usize) { + let row = self.cursor_row; + if row >= self.rows { + return; + } + for i in 0..n { + let c = self.cursor_col + i; + if c < self.cols { + self.cells[row][c] = Cell::default(); + } + } + } + + fn delete_chars(&mut self, n: usize) { + let row = self.cursor_row; + if row >= self.rows { + return; + } + for _ in 0..n { + if self.cursor_col < self.cols { + self.cells[row].remove(self.cursor_col); + self.cells[row].push(Cell::default()); + } + } + } + + fn insert_chars(&mut self, n: usize) { + let row = self.cursor_row; + if row >= self.rows { + return; + } + for _ in 0..n { + if self.cursor_col < self.cols { + self.cells[row].insert(self.cursor_col, Cell::default()); + self.cells[row].truncate(self.cols); + } + } + } + + fn insert_lines(&mut self, n: usize) { + for _ in 0..n { + if self.cursor_row < self.scroll_bottom { + if self.scroll_bottom <= self.rows { + self.cells.remove(self.scroll_bottom - 1); + } + self.cells + .insert(self.cursor_row, vec![Cell::default(); self.cols]); + } + } + } + + fn delete_lines(&mut self, n: usize) { + for _ in 0..n { + if self.cursor_row < self.scroll_bottom && self.cursor_row < self.rows { + self.cells.remove(self.cursor_row); + let insert_at = (self.scroll_bottom - 1).min(self.cells.len()); + self.cells + .insert(insert_at, vec![Cell::default(); self.cols]); + } + } + } + + fn enter_alt_screen(&mut self) { + self.alt_saved = Some((self.cells.clone(), self.cursor_row, self.cursor_col)); + self.erase_display(2); + self.cursor_row = 0; + self.cursor_col = 0; + } + + fn leave_alt_screen(&mut self) { + if let Some((cells, row, col)) = self.alt_saved.take() { + self.cells = cells; + self.cursor_row = row; + self.cursor_col = col; + } + } + + // SGR - set graphics rendition (colors and attributes) + fn sgr(&mut self, params: &[u16]) { + if params.is_empty() { + self.reset_attrs(); + return; + } + let mut i = 0; + while i < params.len() { + match params[i] { + 0 => self.reset_attrs(), + 1 => self.attr_bold = true, + 7 => self.attr_reverse = true, + 22 => self.attr_bold = false, + 27 => self.attr_reverse = false, + 30..=37 => self.attr_fg = TermColor::Indexed(params[i] as u8 - 30), + 38 => { + // extended fg color + if i + 2 < params.len() && params[i + 1] == 5 { + self.attr_fg = TermColor::Indexed(params[i + 2] as u8); + i += 2; + } else if i + 4 < params.len() && params[i + 1] == 2 { + self.attr_fg = TermColor::Rgb( + params[i + 2] as u8, + params[i + 3] as u8, + params[i + 4] as u8, + ); + i += 4; + } + } + 39 => self.attr_fg = TermColor::Default, + 40..=47 => self.attr_bg = TermColor::Indexed(params[i] as u8 - 40), + 48 => { + // extended bg color + if i + 2 < params.len() && params[i + 1] == 5 { + self.attr_bg = TermColor::Indexed(params[i + 2] as u8); + i += 2; + } else if i + 4 < params.len() && params[i + 1] == 2 { + self.attr_bg = TermColor::Rgb( + params[i + 2] as u8, + params[i + 3] as u8, + params[i + 4] as u8, + ); + i += 4; + } + } + 49 => self.attr_bg = TermColor::Default, + 90..=97 => self.attr_fg = TermColor::Indexed(params[i] as u8 - 90 + 8), + 100..=107 => self.attr_bg = TermColor::Indexed(params[i] as u8 - 100 + 8), + _ => {} + } + i += 1; + } + } + + fn handle_csi(&mut self, params: &[u16], intermediates: &[u8], action: char) { + // helper: get param with default + let p = |i: usize, def: u16| -> u16 { + params.get(i).copied().filter(|&v| v > 0).unwrap_or(def) + }; + + let private = intermediates.contains(&b'?'); + + match action { + 'A' => { + let n = p(0, 1) as usize; + self.cursor_row = self.cursor_row.saturating_sub(n); + } + 'B' => { + let n = p(0, 1) as usize; + self.cursor_row = (self.cursor_row + n).min(self.rows.saturating_sub(1)); + } + 'C' => { + let n = p(0, 1) as usize; + self.cursor_col = (self.cursor_col + n).min(self.cols.saturating_sub(1)); + } + 'D' => { + let n = p(0, 1) as usize; + self.cursor_col = self.cursor_col.saturating_sub(n); + } + 'H' | 'f' => { + // cursor position (1-based) + let row = p(0, 1) as usize; + let col = p(1, 1) as usize; + self.cursor_row = row.saturating_sub(1).min(self.rows.saturating_sub(1)); + self.cursor_col = col.saturating_sub(1).min(self.cols.saturating_sub(1)); + } + 'J' => self.erase_display(p(0, 0)), + 'K' => self.erase_line(p(0, 0)), + 'L' => self.insert_lines(p(0, 1) as usize), + 'M' => self.delete_lines(p(0, 1) as usize), + 'P' => self.delete_chars(p(0, 1) as usize), + 'X' => self.erase_chars(p(0, 1) as usize), + '@' => self.insert_chars(p(0, 1) as usize), + 'G' | '`' => { + let col = p(0, 1) as usize; + self.cursor_col = col.saturating_sub(1).min(self.cols.saturating_sub(1)); + } + 'd' => { + let row = p(0, 1) as usize; + self.cursor_row = row.saturating_sub(1).min(self.rows.saturating_sub(1)); + } + 'S' => { + for _ in 0..p(0, 1) { + self.scroll_up(); + } + } + 'T' => { + for _ in 0..p(0, 1) { + self.scroll_down(); + } + } + 'm' => { + if params.is_empty() { + self.sgr(&[0]); + } else { + self.sgr(params); + } + } + 'r' => { + let top = p(0, 1) as usize; + let bottom = p(1, self.rows as u16) as usize; + self.scroll_top = top.saturating_sub(1); + self.scroll_bottom = bottom.min(self.rows); + } + 's' => { + self.saved_cursor = Some((self.cursor_row, self.cursor_col)); + } + 'u' => { + if let Some((r, c)) = self.saved_cursor { + self.cursor_row = r.min(self.rows.saturating_sub(1)); + self.cursor_col = c.min(self.cols.saturating_sub(1)); + } + } + 'h' if private => { + for ¶m in params { + match param { + 25 => self.cursor_visible = true, + 1000 => self.mouse_normal = true, + 1002 => self.mouse_button = true, + 1003 => self.mouse_any = true, + 1006 => self.mouse_sgr = true, + 1049 => self.enter_alt_screen(), + _ => {} + } + } + } + 'l' if private => { + for ¶m in params { + match param { + 25 => self.cursor_visible = false, + 1000 => self.mouse_normal = false, + 1002 => self.mouse_button = false, + 1003 => self.mouse_any = false, + 1006 => self.mouse_sgr = false, + 1049 => self.leave_alt_screen(), + _ => {} + } + } + } + _ => {} + } + } +} + +// ----- vte perform implementation ----- + +impl Perform for TermGrid { + fn print(&mut self, c: char) { + self.put_char(c); + } + + fn execute(&mut self, byte: u8) { + match byte { + 0x08 => { + // backspace + self.cursor_col = self.cursor_col.saturating_sub(1); + } + 0x09 => { + // tab - next tab stop (every 8 cols) + self.cursor_col = ((self.cursor_col / 8) + 1) * 8; + if self.cursor_col >= self.cols { + self.cursor_col = self.cols.saturating_sub(1); + } + } + 0x0A | 0x0B | 0x0C => { + // line feed + self.line_feed(); + } + 0x0D => { + // carriage return + self.cursor_col = 0; + } + _ => {} + } + } + + fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { + if ignore { + return; + } + let flat: Vec = params.iter().map(|sub| sub[0]).collect(); + self.handle_csi(&flat, intermediates, action); + } + + fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { + if !intermediates.is_empty() { + return; + } + match byte { + b'7' => { + self.saved_cursor = Some((self.cursor_row, self.cursor_col)); + } + b'8' => { + if let Some((r, c)) = self.saved_cursor { + self.cursor_row = r.min(self.rows.saturating_sub(1)); + self.cursor_col = c.min(self.cols.saturating_sub(1)); + } + } + b'D' => self.line_feed(), + b'M' => { + // reverse index + if self.cursor_row == self.scroll_top { + self.scroll_down(); + } else { + self.cursor_row = self.cursor_row.saturating_sub(1); + } + } + _ => {} + } + } + + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} + fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {} + fn put(&mut self, _byte: u8) {} + fn unhook(&mut self) {} +} + +// ----- keyboard input mapping ----- + +// map egui keys to terminal escape sequences +fn special_key_bytes(key: &egui::Key, modifiers: &egui::Modifiers) -> Option> { + use egui::Key; + match key { + Key::ArrowUp => Some(b"\x1b[A".to_vec()), + Key::ArrowDown => Some(b"\x1b[B".to_vec()), + Key::ArrowRight => Some(b"\x1b[C".to_vec()), + Key::ArrowLeft => Some(b"\x1b[D".to_vec()), + Key::Home => Some(b"\x1b[H".to_vec()), + Key::End => Some(b"\x1b[F".to_vec()), + Key::PageUp => Some(b"\x1b[5~".to_vec()), + Key::PageDown => Some(b"\x1b[6~".to_vec()), + Key::Insert => Some(b"\x1b[2~".to_vec()), + Key::Delete => Some(b"\x1b[3~".to_vec()), + Key::Escape => Some(b"\x1b".to_vec()), + Key::Tab => { + if modifiers.shift { + Some(b"\x1b[Z".to_vec()) + } else { + Some(b"\x09".to_vec()) + } + } + Key::Backspace => Some(b"\x7f".to_vec()), + Key::Enter => Some(b"\x0d".to_vec()), + Key::F1 => Some(b"\x1bOP".to_vec()), + Key::F2 => Some(b"\x1bOQ".to_vec()), + Key::F3 => Some(b"\x1bOR".to_vec()), + Key::F4 => Some(b"\x1bOS".to_vec()), + Key::F5 => Some(b"\x1b[15~".to_vec()), + Key::F6 => Some(b"\x1b[17~".to_vec()), + Key::F7 => Some(b"\x1b[18~".to_vec()), + Key::F8 => Some(b"\x1b[19~".to_vec()), + Key::F9 => Some(b"\x1b[20~".to_vec()), + Key::F10 => Some(b"\x1b[21~".to_vec()), + Key::F11 => Some(b"\x1b[23~".to_vec()), + Key::F12 => Some(b"\x1b[24~".to_vec()), + _ => None, + } +} + +// ctrl+letter -> control character byte +fn ctrl_key_byte(key: &egui::Key) -> Option { + use egui::Key; + match key { + Key::A => Some(0x01), + Key::B => Some(0x02), + Key::C => Some(0x03), + Key::D => Some(0x04), + Key::E => Some(0x05), + Key::F => Some(0x06), + Key::G => Some(0x07), + Key::H => Some(0x08), + Key::I => Some(0x09), + Key::J => Some(0x0A), + Key::K => Some(0x0B), + Key::L => Some(0x0C), + Key::M => Some(0x0D), + Key::N => Some(0x0E), + Key::O => Some(0x0F), + Key::P => Some(0x10), + Key::Q => Some(0x11), + Key::R => Some(0x12), + Key::S => Some(0x13), + Key::T => Some(0x14), + Key::U => Some(0x15), + Key::V => Some(0x16), + Key::W => Some(0x17), + Key::X => Some(0x18), + Key::Y => Some(0x19), + Key::Z => Some(0x1A), + _ => None, + } +} + +// ----- the egui app ----- + +struct HoardomApp { + grid: Arc>, + pty_writer: Mutex>, + pty_master: Box, + child_exited: Arc, + cell_width: f32, + cell_height: f32, + current_cols: u16, + current_rows: u16, + last_mouse_button: Option, // track held mouse button for drag/release +} + +impl eframe::App for HoardomApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // bail if the child process is gone + if self.child_exited.load(Ordering::Relaxed) { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + return; + } + + // measure cell dimensions on first frame (cant do it in creation callback) + if self.cell_width == 0.0 { + let (cw, ch) = ctx.fonts(|f| { + let fid = FontId::monospace(FONT_SIZE); + let galley = f.layout_no_wrap("M".into(), fid.clone(), DEFAULT_FG); + let row_h = f.row_height(&fid); + (galley.rect.width(), row_h) + }); + self.cell_width = cw; + self.cell_height = ch; + } + + // handle keyboard input + ctx.input(|input| { + for event in &input.events { + match event { + egui::Event::Text(text) => { + // only pass printable chars (specials handled via Key events) + let filtered: String = text.chars().filter(|c| !c.is_control()).collect(); + if !filtered.is_empty() { + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(filtered.as_bytes()); + } + } + } + egui::Event::Key { + key, + pressed: true, + modifiers, + .. + } => { + if modifiers.ctrl || modifiers.mac_cmd { + if let Some(byte) = ctrl_key_byte(key) { + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(&[byte]); + } + } + } else if let Some(bytes) = special_key_bytes(key, modifiers) { + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(&bytes); + } + } + } + _ => {} + } + } + }); + + // handle mouse input + self.handle_mouse(ctx); + + // check if window was resized, update pty dimensions + let avail = ctx.available_rect(); + if self.cell_width > 0.0 && self.cell_height > 0.0 { + let new_cols = (avail.width() / self.cell_width).floor() as u16; + let new_rows = (avail.height() / self.cell_height).floor() as u16; + let new_cols = new_cols.max(20); + let new_rows = new_rows.max(10); + + if new_cols != self.current_cols || new_rows != self.current_rows { + self.current_cols = new_cols; + self.current_rows = new_rows; + let _ = self.pty_master.resize(PtySize { + rows: new_rows, + cols: new_cols, + pixel_width: 0, + pixel_height: 0, + }); + if let Ok(mut grid) = self.grid.lock() { + grid.resize(new_rows as usize, new_cols as usize); + } + } + } + + // render the terminal grid + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(DEFAULT_BG)) + .show(ctx, |ui| { + self.render_grid(ui); + }); + + ctx.request_repaint_after(Duration::from_millis(16)); + } +} + +impl HoardomApp { + // translate egui pointer events to terminal mouse sequences + fn handle_mouse(&mut self, ctx: &egui::Context) { + let (mouse_enabled, use_sgr) = { + match self.grid.lock() { + Ok(g) => (g.mouse_enabled(), g.mouse_sgr), + Err(_) => return, + } + }; + if !mouse_enabled { + return; + } + + let cw = self.cell_width; + let ch = self.cell_height; + if cw <= 0.0 || ch <= 0.0 { + return; + } + + let avail = ctx.available_rect(); + + ctx.input(|input| { + if let Some(pos) = input.pointer.latest_pos() { + let col = ((pos.x - avail.min.x) / cw).floor() as i32; + let row = ((pos.y - avail.min.y) / ch).floor() as i32; + let col = col.max(0) as u16; + let row = row.max(0) as u16; + + // scroll events + let scroll_y = input.raw_scroll_delta.y; + if scroll_y != 0.0 { + let button: u8 = if scroll_y > 0.0 { 64 } else { 65 }; + let seq = if use_sgr { + format!("\x1b[<{};{};{}M", button, col + 1, row + 1) + } else { + let cb = (button + 32) as char; + let cx = (col + 33).min(255) as u8 as char; + let cy = (row + 33).min(255) as u8 as char; + format!("\x1b[M{}{}{}", cb, cx, cy) + }; + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(seq.as_bytes()); + } + } + + // button press + if input.pointer.any_pressed() { + let button: u8 = if input.pointer.button_pressed(egui::PointerButton::Primary) { + 0 + } else if input.pointer.button_pressed(egui::PointerButton::Middle) { + 1 + } else if input.pointer.button_pressed(egui::PointerButton::Secondary) { + 2 + } else { + 0 + }; + self.last_mouse_button = Some(button); + let seq = if use_sgr { + format!("\x1b[<{};{};{}M", button, col + 1, row + 1) + } else { + let cb = (button + 32) as char; + let cx = (col + 33).min(255) as u8 as char; + let cy = (row + 33).min(255) as u8 as char; + format!("\x1b[M{}{}{}", cb, cx, cy) + }; + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(seq.as_bytes()); + } + } + + // button release + if input.pointer.any_released() { + let button = self.last_mouse_button.unwrap_or(0); + self.last_mouse_button = None; + let seq = if use_sgr { + format!("\x1b[<{};{};{}m", button, col + 1, row + 1) + } else { + let cb = (3u8 + 32) as char; // release = button 3 in normal mode + let cx = (col + 33).min(255) as u8 as char; + let cy = (row + 33).min(255) as u8 as char; + format!("\x1b[M{}{}{}", cb, cx, cy) + }; + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(seq.as_bytes()); + } + } + + // drag / motion + if input.pointer.is_moving() && self.last_mouse_button.is_some() { + let button = self.last_mouse_button.unwrap_or(0) + 32; // motion flag + let seq = if use_sgr { + format!("\x1b[<{};{};{}M", button, col + 1, row + 1) + } else { + let cb = (button + 32) as char; + let cx = (col + 33).min(255) as u8 as char; + let cy = (row + 33).min(255) as u8 as char; + format!("\x1b[M{}{}{}", cb, cx, cy) + }; + if let Ok(mut w) = self.pty_writer.lock() { + let _ = w.write_all(seq.as_bytes()); + } + } + } + }); + } + + fn render_grid(&self, ui: &mut egui::Ui) { + let grid = match self.grid.lock() { + Ok(g) => g, + Err(_) => return, + }; + + let painter = ui.painter(); + let rect = ui.available_rect_before_wrap(); + let cw = self.cell_width; + let ch = self.cell_height; + + // draw each row - render character by character at exact cell positions + // to keep backgrounds and text perfectly aligned + for row in 0..grid.rows { + let y = rect.min.y + row as f32 * ch; + + // draw background spans (batch consecutive same-bg cells) + let mut bg_start = 0usize; + let mut current_bg = grid.cells[row][0].resolved_bg(); + + for col in 0..=grid.cols { + let cell_bg = if col < grid.cols { + grid.cells[row][col].resolved_bg() + } else { + Color32::TRANSPARENT // sentinel to flush last span + }; + + if cell_bg != current_bg || col == grid.cols { + // draw the background span + if current_bg != DEFAULT_BG { + let x0 = rect.min.x + bg_start as f32 * cw; + let x1 = rect.min.x + col as f32 * cw; + painter.rect_filled( + Rect::from_min_max(egui::pos2(x0, y), egui::pos2(x1, y + ch)), + 0.0, + current_bg, + ); + } + bg_start = col; + current_bg = cell_bg; + } + } + + // draw text - render each cell at its exact x position + // this prevents sub-pixel drift that causes bg/text misalignment + for col in 0..grid.cols { + let cell = &grid.cells[row][col]; + if cell.ch == ' ' || cell.ch == '\0' { + continue; + } + let x = rect.min.x + col as f32 * cw; + let fg = cell.resolved_fg(); + let mut buf = [0u8; 4]; + let s = cell.ch.encode_utf8(&mut buf); + painter.text( + egui::pos2(x, y), + egui::Align2::LEFT_TOP, + s, + FontId::monospace(FONT_SIZE), + fg, + ); + } + } + + // draw cursor + if grid.cursor_visible && grid.cursor_row < grid.rows && grid.cursor_col < grid.cols { + let cx = rect.min.x + grid.cursor_col as f32 * cw; + let cy = rect.min.y + grid.cursor_row as f32 * ch; + painter.rect_filled( + Rect::from_min_size(egui::pos2(cx, cy), egui::vec2(cw, ch)), + 0.0, + Color32::from_rgba_premultiplied(180, 180, 180, 100), + ); + } + + // reserve the space so egui knows we used it + ui.allocate_exact_size( + egui::vec2(grid.cols as f32 * cw, grid.rows as f32 * ch), + Sense::hover(), + ); + } +} + +// ----- find the hoardom binary ----- + +fn find_hoardom() -> PathBuf { + // check same directory as ourselves + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + // check for hoardom next to us + let candidate = dir.join("hoardom"); + if candidate.exists() && candidate != exe { + return candidate; + } + // in a mac .app bundle the binary might be named differently + let candidate = dir.join("hoardom-bin"); + if candidate.exists() { + return candidate; + } + } + } + // fall back to PATH + PathBuf::from("hoardom") +} + +// ----- main ----- + +fn main() -> eframe::Result<()> { + let hoardom_bin = find_hoardom(); + + // setup pty + let pty_system = native_pty_system(); + let pair = pty_system + .openpty(PtySize { + rows: DEFAULT_ROWS, + cols: DEFAULT_COLS, + pixel_width: 0, + pixel_height: 0, + }) + .expect("failed to open pty"); + + // spawn hoardom --tui in the pty + let mut cmd = CommandBuilder::new(&hoardom_bin); + cmd.arg("--tui"); + cmd.env("TERM", "xterm-256color"); + + let mut child = pair + .slave + .spawn_command(cmd) + .unwrap_or_else(|e| panic!("failed to spawn {:?}: {}", hoardom_bin, e)); + + // close the slave end in the parent so pty gets proper eof + drop(pair.slave); + + let reader = pair + .master + .try_clone_reader() + .expect("failed to clone pty reader"); + let writer = pair + .master + .take_writer() + .expect("failed to take pty writer"); + + let grid = Arc::new(Mutex::new(TermGrid::new( + DEFAULT_ROWS as usize, + DEFAULT_COLS as usize, + ))); + let child_exited = Arc::new(AtomicBool::new(false)); + + // egui context holder so the reader thread can request repaints + let ctx_holder: Arc>> = Arc::new(Mutex::new(None)); + + // reader thread: reads pty output and feeds it through the vt parser + let grid_clone = grid.clone(); + let exited_clone = child_exited.clone(); + let ctx_clone = ctx_holder.clone(); + thread::spawn(move || { + let mut parser = vte::Parser::new(); + let mut reader = reader; + let mut buf = [0u8; 8192]; + loop { + match reader.read(&mut buf) { + Ok(0) | Err(_) => { + exited_clone.store(true, Ordering::Relaxed); + if let Ok(lock) = ctx_clone.lock() { + if let Some(ctx) = lock.as_ref() { + ctx.request_repaint(); + } + } + break; + } + Ok(n) => { + if let Ok(mut g) = grid_clone.lock() { + parser.advance(&mut *g, &buf[..n]); + } + if let Ok(lock) = ctx_clone.lock() { + if let Some(ctx) = lock.as_ref() { + ctx.request_repaint(); + } + } + } + } + } + }); + + // child reaper thread + let exited_clone2 = child_exited.clone(); + thread::spawn(move || { + let _ = child.wait(); + exited_clone2.store(true, Ordering::Relaxed); + }); + + // calculate initial window size from cell dimensions + // (rough estimate, refined on first frame) + let est_width = DEFAULT_COLS as f32 * 8.5 + 20.0; + let est_height = DEFAULT_ROWS as f32 * 18.0 + 20.0; + + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("hoardom") + .with_inner_size([est_width, est_height]) + .with_min_inner_size([300.0, 200.0]), + ..Default::default() + }; + + eframe::run_native( + "hoardom", + options, + Box::new(move |cc| { + // store the egui context for the reader thread + if let Ok(mut holder) = ctx_holder.lock() { + *holder = Some(cc.egui_ctx.clone()); + } + + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + + 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_height: 0.0, + current_cols: DEFAULT_COLS, + current_rows: DEFAULT_ROWS, + last_mouse_button: None, + })) + }), + ) +} diff --git a/src/cli.rs b/src/cli.rs index 297e4e3..e4f905b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -135,13 +135,16 @@ Mode : --tui Easy to use Terminal based Graphical user interface Basics : --e --environement=PATH Define where .hoardom folder should be - Defaults to /home/USER/.hoardom/ - Stores settings, imported lists, favs, cache etc. -a --all Show all in list even when unavailable - (Unless changed after launch in TUI mode) +-H --fullhelp Show full help --H --fullhelp Show full help", +Example usage : +hoardom --tui Launch Terminal UI. +hoardom idea.com See if idea.com is available +hoardom -a idea1 idea2 See Table of available domains starting with that + + +", env!("CARGO_PKG_VERSION") ); } @@ -157,18 +160,14 @@ Mode : --tui Easy to use Terminal based Graphical user interface Basics : --e --environement=PATH Define where .hoardom folder should be - Defaults to /home/USER/.hoardom/ - Stores settings, imported lists, favs, cache etc. -a --all Show all in list even when unavailable - (Unless changed after launch in TUI mode) +-c --csv=PATH Out in CSV, Path is optional +-l --list=LIST Built in TLD Lists are : {} Advanced : --c --csv=PATH Out in CSV,Path is optional - if path isnt given will be printed to terminal with no logs --l --list=LIST Built in TLD Lists are : {} - Selects which list is applied - (Unless changed after launch in TUI mode) +-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 -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 @@ -182,13 +181,11 @@ Advanced : Various : -j --jobs=NUMBER Number of concurrent lookup requests - How many TLDs to look up at the same time (default: 1) + How many TLDs to look up at the same time (default: 32) -D --delay=DELAY Set the global delay in Seconds between lookup requests -R --retry=NUMBER Retry NUMBER amount of times if domain lookup errors out -V --verbose Verbose output for debugging --A --autosearch=FILE Search for names/domains in text file one domain per new line, - lines starting with invalid character for a domain are ignored - (allows for commenting) +-A --autosearch=FILE Search for names/domains in text file one domain per new line -C --no-color Use a monochrome color scheme -U --no-unicode Do not use unicode only plain ASCII -M --no-mouse Disable the mouse integration for TUI -- cgit v1.2.3-70-g09d2