mirror of
https://github.com/uqbar-dao/nectar.git
synced 2025-01-03 06:11:01 +03:00
Merge pull request #402 from kinode-dao/dr/terminal-refactor
refactor terminal a bit
This commit is contained in:
commit
0b94aec95c
@ -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<u8>)> = 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 {
|
||||
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
|
||||
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");
|
||||
|
@ -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,10 +746,7 @@ async fn serve_register_fe(
|
||||
}
|
||||
};
|
||||
|
||||
tokio::fs::write(
|
||||
format!("{}/.keys", home_directory_path),
|
||||
encoded_keyfile.clone(),
|
||||
)
|
||||
tokio::fs::write(format!("{}/.keys", home_directory_path), &encoded_keyfile)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -831,10 +819,7 @@ 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(),
|
||||
)
|
||||
tokio::fs::write(format!("{}/.keys", home_directory_path), &disk_keyfile)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -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) = <Vec<FixedBytes<32>>>::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(())
|
||||
}
|
||||
|
@ -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<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 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))
|
||||
)?;
|
||||
) -> anyhow::Result<()> {
|
||||
let (stdout, _maybe_raw_mode) = utils::startup(&our, version, is_detached)?;
|
||||
|
||||
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
|
||||
// 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");
|
||||
|
||||
{} ({})
|
||||
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 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::<String>();
|
||||
@ -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),
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
(win_cols, win_rows),
|
||||
(line_col, cursor_col),
|
||||
&mut command_history,
|
||||
search_depth,
|
||||
)?;
|
||||
} 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),
|
||||
)?;
|
||||
}
|
||||
},
|
||||
//
|
||||
// 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,23 +434,18 @@ 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),
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
(win_cols, win_rows),
|
||||
(line_col, cursor_col),
|
||||
&mut command_history,
|
||||
search_depth,
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
@ -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,23 +467,18 @@ 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),
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
(win_cols, win_rows),
|
||||
(line_col, cursor_col),
|
||||
&mut command_history,
|
||||
search_depth,
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
@ -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(())
|
||||
}
|
||||
|
@ -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<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)]
|
||||
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 {
|
||||
|
@ -1180,8 +1180,9 @@ pub type Rsvp = Option<Address>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum DebugCommand {
|
||||
Toggle,
|
||||
ToggleStepthrough,
|
||||
Step,
|
||||
ToggleEventLoop,
|
||||
}
|
||||
|
||||
/// IPC format for requests sent to kernel runtime module
|
||||
|
Loading…
Reference in New Issue
Block a user