Merge pull request #402 from kinode-dao/dr/terminal-refactor

refactor terminal a bit
This commit is contained in:
doria 2024-06-18 00:55:31 +09:00 committed by GitHub
commit 0b94aec95c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 376 additions and 241 deletions

View File

@ -710,6 +710,9 @@ pub async fn kernel(
let mut process_handles: ProcessHandles = HashMap::new(); let mut process_handles: ProcessHandles = HashMap::new();
let mut is_debug: bool = false; 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<u8>)> = vec![]; let mut reboot_processes: Vec<(t::ProcessId, StartProcessMetadata, Vec<u8>)> = vec![];
// filter out OnExit::None processes from process_map // filter out OnExit::None processes from process_map
@ -870,9 +873,17 @@ pub async fn kernel(
loop { loop {
tokio::select! { tokio::select! {
// debug mode toggle: when on, this loop becomes a manual step-through // debug mode toggle: when on, this loop becomes a manual step-through
debug = recv_debug_in_loop.recv() => { Some(debug_command) = recv_debug_in_loop.recv() => {
if let Some(t::DebugCommand::Toggle) = debug { match debug_command {
is_debug = !is_debug; 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 // network error message receiver: handle `timeout` and `offline` errors
@ -1042,17 +1053,20 @@ pub async fn kernel(
while is_debug { while is_debug {
let debug = recv_debug_in_loop.recv().await.expect("event loop: debug channel died"); let debug = recv_debug_in_loop.recv().await.expect("event loop: debug channel died");
match debug { match debug {
t::DebugCommand::Toggle => is_debug = !is_debug, t::DebugCommand::ToggleStepthrough => is_debug = !is_debug,
t::DebugCommand::Step => break, t::DebugCommand::Step => break,
t::DebugCommand::ToggleEventLoop => print_full_event_loop = !print_full_event_loop,
} }
} }
// display every single event when verbose // display every single event when verbose
let _ = send_to_terminal.send( if print_full_event_loop {
let _ = send_to_terminal.send(
t::Printout { t::Printout {
verbosity: 3, verbosity: 3,
content: format!("{kernel_message}") content: format!("{kernel_message}")
} }
).await; ).await;
}
if our.name != kernel_message.target.node { if our.name != kernel_message.target.node {
send_to_net.send(kernel_message).await.expect("fatal: net module died"); send_to_net.send(kernel_message).await.expect("fatal: net module died");

View File

@ -455,17 +455,8 @@ async fn main() {
// abort all remaining tasks // abort all remaining tasks
tasks.shutdown().await; tasks.shutdown().await;
let stdout = std::io::stdout(); // reset all modified aspects of terminal -- clean ourselves up
let mut stdout = stdout.lock(); terminal::utils::cleanup(&quit_msg);
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");
} }
async fn set_http_server_port(set_port: Option<&u16>) -> u16 { async fn set_http_server_port(set_port: Option<&u16>) -> u16 {
@ -755,12 +746,9 @@ async fn serve_register_fe(
} }
}; };
tokio::fs::write( tokio::fs::write(format!("{}/.keys", home_directory_path), &encoded_keyfile)
format!("{}/.keys", home_directory_path), .await
encoded_keyfile.clone(), .unwrap();
)
.await
.unwrap();
let _ = kill_tx.send(true); let _ = kill_tx.send(true);
@ -831,12 +819,9 @@ async fn login_with_password(
.await .await
.expect("information used to boot does not match information onchain"); .expect("information used to boot does not match information onchain");
tokio::fs::write( tokio::fs::write(format!("{}/.keys", home_directory_path), &disk_keyfile)
format!("{}/.keys", home_directory_path), .await
disk_keyfile.clone(), .unwrap();
)
.await
.unwrap();
(our, disk_keyfile, k) (our, disk_keyfile, k)
} }

View File

@ -36,6 +36,7 @@ sol! {
function key(bytes32) external view returns (bytes32); function key(bytes32) external view returns (bytes32);
function nodes(bytes32) external view returns (address, uint96); function nodes(bytes32) external view returns (address, uint96);
function ip(bytes32) external view returns (uint128, uint16, uint16, uint16, uint16); 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 /// 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 namehash = FixedBytes::<32>::from_slice(&keygen::namehash(&our.name));
let ip_call = ipCall { _0: namehash }.abi_encode(); let ip_call = ipCall { _0: namehash }.abi_encode();
let key_call = keyCall { _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_input = TransactionInput::new(Bytes::from(ip_call));
let tx = TransactionRequest::default() let tx = TransactionRequest::default()
.to(kns_address) .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) = <Vec<FixedBytes<32>>>::abi_decode(&routers, false) else {
return Err(anyhow::anyhow!("Failed to decode node routers from PKI"));
};
let node_ip = format!( let node_ip = format!(
"{}.{}.{}.{}", "{}.{}.{}.{}",
(ip >> 24) & 0xFF, (ip >> 24) & 0xFF,
@ -739,6 +753,10 @@ pub async fn assign_routing(
ip & 0xFF ip & 0xFF
); );
if !routers.is_empty() {
// indirect node
return Ok(());
}
if node_ip != *"0.0.0.0" && (ws != 0 || tcp != 0) { if node_ip != *"0.0.0.0" && (ws != 0 || tcp != 0) {
// direct node // direct node
let mut ports = std::collections::BTreeMap::new(); let mut ports = std::collections::BTreeMap::new();
@ -763,8 +781,6 @@ pub async fn assign_routing(
ports.insert("tcp".to_string(), tcp); ports.insert("tcp".to_string(), tcp);
} }
our.routing = NodeRouting::Direct { ip: node_ip, ports }; our.routing = NodeRouting::Direct { ip: node_ip, ports };
} else {
// indirect node
} }
Ok(()) Ok(())
} }

View File

@ -1,41 +1,23 @@
use anyhow::Result;
use chrono::{Datelike, Local, Timelike}; use chrono::{Datelike, Local, Timelike};
use crossterm::{ use crossterm::{
cursor, cursor,
event::{ event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
DisableBracketedPaste, EnableBracketedPaste, Event, EventStream, KeyCode, KeyEvent,
KeyModifiers,
},
execute, style, execute, style,
style::Print, style::Print,
terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, terminal::{self, ClearType},
}; };
use futures::{future::FutureExt, StreamExt}; use futures::{future::FutureExt, StreamExt};
use std::fs::{read_to_string, OpenOptions}; use lib::types::core::{
use std::io::{stdout, BufWriter, Write}; 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 tokio::signal::unix::{signal, SignalKind};
use lib::types::core::*; pub mod utils;
mod utils;
struct RawMode;
impl RawMode {
fn new() -> anyhow::Result<Self> {
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");
}
}
}
}
/* /*
* terminal driver * terminal driver
@ -50,122 +32,50 @@ pub async fn terminal(
mut print_rx: PrintReceiver, mut print_rx: PrintReceiver,
is_detached: bool, is_detached: bool,
mut verbose_mode: u8, mut verbose_mode: u8,
) -> Result<()> { ) -> anyhow::Result<()> {
let mut stdout = stdout(); let (stdout, _maybe_raw_mode) = utils::startup(&our, version, is_detached)?;
execute!(
stdout,
EnableBracketedPaste,
terminal::SetTitle(format!("{}", our.name))
)?;
let (mut win_cols, mut win_rows) = terminal::size().unwrap(); // mutable because we adjust them on window resize events
// print initial splash screen, large if there's room, small otherwise let (mut win_cols, mut win_rows) =
if win_cols >= 90 { crossterm::terminal::size().expect("terminal: couldn't fetch size");
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
{} ({})
version {}
a general purpose sovereign cloud computer
net pubkey: {}
"#,
our.name,
if our.is_direct() {
"direct"
} else {
"indirect"
},
version,
our.networking_key,
)),
style::ResetColor
)?;
}
let _raw_mode = if is_detached {
None
} else {
Some(RawMode::new()?)
};
let mut reader = EventStream::new();
let mut current_line = format!("{} > ", our.name); let mut current_line = format!("{} > ", our.name);
let prompt_len: usize = our.name.len() + 3; 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 line_col: usize = cursor_col as usize;
let mut in_step_through: bool = false; let mut in_step_through: bool = false;
let mut search_mode: bool = false; let mut search_mode: bool = false;
let mut search_depth: usize = 0; let mut search_depth: usize = 0;
let mut logging_mode: bool = false; 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) let history_path = std::fs::canonicalize(&home_directory_path)
.unwrap() .expect("terminal: could not get path for .terminal_history file")
.join(".terminal_history"); .join(".terminal_history");
let history = read_to_string(&history_path).unwrap_or_default(); let history = read_to_string(&history_path).unwrap_or_default();
let history_handle = OpenOptions::new() let history_handle = OpenOptions::new()
.append(true) .append(true)
.create(true) .create(true)
.open(&history_path) .open(&history_path)
.unwrap(); .expect("terminal: could not open/create .terminal_history");
let history_writer = BufWriter::new(history_handle); let history_writer = BufWriter::new(history_handle);
// TODO make adjustable max history length
let mut command_history = utils::CommandHistory::new(1000, history, history_writer); 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) let log_path = std::fs::canonicalize(&home_directory_path)
.unwrap() .expect("terminal: could not get path for .terminal_log file")
.join(".terminal_log"); .join(".terminal_log");
let log_handle = OpenOptions::new() let log_handle = OpenOptions::new()
.append(true) .append(true)
.create(true) .create(true)
.open(&log_path) .open(&log_path)
.unwrap(); .expect("terminal: could not open/create .terminal_log");
let mut log_writer = BufWriter::new(log_handle); let mut log_writer = BufWriter::new(log_handle);
// use to trigger cleanup if receive signal to kill process // use to trigger cleanup if receive signal to kill process
@ -186,21 +96,32 @@ pub async fn terminal(
let mut sigusr2 = let mut sigusr2 =
signal(SignalKind::user_defined2()).expect("terminal: failed to set up SIGUSR2 handler"); signal(SignalKind::user_defined2()).expect("terminal: failed to set up SIGUSR2 handler");
loop { // if the verbosity boot flag was **not** set to "full event loop", tell kernel
let event = reader.next().fuse(); // 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! { tokio::select! {
Some(printout) = print_rx.recv() => { Some(printout) = print_rx.recv() => {
let now = Local::now(); let now = Local::now();
// always write print to log if in logging mode
if 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 { if printout.verbosity > verbose_mode {
continue; continue;
} }
let mut stdout = stdout.lock();
execute!( execute!(
stdout, stdout,
// print goes immediately above the dedicated input line at bottom
cursor::MoveTo(0, win_rows - 1), cursor::MoveTo(0, win_rows - 1),
terminal::Clear(ClearType::CurrentLine), terminal::Clear(ClearType::CurrentLine),
Print(format!("{} {:02}:{:02} ", Print(format!("{} {:02}:{:02} ",
@ -208,41 +129,45 @@ pub async fn terminal(
now.hour(), now.hour(),
now.minute(), now.minute(),
)), )),
)?; style::SetForegroundColor(match printout.verbosity {
let color = match printout.verbosity {
0 => style::Color::Reset, 0 => style::Color::Reset,
1 => style::Color::Green, 1 => style::Color::Green,
2 => style::Color::Magenta, 2 => style::Color::Magenta,
_ => style::Color::Red, _ => style::Color::Red,
}; }),
)?;
for line in printout.content.lines() { for line in printout.content.lines() {
execute!( execute!(
stdout, stdout,
style::SetForegroundColor(color),
Print(format!("{}\r\n", line)), 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!( execute!(
stdout, stdout,
style::ResetColor,
cursor::MoveTo(0, win_rows), cursor::MoveTo(0, win_rows),
Print(utils::truncate_in_place(&current_line, prompt_len, win_cols, (line_col, cursor_col))), Print(utils::truncate_in_place(&current_line, prompt_len, win_cols, (line_col, cursor_col))),
cursor::MoveTo(cursor_col, win_rows), cursor::MoveTo(cursor_col, win_rows),
)?; )?;
} }
Some(Ok(event)) = event => { Some(Ok(event)) = reader.next().fuse() => {
let mut stdout = stdout.lock();
match event { 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 // comes "too late" to stop terminal from messing with the
// already-printed lines. TODO figure out the right way // already-printed lines. TODO figure out the right way
// to compensate for this cross-platform and do this in a // to compensate for this cross-platform and do this in a
// generally stable way. // generally stable way.
//
Event::Resize(width, height) => { Event::Resize(width, height) => {
win_cols = width; win_cols = width;
win_rows = height; win_rows = height;
}, },
// handle pasting of text from outside //
// PASTE: handle pasting of text from outside
//
Event::Paste(pasted) => { Event::Paste(pasted) => {
// strip out control characters and newlines // strip out control characters and newlines
let pasted = pasted.chars().filter(|c| !c.is_control() && !c.is_ascii_control()).collect::<String>(); let pasted = pasted.chars().filter(|c| !c.is_control() && !c.is_ascii_control()).collect::<String>();
@ -256,7 +181,9 @@ pub async fn terminal(
cursor::MoveTo(cursor_col, win_rows), cursor::MoveTo(cursor_col, win_rows),
)?; )?;
} }
//
// CTRL+C, CTRL+D: turn off the node // CTRL+C, CTRL+D: turn off the node
//
Event::Key(KeyEvent { Event::Key(KeyEvent {
code: KeyCode::Char('c'), code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
@ -267,10 +194,18 @@ pub async fn terminal(
modifiers: KeyModifiers::CONTROL, 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; break;
}, },
//
// CTRL+V: toggle through verbosity modes // CTRL+V: toggle through verbosity modes
//
Event::Key(KeyEvent { Event::Key(KeyEvent {
code: KeyCode::Char('v'), code: KeyCode::Char('v'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
@ -294,26 +229,34 @@ pub async fn terminal(
} }
} }
).await; ).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+J: toggle debug mode -- makes system-level event loop step-through
// CTRL+S: step through system-level event loop //
Event::Key(KeyEvent { Event::Key(KeyEvent {
code: KeyCode::Char('j'), code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
.. ..
}) => { }) => {
let _ = debug_event_loop.send(DebugCommand::ToggleStepthrough).await;
in_step_through = !in_step_through;
let _ = print_tx.send( let _ = print_tx.send(
Printout { Printout {
verbosity: 0, verbosity: 0,
content: match in_step_through { content: match in_step_through {
true => "debug mode off".into(), false => "debug mode off".into(),
false => "debug mode on: use CTRL+S to step through events".into(), true => "debug mode on: use CTRL+S to step through events".into(),
} }
} }
).await; ).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 { Event::Key(KeyEvent {
code: KeyCode::Char('s'), code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
@ -342,7 +285,6 @@ pub async fn terminal(
}, },
// //
// UP / CTRL+P: go up one command in history // 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 { code: KeyCode::Up, .. }) |
Event::Key(KeyEvent { Event::Key(KeyEvent {
@ -357,6 +299,7 @@ pub async fn terminal(
line_col = current_line.len(); line_col = current_line.len();
}, },
None => { None => {
// the "no-no" ding
print!("\x07"); print!("\x07");
}, },
} }
@ -368,6 +311,9 @@ pub async fn terminal(
Print(utils::truncate_rightward(&current_line, prompt_len, win_cols)), Print(utils::truncate_rightward(&current_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::Down, .. }) |
Event::Key(KeyEvent { Event::Key(KeyEvent {
code: KeyCode::Char('n'), code: KeyCode::Char('n'),
@ -381,6 +327,7 @@ pub async fn terminal(
line_col = current_line.len(); line_col = current_line.len();
}, },
None => { None => {
// the "no-no" ding
print!("\x07"); print!("\x07");
}, },
} }
@ -401,7 +348,7 @@ pub async fn terminal(
.. ..
}) => { }) => {
line_col = prompt_len; line_col = prompt_len;
cursor_col = prompt_len.try_into().unwrap(); cursor_col = prompt_len as u16;
execute!( execute!(
stdout, stdout,
cursor::MoveTo(0, win_rows), cursor::MoveTo(0, win_rows),
@ -438,32 +385,16 @@ pub async fn terminal(
search_depth += 1; search_depth += 1;
} }
search_mode = true; search_mode = true;
let search_query = &current_line[prompt_len..]; utils::execute_search(
if search_query.is_empty() { &our,
continue; &mut stdout,
} &current_line,
if let Some(result) = command_history.search(search_query, search_depth) { prompt_len,
let result_underlined = utils::underline(result, search_query); (win_cols, win_rows),
execute!( (line_col, cursor_col),
stdout, &mut command_history,
cursor::MoveTo(0, win_rows), search_depth,
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(&current_line, prompt_len, win_cols, (line_col, cursor_col))),
cursor::MoveTo(cursor_col, win_rows),
)?;
}
}, },
// //
// CTRL+G: exit search mode // CTRL+G: exit search mode
@ -480,15 +411,22 @@ pub async fn terminal(
stdout, stdout,
cursor::MoveTo(0, win_rows), cursor::MoveTo(0, win_rows),
terminal::Clear(ClearType::CurrentLine), terminal::Clear(ClearType::CurrentLine),
Print(utils::truncate_in_place(&current_line, prompt_len, win_cols, (line_col, cursor_col))), Print(utils::truncate_in_place(
&format!("{} > {}", our.name, &current_line[prompt_len..]),
prompt_len,
win_cols,
(line_col, cursor_col))),
cursor::MoveTo(cursor_col, win_rows), cursor::MoveTo(cursor_col, win_rows),
)?; )?;
}, },
// //
// handle keypress events // KEY: handle keypress events
// //
Event::Key(k) => { Event::Key(k) => {
match k.code { match k.code {
//
// CHAR: write a single character
//
KeyCode::Char(c) => { KeyCode::Char(c) => {
current_line.insert(line_col, c); current_line.insert(line_col, c);
if cursor_col < win_cols { if cursor_col < win_cols {
@ -496,22 +434,17 @@ pub async fn terminal(
} }
line_col += 1; line_col += 1;
if search_mode { if search_mode {
let search_query = &current_line[prompt_len..]; utils::execute_search(
if let Some(result) = command_history.search(search_query, search_depth) { &our,
let result_underlined = utils::underline(result, search_query); &mut stdout,
execute!( &current_line,
stdout, prompt_len,
cursor::MoveTo(0, win_rows), (win_cols, win_rows),
terminal::Clear(ClearType::CurrentLine), (line_col, cursor_col),
Print(utils::truncate_in_place( &mut command_history,
&format!("{} * {}", our.name, result_underlined), search_depth,
prompt_len, )?;
win_cols, continue;
(line_col, cursor_col))),
cursor::MoveTo(cursor_col, win_rows),
)?;
continue;
}
} }
execute!( execute!(
stdout, stdout,
@ -521,6 +454,9 @@ pub async fn terminal(
cursor::MoveTo(cursor_col, win_rows), cursor::MoveTo(cursor_col, win_rows),
)?; )?;
}, },
//
// BACKSPACE or DELETE: delete a single character at cursor
//
KeyCode::Backspace | KeyCode::Delete => { KeyCode::Backspace | KeyCode::Delete => {
if line_col == prompt_len { if line_col == prompt_len {
continue; continue;
@ -531,22 +467,17 @@ pub async fn terminal(
line_col -= 1; line_col -= 1;
current_line.remove(line_col); current_line.remove(line_col);
if search_mode { if search_mode {
let search_query = &current_line[prompt_len..]; utils::execute_search(
if let Some(result) = command_history.search(search_query, search_depth) { &our,
let result_underlined = utils::underline(result, search_query); &mut stdout,
execute!( &current_line,
stdout, prompt_len,
cursor::MoveTo(0, win_rows), (win_cols, win_rows),
terminal::Clear(ClearType::CurrentLine), (line_col, cursor_col),
Print(utils::truncate_in_place( &mut command_history,
&format!("{} * {}", our.name, result_underlined), search_depth,
prompt_len, )?;
win_cols, continue;
(line_col, cursor_col))),
cursor::MoveTo(cursor_col, win_rows),
)?;
continue;
}
} }
execute!( execute!(
stdout, stdout,
@ -556,6 +487,9 @@ pub async fn terminal(
cursor::MoveTo(cursor_col, win_rows), cursor::MoveTo(cursor_col, win_rows),
)?; )?;
}, },
//
// LEFT: move cursor one spot left
//
KeyCode::Left => { KeyCode::Left => {
if cursor_col as usize == prompt_len { if cursor_col as usize == prompt_len {
if line_col == prompt_len { if line_col == prompt_len {
@ -581,6 +515,9 @@ pub async fn terminal(
line_col -= 1; line_col -= 1;
} }
}, },
//
// RIGHT: move cursor one spot right
//
KeyCode::Right => { KeyCode::Right => {
if line_col == current_line.len() { if line_col == current_line.len() {
// at the very end of the current typed line // 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 => { KeyCode::Enter => {
// if we were in search mode, pull command from that // if we were in search mode, pull command from that
let command = if !search_mode { let command = if !search_mode {
@ -612,7 +552,7 @@ pub async fn terminal(
command_history.search( command_history.search(
&current_line[prompt_len..], &current_line[prompt_len..],
search_depth search_depth
).unwrap_or(&current_line[prompt_len..]).to_string() ).unwrap_or_default().to_string()
}; };
let next = format!("{} > ", our.name); let next = format!("{} > ", our.name);
execute!( execute!(
@ -627,7 +567,7 @@ pub async fn terminal(
search_depth = 0; search_depth = 0;
current_line = next; current_line = next;
command_history.add(command.clone()); command_history.add(command.clone());
cursor_col = prompt_len.try_into().unwrap(); cursor_col = prompt_len as u16;
line_col = prompt_len; line_col = prompt_len;
event_loop.send( event_loop.send(
KernelMessage { KernelMessage {
@ -652,10 +592,14 @@ pub async fn terminal(
} }
).await.expect("terminal: couldn't execute command!"); ).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")), _ = 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")), _ = sigusr2.recv() => return Err(anyhow::anyhow!("exiting due to SIGUSR2")),
} }
} }
execute!(stdout.lock(), DisableBracketedPaste, terminal::SetTitle(""))?;
Ok(()) Ok(())
} }

View File

@ -1,6 +1,137 @@
use std::collections::VecDeque; use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::File; use lib::types::core::Identity;
use std::io::{BufWriter, Write}; use std::{
collections::VecDeque,
fs::File,
io::{BufWriter, Stdout, Write},
};
pub struct RawMode;
impl RawMode {
fn new() -> std::io::Result<Self> {
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<RawMode>)> {
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)] #[derive(Debug)]
pub struct CommandHistory { pub struct CommandHistory {
@ -70,6 +201,9 @@ impl CommandHistory {
/// yes this is O(n) to provide desired ordering, can revisit if slow /// yes this is O(n) to provide desired ordering, can revisit if slow
pub fn search(&mut self, find: &str, depth: usize) -> Option<&str> { pub fn search(&mut self, find: &str, depth: usize) -> Option<&str> {
let mut skips = 0; 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 // if there is at least one match, and we've skipped past it, return oldest match
let mut last_match: Option<&str> = None; let mut last_match: Option<&str> = None;
for line in self.lines.iter() { 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 = &current_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, &current_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 // format result string to have query portion underlined
let mut result = s.to_string(); let mut result = s.to_string();
let u_start = s.find(to_underline).unwrap(); let u_start = s.find(to_underline).unwrap();
let u_end = u_start + to_underline.len(); let u_end = u_start + to_underline.len();
result.insert_str(u_end, "\x1b[24m"); result.insert_str(u_end, "\x1b[24m");
result.insert_str(u_start, "\x1b[4m"); 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 { pub fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String {

View File

@ -1180,8 +1180,9 @@ pub type Rsvp = Option<Address>;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum DebugCommand { pub enum DebugCommand {
Toggle, ToggleStepthrough,
Step, Step,
ToggleEventLoop,
} }
/// IPC format for requests sent to kernel runtime module /// IPC format for requests sent to kernel runtime module