diff --git a/Cargo.lock b/Cargo.lock index 1e072e4..1a05bec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -37,6 +49,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "arrayvec" version = "0.7.4" @@ -81,6 +99,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bitvec" version = "1.0.1" @@ -156,6 +180,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.90" @@ -174,12 +213,50 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "configparser" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec6d3da8e550377a85339063af6e3735f4b1d9392108da4e083a1b3b9820288" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -191,7 +268,9 @@ name = "dns_lookup_project" version = "0.1.0" dependencies = [ "configparser", + "crossterm", "hickory-resolver", + "ratatui", "regex", "whois-rust", ] @@ -208,6 +287,12 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "endian-type" version = "0.1.2" @@ -330,7 +415,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -338,6 +423,10 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] [[package]] name = "heck" @@ -437,6 +526,12 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "ipconfig" version = "0.3.2" @@ -455,6 +550,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -483,6 +587,21 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -520,6 +639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -590,6 +710,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -736,13 +862,33 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -844,6 +990,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.17" @@ -899,6 +1051,36 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -930,6 +1112,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stability" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.53", +] + [[package]] name = "syn" version = "1.0.109" @@ -1134,6 +1354,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "url" version = "2.5.0" @@ -1397,3 +1629,23 @@ checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] diff --git a/Cargo.toml b/Cargo.toml index e8aa0fd..82d2b7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" [dependencies] configparser = "3.0.4" +crossterm = "0.27.0" hickory-resolver = { version = "0.24.0" } +ratatui = "0.26.1" regex = "1.10.3" whois-rust = "1.6.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..83e20d9 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,29 @@ +pub enum AppRenderDir { + Vertical, + Horizontal, +} + +pub enum CurrentState { + Lookup, + Menu, +} + +pub struct App { + pub domain_input: String, + pub whois_info: Vec, + pub dns_info: Vec, + pub render_direction: AppRenderDir, + pub current_state: CurrentState, +} + +impl App { + pub fn new() -> App { + App { + domain_input: String::new(), + whois_info: vec![], + dns_info: vec![], + render_direction: AppRenderDir::Horizontal, + current_state: CurrentState::Lookup, + } + } +} diff --git a/src/main.rs b/src/main.rs index 19e738b..94b639c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,38 +7,107 @@ use crate::config::Config; mod whois; use crate::whois::WhoisData; -use std::io; -use std::io::Write; +mod app; +use crate::app::*; -fn main() { - // let mut running = true; +mod ui; +use crate::ui::*; + +use crossterm::event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, +}; +use crossterm::execute; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use ratatui::backend::{Backend, CrosstermBackend}; +use ratatui::Terminal; +use std::error::Error; +use std::io::Write; +use std::io::{self, stderr}; + +fn main() -> Result<(), Box> { + enable_raw_mode()?; + let mut stderr = stderr(); + execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; + + let backend = CrosstermBackend::new(stderr); + let mut terminal = Terminal::new(backend)?; + + // Initialise the App and config + let mut app = App::new(); let config = Config::from_file("test.ini".to_string()); + + // Run the app + let res = run_app(&mut terminal, &mut app, &config); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + // Clear screen + // print!("{}[2J", 27 as char); + // + // loop { + // let mut domain = String::new(); + // print!("Enter domain name: "); + // io::stdout().flush().expect("Failed to flush buffer"); + // match io::stdin().read_line(&mut domain) { + // Ok(_n) => { + // domain = String::from(domain.trim()); + // match domain.as_ref() { + // "q" => break, + // "" => continue, + // _ => { + // let mut dns = Domain::new(domain.clone()); + // dns.apply_config(&config); + // dns.lookup_all_records(); + // let whois = WhoisData::new(domain); + // println!("DNS: {}\n\n\nWhois: {}", dns, whois); // Replace this with fancy UI render update command + // } + // } + // } + // Err(err) => { + // println!("{}", err); + // } + // } + // } + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + app: &mut App, + config: &Config, +) -> io::Result { loop { - let mut domain = String::new(); - print!("Enter domain name: "); - io::stdout().flush().expect("Failed to flush buffer"); - match io::stdin().read_line(&mut domain) { - Ok(_n) => { - if domain.trim() == "q" { - break; - } - let mut dns = Domain::new(String::from(domain.trim())); - dns.apply_config(&config); - dns.lookup_all_records(); - let whois = WhoisData::new(String::from(domain.trim())); - println!("DNS: {}\n\n\nWhois: {}", dns, whois); + terminal.draw(|f| ui(f, app))?; + + if let Event::Key(key) = event::read()? { + if key.kind == event::KeyEventKind::Release { + continue; } - Err(err) => { - println!("{}", err); + match app.current_state { + CurrentState::Lookup => match key.code { + KeyCode::Esc => return Ok(false), + KeyCode::Backspace => { + app.domain_input.pop(); + } + KeyCode::Char(char) => { + app.domain_input.push(char); + } + KeyCode::Enter => { + // This will do the lookup and populate the UI with the info + let mut domain = Domain::new(app.domain_input.clone()); + domain.lookup_all_records(); + } + _ => {} + }, + CurrentState::Menu => {} } } } - // let mut test = Domain::new("ventraip.com.au".to_string()); - // let config = Config::from_file("test.ini".to_string()); - // test.apply_config(&config); - - // test.lookup_all_records(); - // let whois = WhoisData::new(test.domain_name.clone()); - - // println!("Domain: {}\n{}", test.domain_name.clone(), whois); } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..f37367c --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,126 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style, Styled}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, + Frame, +}; + +use crate::{ + app::{App, AppRenderDir}, + CurrentState, +}; + +pub fn ui(f: &mut Frame, app: &App) { + match app.current_state { + CurrentState::Lookup => { + match app.render_direction { + /* + Initial contianer is vertical but we have the large center Rect that we will split horizontally + Constraint 1: 30px title bar + Constraint 2: Rest of unused space + Constraint 3: 30px input / key hints + + Constraint 2.a: 50% of usable space for whois + Constraint 2.b: 50% of usable space for dns + */ + AppRenderDir::Horizontal => { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + .split(f.size()); + + let title_block = Block::default() + .borders(Borders::ALL) + .style(Style::default()); + + let title = Paragraph::new(Span::styled( + "Dns Lookup tool", + Style::default().fg(Color::White), + )) + .block(title_block); + + f.render_widget(title, chunks[0]); + + let data_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(chunks[1]); + + let whois_block = Block::default() + .borders(Borders::RIGHT) + .style(Style::default()); + + let whois_text = Paragraph::new(Text::styled( + "Whois data in +style +with +multiline support", + Style::default().fg(Color::Green), + )) + .block(whois_block); + f.render_widget(whois_text, data_layout[0]); + + let dns_block = Block::default().style(Style::default()); + let dns_text = Paragraph::new(Text::styled( + "Dns records +also +with +multiline +support", + Style::default().fg(Color::Red), + )) + .block(dns_block); + f.render_widget(dns_text, data_layout[1]); + + let footer_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(chunks[2]); + + let domain_block = Block::new().borders(Borders::ALL).style(Style::default()); + let mut domain_string = String::from("Domain: "); + domain_string.push_str(&app.domain_input); + let domain_text = Paragraph::new(Text::styled( + domain_string, + Style::default().fg(Color::Blue), + )) + .block(domain_block); + f.render_widget(domain_text, footer_chunks[0]); + + let key_hint_block = Block::default() + .borders(Borders::ALL) + .style(Style::default()); + let key_hint = Paragraph::new(Text::styled( + "[ESC] Quit / [Enter] Check domain", + Style::default().fg(Color::Red), + )) + .block(key_hint_block); + f.render_widget(key_hint, footer_chunks[1]); + } + /* + Constraint 1: 30px title bar + Constraint 2: 50% of unused space for whois + Constraint 3: 50% of unused space for dns + Constraint 4: 30px input / key hints + */ + AppRenderDir::Vertical => { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Max(30), + Constraint::Percentage(50), + Constraint::Percentage(50), + Constraint::Max(30), + ]) + .split(f.size()); + } + } + } + CurrentState::Menu => {} + } +}