diff --git a/kinode/src/kernel/mod.rs b/kinode/src/kernel/mod.rs index c7bc759ba..672d25958 100644 --- a/kinode/src/kernel/mod.rs +++ b/kinode/src/kernel/mod.rs @@ -710,6 +710,9 @@ pub async fn kernel( let mut process_handles: ProcessHandles = HashMap::new(); let mut is_debug: bool = false; + // this flag starts as true, and terminal will alert us if we can + // skip sending prints for every event. + let mut print_full_event_loop: bool = true; let mut reboot_processes: Vec<(t::ProcessId, StartProcessMetadata, Vec)> = vec![]; // filter out OnExit::None processes from process_map @@ -870,9 +873,17 @@ pub async fn kernel( loop { tokio::select! { // debug mode toggle: when on, this loop becomes a manual step-through - debug = recv_debug_in_loop.recv() => { - if let Some(t::DebugCommand::Toggle) = debug { - is_debug = !is_debug; + Some(debug_command) = recv_debug_in_loop.recv() => { + match debug_command { + t::DebugCommand::ToggleStepthrough => { + is_debug = !is_debug; + }, + t::DebugCommand::Step => { + // can't step here, must be in stepthrough-mode + }, + t::DebugCommand::ToggleEventLoop => { + print_full_event_loop = !print_full_event_loop; + } } }, // network error message receiver: handle `timeout` and `offline` errors @@ -1042,17 +1053,20 @@ pub async fn kernel( while is_debug { let debug = recv_debug_in_loop.recv().await.expect("event loop: debug channel died"); match debug { - t::DebugCommand::Toggle => is_debug = !is_debug, + t::DebugCommand::ToggleStepthrough => is_debug = !is_debug, t::DebugCommand::Step => break, + t::DebugCommand::ToggleEventLoop => print_full_event_loop = !print_full_event_loop, } } // display every single event when verbose - let _ = send_to_terminal.send( + if print_full_event_loop { + let _ = send_to_terminal.send( t::Printout { verbosity: 3, content: format!("{kernel_message}") } ).await; + } if our.name != kernel_message.target.node { send_to_net.send(kernel_message).await.expect("fatal: net module died"); diff --git a/kinode/src/main.rs b/kinode/src/main.rs index 43c8e02f6..ac4a4b671 100644 --- a/kinode/src/main.rs +++ b/kinode/src/main.rs @@ -455,17 +455,8 @@ async fn main() { // abort all remaining tasks tasks.shutdown().await; - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - crossterm::execute!( - stdout, - crossterm::event::DisableBracketedPaste, - crossterm::terminal::SetTitle(""), - crossterm::style::SetForegroundColor(crossterm::style::Color::Red), - crossterm::style::Print(format!("\r\n{quit_msg}\r\n")), - crossterm::style::ResetColor, - ) - .expect("failed to clean up terminal visual state! your terminal window might be funky now"); + // reset all modified aspects of terminal -- clean ourselves up + terminal::utils::cleanup(&quit_msg); } async fn set_http_server_port(set_port: Option<&u16>) -> u16 { @@ -755,12 +746,9 @@ async fn serve_register_fe( } }; - tokio::fs::write( - format!("{}/.keys", home_directory_path), - encoded_keyfile.clone(), - ) - .await - .unwrap(); + tokio::fs::write(format!("{}/.keys", home_directory_path), &encoded_keyfile) + .await + .unwrap(); let _ = kill_tx.send(true); @@ -831,12 +819,9 @@ async fn login_with_password( .await .expect("information used to boot does not match information onchain"); - tokio::fs::write( - format!("{}/.keys", home_directory_path), - disk_keyfile.clone(), - ) - .await - .unwrap(); + tokio::fs::write(format!("{}/.keys", home_directory_path), &disk_keyfile) + .await + .unwrap(); (our, disk_keyfile, k) } diff --git a/kinode/src/register.rs b/kinode/src/register.rs index 863a02998..db512a6e0 100644 --- a/kinode/src/register.rs +++ b/kinode/src/register.rs @@ -36,6 +36,7 @@ sol! { function key(bytes32) external view returns (bytes32); function nodes(bytes32) external view returns (address, uint96); function ip(bytes32) external view returns (uint128, uint16, uint16, uint16, uint16); + function routers(bytes32) external view returns (bytes32[]); } /// Serve the registration page and receive POSTs and PUTs from it @@ -702,6 +703,7 @@ pub async fn assign_routing( let namehash = FixedBytes::<32>::from_slice(&keygen::namehash(&our.name)); let ip_call = ipCall { _0: namehash }.abi_encode(); let key_call = keyCall { _0: namehash }.abi_encode(); + let router_call = routersCall { _0: namehash }.abi_encode(); let tx_input = TransactionInput::new(Bytes::from(ip_call)); let tx = TransactionRequest::default() .to(kns_address) @@ -731,6 +733,18 @@ pub async fn assign_routing( )); } + let router_tx_input = TransactionInput::new(Bytes::from(router_call)); + let router_tx = TransactionRequest::default() + .to(kns_address) + .input(router_tx_input); + + let Ok(routers) = provider.call(&router_tx).await else { + return Err(anyhow::anyhow!("Failed to fetch node routers from PKI")); + }; + let Ok(routers) = >>::abi_decode(&routers, false) else { + return Err(anyhow::anyhow!("Failed to decode node routers from PKI")); + }; + let node_ip = format!( "{}.{}.{}.{}", (ip >> 24) & 0xFF, @@ -739,6 +753,10 @@ pub async fn assign_routing( ip & 0xFF ); + if !routers.is_empty() { + // indirect node + return Ok(()); + } if node_ip != *"0.0.0.0" && (ws != 0 || tcp != 0) { // direct node let mut ports = std::collections::BTreeMap::new(); @@ -763,8 +781,6 @@ pub async fn assign_routing( ports.insert("tcp".to_string(), tcp); } our.routing = NodeRouting::Direct { ip: node_ip, ports }; - } else { - // indirect node } Ok(()) } diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 4adcb0505..9c5a9eb1b 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -1,41 +1,23 @@ -use anyhow::Result; use chrono::{Datelike, Local, Timelike}; use crossterm::{ cursor, - event::{ - DisableBracketedPaste, EnableBracketedPaste, Event, EventStream, KeyCode, KeyEvent, - KeyModifiers, - }, + event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, execute, style, style::Print, - terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, + terminal::{self, ClearType}, }; use futures::{future::FutureExt, StreamExt}; -use std::fs::{read_to_string, OpenOptions}; -use std::io::{stdout, BufWriter, Write}; +use lib::types::core::{ + Address, DebugCommand, DebugSender, Identity, KernelMessage, Message, MessageSender, + PrintReceiver, PrintSender, Printout, Request, TERMINAL_PROCESS_ID, +}; +use std::{ + fs::{read_to_string, OpenOptions}, + io::{BufWriter, Write}, +}; use tokio::signal::unix::{signal, SignalKind}; -use lib::types::core::*; - -mod utils; - -struct RawMode; -impl RawMode { - fn new() -> anyhow::Result { - enable_raw_mode()?; - Ok(RawMode) - } -} -impl Drop for RawMode { - fn drop(&mut self) { - match disable_raw_mode() { - Ok(_) => {} - Err(e) => { - println!("terminal: failed to disable raw mode: {e:?}\r"); - } - } - } -} +pub mod utils; /* * terminal driver @@ -50,122 +32,50 @@ pub async fn terminal( mut print_rx: PrintReceiver, is_detached: bool, mut verbose_mode: u8, -) -> Result<()> { - let mut stdout = stdout(); - execute!( - stdout, - EnableBracketedPaste, - terminal::SetTitle(format!("{}", our.name)) - )?; - - let (mut win_cols, mut win_rows) = terminal::size().unwrap(); - // print initial splash screen, large if there's room, small otherwise - if win_cols >= 90 { - execute!( - stdout, - style::SetForegroundColor(style::Color::Magenta), - Print(format!( - r#" - .` - `@@,, ,* 888 d8P d8b 888 - `@%@@@, ,~-##` 888 d8P Y8P 888 - ~@@#@%#@@, ##### 888 d8P 888 - ~-%######@@@, ##### 888d88K 888 88888b. .d88b. .d88888 .d88b. - -%%#######@#####, 8888888b 888 888 "88b d88""88b d88" 888 d8P Y8b - ~^^%##########@ 888 Y88b 888 888 888 888 888 888 888 88888888 - >^#########@ 888 Y88b 888 888 888 Y88..88P Y88b 888 Y8b. - `>#######` 888 Y88b 888 888 888 "Y88P" "Y88888 "Y8888 - .>######% - /###%^#% {} ({}) - /##%@# ` runtime version {} - ./######` a general purpose sovereign cloud computer - /.^`.#^#^` - ` ,#`#`#, - ,/ /` ` - .*` - networking public key: {} - "#, - our.name, - if our.is_direct() { - "direct" - } else { - "indirect" - }, - version, - our.networking_key, - )), - style::ResetColor - )?; - } else { - execute!( - stdout, - style::SetForegroundColor(style::Color::Magenta), - Print(format!( - r#" - 888 d8P d8b 888 - 888 d8P Y8P 888 - 888 d8P 888 - 888d88K 888 88888b. .d88b. .d88888 .d88b. - 8888888b 888 888 "88b d88""88b d88" 888 d8P Y8b - 888 Y88b 888 888 888 888 888 888 888 88888888 - 888 Y88b 888 888 888 Y88..88P Y88b 888 Y8b. - 888 Y88b 888 888 888 "Y88P" "Y88888 "Y8888 +) -> anyhow::Result<()> { + let (stdout, _maybe_raw_mode) = utils::startup(&our, version, is_detached)?; - {} ({}) - version {} - a general purpose sovereign cloud computer - net pubkey: {} - "#, - our.name, - if our.is_direct() { - "direct" - } else { - "indirect" - }, - version, - our.networking_key, - )), - style::ResetColor - )?; - } + // mutable because we adjust them on window resize events + let (mut win_cols, mut win_rows) = + crossterm::terminal::size().expect("terminal: couldn't fetch size"); - let _raw_mode = if is_detached { - None - } else { - Some(RawMode::new()?) - }; - - let mut reader = EventStream::new(); let mut current_line = format!("{} > ", our.name); let prompt_len: usize = our.name.len() + 3; - let mut cursor_col: u16 = prompt_len.try_into().unwrap(); + let mut cursor_col: u16 = prompt_len as u16; let mut line_col: usize = cursor_col as usize; + let mut in_step_through: bool = false; + let mut search_mode: bool = false; let mut search_depth: usize = 0; + let mut logging_mode: bool = false; + // the terminal stores the most recent 1000 lines entered by user + // in history. TODO should make history size adjustable. let history_path = std::fs::canonicalize(&home_directory_path) - .unwrap() + .expect("terminal: could not get path for .terminal_history file") .join(".terminal_history"); let history = read_to_string(&history_path).unwrap_or_default(); let history_handle = OpenOptions::new() .append(true) .create(true) .open(&history_path) - .unwrap(); + .expect("terminal: could not open/create .terminal_history"); let history_writer = BufWriter::new(history_handle); - // TODO make adjustable max history length let mut command_history = utils::CommandHistory::new(1000, history, history_writer); + // if CTRL+L is used to turn on logging, all prints to terminal + // will also be written with their full timestamp to the .terminal_log file. + // logging mode is always off by default. TODO add a boot flag to change this. let log_path = std::fs::canonicalize(&home_directory_path) - .unwrap() + .expect("terminal: could not get path for .terminal_log file") .join(".terminal_log"); let log_handle = OpenOptions::new() .append(true) .create(true) .open(&log_path) - .unwrap(); + .expect("terminal: could not open/create .terminal_log"); let mut log_writer = BufWriter::new(log_handle); // use to trigger cleanup if receive signal to kill process @@ -186,21 +96,32 @@ pub async fn terminal( let mut sigusr2 = signal(SignalKind::user_defined2()).expect("terminal: failed to set up SIGUSR2 handler"); - loop { - let event = reader.next().fuse(); + // if the verbosity boot flag was **not** set to "full event loop", tell kernel + // the kernel will try and print all events by default so that booting with + // verbosity mode 3 guarantees all events from boot are shown. + if verbose_mode != 3 { + let _ = debug_event_loop.send(DebugCommand::ToggleEventLoop).await; + } + + let mut reader = EventStream::new(); + let mut stdout = stdout.lock(); + loop { tokio::select! { Some(printout) = print_rx.recv() => { let now = Local::now(); + // always write print to log if in logging mode if logging_mode { - let _ = writeln!(log_writer, "[{}] {}", now.to_rfc2822(), printout.content); + writeln!(log_writer, "[{}] {}", now.to_rfc2822(), printout.content)?; } + // skip writing print to terminal if it's of a greater + // verbosity level than our current mode if printout.verbosity > verbose_mode { continue; } - let mut stdout = stdout.lock(); execute!( stdout, + // print goes immediately above the dedicated input line at bottom cursor::MoveTo(0, win_rows - 1), terminal::Clear(ClearType::CurrentLine), Print(format!("{} {:02}:{:02} ", @@ -208,41 +129,45 @@ pub async fn terminal( now.hour(), now.minute(), )), - )?; - let color = match printout.verbosity { + style::SetForegroundColor(match printout.verbosity { 0 => style::Color::Reset, 1 => style::Color::Green, 2 => style::Color::Magenta, _ => style::Color::Red, - }; + }), + )?; for line in printout.content.lines() { execute!( stdout, - style::SetForegroundColor(color), Print(format!("{}\r\n", line)), - style::ResetColor, )?; } + // reset color and re-display the current input line + // re-place cursor where user had it at input line execute!( stdout, + style::ResetColor, cursor::MoveTo(0, win_rows), Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))), cursor::MoveTo(cursor_col, win_rows), )?; } - Some(Ok(event)) = event => { - let mut stdout = stdout.lock(); + Some(Ok(event)) = reader.next().fuse() => { match event { - // resize is super annoying because this event trigger often + // + // RESIZE: resize is super annoying because this event trigger often // comes "too late" to stop terminal from messing with the // already-printed lines. TODO figure out the right way // to compensate for this cross-platform and do this in a // generally stable way. + // Event::Resize(width, height) => { win_cols = width; win_rows = height; }, - // handle pasting of text from outside + // + // PASTE: handle pasting of text from outside + // Event::Paste(pasted) => { // strip out control characters and newlines let pasted = pasted.chars().filter(|c| !c.is_control() && !c.is_ascii_control()).collect::(); @@ -256,7 +181,9 @@ pub async fn terminal( cursor::MoveTo(cursor_col, win_rows), )?; } + // // CTRL+C, CTRL+D: turn off the node + // Event::Key(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, @@ -267,10 +194,18 @@ pub async fn terminal( modifiers: KeyModifiers::CONTROL, .. }) => { - execute!(stdout, DisableBracketedPaste, terminal::SetTitle(""))?; + execute!( + stdout, + // print goes immediately above the dedicated input line at bottom + cursor::MoveTo(0, win_rows - 1), + terminal::Clear(ClearType::CurrentLine), + Print("exit code received"), + )?; break; }, + // // CTRL+V: toggle through verbosity modes + // Event::Key(KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, @@ -294,26 +229,34 @@ pub async fn terminal( } } ).await; + if verbose_mode == 3 { + let _ = debug_event_loop.send(DebugCommand::ToggleEventLoop).await; + } }, + // // CTRL+J: toggle debug mode -- makes system-level event loop step-through - // CTRL+S: step through system-level event loop + // Event::Key(KeyEvent { code: KeyCode::Char('j'), modifiers: KeyModifiers::CONTROL, .. }) => { + let _ = debug_event_loop.send(DebugCommand::ToggleStepthrough).await; + in_step_through = !in_step_through; let _ = print_tx.send( Printout { verbosity: 0, content: match in_step_through { - true => "debug mode off".into(), - false => "debug mode on: use CTRL+S to step through events".into(), + false => "debug mode off".into(), + true => "debug mode on: use CTRL+S to step through events".into(), } } ).await; - let _ = debug_event_loop.send(DebugCommand::Toggle).await; - in_step_through = !in_step_through; + }, + // + // CTRL+S: step through system-level event loop (when in step-through mode) + // Event::Key(KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, @@ -342,7 +285,6 @@ pub async fn terminal( }, // // UP / CTRL+P: go up one command in history - // DOWN / CTRL+N: go down one command in history // Event::Key(KeyEvent { code: KeyCode::Up, .. }) | Event::Key(KeyEvent { @@ -357,6 +299,7 @@ pub async fn terminal( line_col = current_line.len(); }, None => { + // the "no-no" ding print!("\x07"); }, } @@ -368,6 +311,9 @@ pub async fn terminal( Print(utils::truncate_rightward(¤t_line, prompt_len, win_cols)), )?; }, + // + // DOWN / CTRL+N: go down one command in history + // Event::Key(KeyEvent { code: KeyCode::Down, .. }) | Event::Key(KeyEvent { code: KeyCode::Char('n'), @@ -381,6 +327,7 @@ pub async fn terminal( line_col = current_line.len(); }, None => { + // the "no-no" ding print!("\x07"); }, } @@ -401,7 +348,7 @@ pub async fn terminal( .. }) => { line_col = prompt_len; - cursor_col = prompt_len.try_into().unwrap(); + cursor_col = prompt_len as u16; execute!( stdout, cursor::MoveTo(0, win_rows), @@ -438,32 +385,16 @@ pub async fn terminal( search_depth += 1; } search_mode = true; - let search_query = ¤t_line[prompt_len..]; - if search_query.is_empty() { - continue; - } - if let Some(result) = command_history.search(search_query, search_depth) { - let result_underlined = utils::underline(result, search_query); - execute!( - stdout, - cursor::MoveTo(0, win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - &format!("{} * {}", our.name, result_underlined), - prompt_len, - win_cols, - (line_col, cursor_col))), - cursor::MoveTo(cursor_col, win_rows), - )?; - } else { - execute!( - stdout, - cursor::MoveTo(0, win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))), - cursor::MoveTo(cursor_col, win_rows), - )?; - } + utils::execute_search( + &our, + &mut stdout, + ¤t_line, + prompt_len, + (win_cols, win_rows), + (line_col, cursor_col), + &mut command_history, + search_depth, + )?; }, // // CTRL+G: exit search mode @@ -480,15 +411,22 @@ pub async fn terminal( stdout, cursor::MoveTo(0, win_rows), terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))), + Print(utils::truncate_in_place( + &format!("{} > {}", our.name, ¤t_line[prompt_len..]), + prompt_len, + win_cols, + (line_col, cursor_col))), cursor::MoveTo(cursor_col, win_rows), )?; }, // - // handle keypress events + // KEY: handle keypress events // Event::Key(k) => { match k.code { + // + // CHAR: write a single character + // KeyCode::Char(c) => { current_line.insert(line_col, c); if cursor_col < win_cols { @@ -496,22 +434,17 @@ pub async fn terminal( } line_col += 1; if search_mode { - let search_query = ¤t_line[prompt_len..]; - if let Some(result) = command_history.search(search_query, search_depth) { - let result_underlined = utils::underline(result, search_query); - execute!( - stdout, - cursor::MoveTo(0, win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - &format!("{} * {}", our.name, result_underlined), - prompt_len, - win_cols, - (line_col, cursor_col))), - cursor::MoveTo(cursor_col, win_rows), - )?; - continue; - } + utils::execute_search( + &our, + &mut stdout, + ¤t_line, + prompt_len, + (win_cols, win_rows), + (line_col, cursor_col), + &mut command_history, + search_depth, + )?; + continue; } execute!( stdout, @@ -521,6 +454,9 @@ pub async fn terminal( cursor::MoveTo(cursor_col, win_rows), )?; }, + // + // BACKSPACE or DELETE: delete a single character at cursor + // KeyCode::Backspace | KeyCode::Delete => { if line_col == prompt_len { continue; @@ -531,22 +467,17 @@ pub async fn terminal( line_col -= 1; current_line.remove(line_col); if search_mode { - let search_query = ¤t_line[prompt_len..]; - if let Some(result) = command_history.search(search_query, search_depth) { - let result_underlined = utils::underline(result, search_query); - execute!( - stdout, - cursor::MoveTo(0, win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - &format!("{} * {}", our.name, result_underlined), - prompt_len, - win_cols, - (line_col, cursor_col))), - cursor::MoveTo(cursor_col, win_rows), - )?; - continue; - } + utils::execute_search( + &our, + &mut stdout, + ¤t_line, + prompt_len, + (win_cols, win_rows), + (line_col, cursor_col), + &mut command_history, + search_depth, + )?; + continue; } execute!( stdout, @@ -556,6 +487,9 @@ pub async fn terminal( cursor::MoveTo(cursor_col, win_rows), )?; }, + // + // LEFT: move cursor one spot left + // KeyCode::Left => { if cursor_col as usize == prompt_len { if line_col == prompt_len { @@ -581,6 +515,9 @@ pub async fn terminal( line_col -= 1; } }, + // + // RIGHT: move cursor one spot right + // KeyCode::Right => { if line_col == current_line.len() { // at the very end of the current typed line @@ -604,6 +541,9 @@ pub async fn terminal( )?; } }, + // + // ENTER: send current input to terminal process, clearing input line + // KeyCode::Enter => { // if we were in search mode, pull command from that let command = if !search_mode { @@ -612,7 +552,7 @@ pub async fn terminal( command_history.search( ¤t_line[prompt_len..], search_depth - ).unwrap_or(¤t_line[prompt_len..]).to_string() + ).unwrap_or_default().to_string() }; let next = format!("{} > ", our.name); execute!( @@ -627,7 +567,7 @@ pub async fn terminal( search_depth = 0; current_line = next; command_history.add(command.clone()); - cursor_col = prompt_len.try_into().unwrap(); + cursor_col = prompt_len as u16; line_col = prompt_len; event_loop.send( KernelMessage { @@ -652,10 +592,14 @@ pub async fn terminal( } ).await.expect("terminal: couldn't execute command!"); }, - _ => {}, + _ => { + // some keycode we don't care about, yet + }, } }, - _ => {}, + _ => { + // some terminal event we don't care about, yet + }, } } _ = sigalrm.recv() => return Err(anyhow::anyhow!("exiting due to SIGALRM")), @@ -668,6 +612,5 @@ pub async fn terminal( _ = sigusr2.recv() => return Err(anyhow::anyhow!("exiting due to SIGUSR2")), } } - execute!(stdout.lock(), DisableBracketedPaste, terminal::SetTitle(""))?; Ok(()) } diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index 59b4c2527..825d58961 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -1,6 +1,137 @@ -use std::collections::VecDeque; -use std::fs::File; -use std::io::{BufWriter, Write}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use lib::types::core::Identity; +use std::{ + collections::VecDeque, + fs::File, + io::{BufWriter, Stdout, Write}, +}; + +pub struct RawMode; +impl RawMode { + fn new() -> std::io::Result { + enable_raw_mode()?; + Ok(RawMode) + } +} +impl Drop for RawMode { + fn drop(&mut self) { + match disable_raw_mode() { + Ok(_) => {} + Err(e) => { + println!("terminal: failed to disable raw mode: {e:?}\r"); + } + } + } +} + +pub fn startup( + our: &Identity, + version: &str, + is_detached: bool, +) -> std::io::Result<(Stdout, Option)> { + let mut stdout = std::io::stdout(); + crossterm::execute!( + stdout, + crossterm::event::EnableBracketedPaste, + crossterm::terminal::SetTitle(format!("kinode {}", our.name)) + )?; + + let (win_cols, _) = crossterm::terminal::size().expect("terminal: couldn't fetch size"); + + // print initial splash screen, large if there's room, small otherwise + if win_cols >= 90 { + crossterm::execute!( + stdout, + crossterm::style::SetForegroundColor(crossterm::style::Color::Magenta), + crossterm::style::Print(format!( + r#" + .` + `@@,, ,* 888 d8P d8b 888 + `@%@@@, ,~-##` 888 d8P Y8P 888 + ~@@#@%#@@, ##### 888 d8P 888 + ~-%######@@@, ##### 888d88K 888 88888b. .d88b. .d88888 .d88b. + -%%#######@#####, 8888888b 888 888 "88b d88""88b d88" 888 d8P Y8b + ~^^%##########@ 888 Y88b 888 888 888 888 888 888 888 88888888 + >^#########@ 888 Y88b 888 888 888 Y88..88P Y88b 888 Y8b. + `>#######` 888 Y88b 888 888 888 "Y88P" "Y88888 "Y8888 + .>######% + /###%^#% {} ({}) + /##%@# ` runtime version {} + ./######` a general purpose sovereign cloud computer + /.^`.#^#^` + ` ,#`#`#, + ,/ /` ` + .*` + networking public key: {} + "#, + our.name, + if our.is_direct() { + "direct" + } else { + "indirect" + }, + version, + our.networking_key, + )), + crossterm::style::ResetColor + ) + .expect("terminal: couldn't print splash"); + } else { + crossterm::execute!( + stdout, + crossterm::style::SetForegroundColor(crossterm::style::Color::Magenta), + crossterm::style::Print(format!( + r#" + 888 d8P d8b 888 + 888 d8P Y8P 888 + 888 d8P 888 + 888d88K 888 88888b. .d88b. .d88888 .d88b. + 8888888b 888 888 "88b d88""88b d88" 888 d8P Y8b + 888 Y88b 888 888 888 888 888 888 888 88888888 + 888 Y88b 888 888 888 Y88..88P Y88b 888 Y8b. + 888 Y88b 888 888 888 "Y88P" "Y88888 "Y8888 + + {} ({}) + version {} + a general purpose sovereign cloud computer + net pubkey: {} + "#, + our.name, + if our.is_direct() { + "direct" + } else { + "indirect" + }, + version, + our.networking_key, + )), + crossterm::style::ResetColor + )?; + } + + Ok(( + stdout, + if is_detached { + None + } else { + Some(RawMode::new()?) + }, + )) +} + +pub fn cleanup(quit_msg: &str) { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + crossterm::execute!( + stdout, + crossterm::event::DisableBracketedPaste, + crossterm::terminal::SetTitle(""), + crossterm::style::SetForegroundColor(crossterm::style::Color::Red), + crossterm::style::Print(format!("\r\n{quit_msg}\r\n")), + crossterm::style::ResetColor, + ) + .expect("failed to clean up terminal visual state! your terminal window might be funky now"); +} #[derive(Debug)] pub struct CommandHistory { @@ -70,6 +201,9 @@ impl CommandHistory { /// yes this is O(n) to provide desired ordering, can revisit if slow pub fn search(&mut self, find: &str, depth: usize) -> Option<&str> { let mut skips = 0; + if find.is_empty() { + return None; + } // if there is at least one match, and we've skipped past it, return oldest match let mut last_match: Option<&str> = None; for line in self.lines.iter() { @@ -86,14 +220,56 @@ impl CommandHistory { } } -pub fn underline(s: &str, to_underline: &str) -> String { +pub fn execute_search( + our: &Identity, + stdout: &mut std::io::StdoutLock, + current_line: &str, + prompt_len: usize, + (win_cols, win_rows): (u16, u16), + (line_col, cursor_col): (usize, u16), + command_history: &mut CommandHistory, + search_depth: usize, +) -> Result<(), std::io::Error> { + let search_query = ¤t_line[prompt_len..]; + if let Some(result) = command_history.search(search_query, search_depth) { + let (result_underlined, u_end) = underline(result, search_query); + let search_cursor_col = u_end + prompt_len as u16; + crossterm::execute!( + stdout, + crossterm::cursor::MoveTo(0, win_rows), + crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine), + crossterm::style::Print(truncate_in_place( + &format!("{} * {}", our.name, result_underlined), + prompt_len, + win_cols, + (line_col, search_cursor_col) + )), + crossterm::cursor::MoveTo(search_cursor_col, win_rows), + ) + } else { + crossterm::execute!( + stdout, + crossterm::cursor::MoveTo(0, win_rows), + crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine), + crossterm::style::Print(truncate_in_place( + &format!("{} * {}: no results", our.name, ¤t_line[prompt_len..]), + prompt_len, + win_cols, + (line_col, cursor_col) + )), + crossterm::cursor::MoveTo(cursor_col, win_rows), + ) + } +} + +pub fn underline(s: &str, to_underline: &str) -> (String, u16) { // format result string to have query portion underlined let mut result = s.to_string(); let u_start = s.find(to_underline).unwrap(); let u_end = u_start + to_underline.len(); result.insert_str(u_end, "\x1b[24m"); result.insert_str(u_start, "\x1b[4m"); - result + (result, u_end as u16) } pub fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String { diff --git a/lib/src/core.rs b/lib/src/core.rs index 3f8ce5170..a675e778f 100644 --- a/lib/src/core.rs +++ b/lib/src/core.rs @@ -1180,8 +1180,9 @@ pub type Rsvp = Option
; #[derive(Debug, Serialize, Deserialize)] pub enum DebugCommand { - Toggle, + ToggleStepthrough, Step, + ToggleEventLoop, } /// IPC format for requests sent to kernel runtime module