mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-26 01:54:35 +03:00
terminal: fix: use unicode graphemes to manage input line
ALSO re-jigger 2 errors in terminal, app store
This commit is contained in:
parent
a97147452b
commit
e0751cdbf1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3573,6 +3573,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.21.0",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"walkdir",
|
||||
"warp",
|
||||
|
@ -84,6 +84,7 @@ static_dir = "0.2.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] }
|
||||
tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] }
|
||||
unicode-segmentation = "1.11"
|
||||
url = "2.4.1"
|
||||
warp = "0.3.5"
|
||||
wasi-common = "19.0.1"
|
||||
|
@ -14,13 +14,12 @@ use kinode_process_lib::{
|
||||
await_message, call_init, eth, get_blob, get_state, http, kernel_types as kt, kimap,
|
||||
print_to_terminal, println, timer, Address, Message, PackageId, Request, Response,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "target/wit",
|
||||
generate_unused_types: true,
|
||||
@ -271,10 +270,7 @@ fn handle_eth_log(our: &Address, state: &mut State, log: eth::Log) -> anyhow::Re
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
.map_err(|e| {
|
||||
println!("Couldn't find {hash_note}: {e:?}");
|
||||
anyhow::anyhow!("metadata hash mismatch")
|
||||
})?;
|
||||
.map_err(|e| anyhow::anyhow!("Couldn't find {hash_note}: {e:?}"))?;
|
||||
|
||||
match data {
|
||||
None => {
|
||||
|
@ -20,7 +20,7 @@ enum TerminalAction {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum ScriptError {
|
||||
UnknownName,
|
||||
UnknownName(String),
|
||||
FailedToReadWasm,
|
||||
NoScriptsManifest,
|
||||
NoScriptInManifest,
|
||||
@ -30,18 +30,16 @@ enum ScriptError {
|
||||
|
||||
impl std::fmt::Display for ScriptError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ScriptError::UnknownName => "script not found, either as an alias or process ID",
|
||||
ScriptError::FailedToReadWasm => "failed to read script Wasm from VFS",
|
||||
ScriptError::NoScriptsManifest => "no scripts manifest in package",
|
||||
ScriptError::NoScriptInManifest => "script not in scripts.json file",
|
||||
ScriptError::InvalidScriptsManifest => "could not parse scripts.json file",
|
||||
ScriptError::KernelUnresponsive => "kernel unresponsive",
|
||||
match self {
|
||||
ScriptError::UnknownName(name) => {
|
||||
write!(f, "'{name}' not found, either as an alias or process ID")
|
||||
}
|
||||
)
|
||||
ScriptError::FailedToReadWasm => write!(f, "failed to read script Wasm from VFS"),
|
||||
ScriptError::NoScriptsManifest => write!(f, "no scripts manifest in package"),
|
||||
ScriptError::NoScriptInManifest => write!(f, "script not in scripts.json file"),
|
||||
ScriptError::InvalidScriptsManifest => write!(f, "could not parse scripts.json file"),
|
||||
ScriptError::KernelUnresponsive => write!(f, "kernel unresponsive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,7 +185,7 @@ fn parse_command(state: &mut TerminalState, line: String) -> Result<(), ScriptEr
|
||||
Some(process) => handle_run(&state.our, process, args.to_string()),
|
||||
None => match head.parse::<ProcessId>() {
|
||||
Ok(pid) => handle_run(&state.our, &pid, args.to_string()),
|
||||
Err(_) => Err(ScriptError::UnknownName),
|
||||
Err(_) => Err(ScriptError::UnknownName(head.to_string())),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -16,24 +16,22 @@ use std::{
|
||||
io::{BufWriter, Write},
|
||||
};
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub struct State {
|
||||
struct State {
|
||||
pub stdout: std::io::Stdout,
|
||||
/// handle for writing to on-disk log (disabled by default, triggered by CTRL+L)
|
||||
pub log_writer: BufWriter<std::fs::File>,
|
||||
/// in-memory searchable command history (default size: 1000)
|
||||
/// in-memory searchable command history that persists itself on disk (default size: 1000)
|
||||
pub command_history: utils::CommandHistory,
|
||||
/// terminal window width, 0 is leftmost column
|
||||
pub win_cols: u16,
|
||||
/// terminal window height, 0 is topmost row
|
||||
pub win_rows: u16,
|
||||
/// prompt for user input (e.g. "mynode.os > ")
|
||||
pub prompt: &'static str,
|
||||
pub line_col: usize,
|
||||
pub cursor_col: u16,
|
||||
pub current_line: String,
|
||||
/// the input line (bottom row)
|
||||
pub current_line: CurrentLine,
|
||||
/// flag representing whether we are in step-through mode (activated by CTRL+J, stepped by CTRL+S)
|
||||
pub in_step_through: bool,
|
||||
/// flag representing whether we are in search mode (activated by CTRL+R, exited by CTRL+G)
|
||||
@ -46,6 +44,94 @@ pub struct State {
|
||||
pub verbose_mode: u8,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn display_current_input_line(&mut self) -> Result<(), std::io::Error> {
|
||||
execute!(
|
||||
self.stdout,
|
||||
cursor::MoveTo(0, self.win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
style::SetForegroundColor(style::Color::Reset),
|
||||
Print(self.current_line.prompt),
|
||||
Print(utils::truncate_in_place(
|
||||
&self.current_line.line,
|
||||
self.win_cols as usize - self.current_line.prompt_len,
|
||||
self.current_line.line_col,
|
||||
self.current_line.cursor_col
|
||||
)),
|
||||
cursor::MoveTo(
|
||||
self.current_line.prompt_len as u16 + self.current_line.cursor_col,
|
||||
self.win_rows
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn search(&mut self, our_name: &str) -> Result<(), std::io::Error> {
|
||||
let search_prompt = format!("{} *", our_name);
|
||||
let search_query = &self.current_line.line;
|
||||
if let Some(result) = self.command_history.search(search_query, self.search_depth) {
|
||||
let (result_underlined, u_end) = utils::underline(result, search_query);
|
||||
let search_cursor_col = u_end + search_prompt.graphemes(true).count() as u16;
|
||||
execute!(
|
||||
self.stdout,
|
||||
cursor::MoveTo(0, self.win_rows),
|
||||
terminal::Clear(terminal::ClearType::CurrentLine),
|
||||
style::SetForegroundColor(style::Color::Reset),
|
||||
style::Print(&search_prompt),
|
||||
style::Print(utils::truncate_in_place(
|
||||
&result_underlined,
|
||||
self.win_cols as usize - self.current_line.prompt_len,
|
||||
self.current_line.line_col,
|
||||
search_cursor_col
|
||||
)),
|
||||
cursor::MoveTo(
|
||||
self.current_line.prompt_len as u16 + search_cursor_col,
|
||||
self.win_rows
|
||||
),
|
||||
)
|
||||
} else {
|
||||
execute!(
|
||||
self.stdout,
|
||||
cursor::MoveTo(0, self.win_rows),
|
||||
terminal::Clear(terminal::ClearType::CurrentLine),
|
||||
style::SetForegroundColor(style::Color::Reset),
|
||||
style::Print(&search_prompt),
|
||||
style::Print(utils::truncate_in_place(
|
||||
&format!("{}: no results", &self.current_line.line),
|
||||
self.win_cols as usize - self.current_line.prompt_len,
|
||||
self.current_line.line_col,
|
||||
self.current_line.cursor_col
|
||||
)),
|
||||
cursor::MoveTo(
|
||||
self.current_line.prompt_len as u16 + self.current_line.cursor_col,
|
||||
self.win_rows
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentLine {
|
||||
/// prompt for user input (e.g. "mynode.os > ")
|
||||
pub prompt: &'static str,
|
||||
pub prompt_len: usize,
|
||||
/// the grapheme index of the cursor in the current line
|
||||
pub line_col: usize,
|
||||
/// the column index of the cursor in the terminal window (not including prompt)
|
||||
pub cursor_col: u16,
|
||||
/// the line itself, which does not include the prompt
|
||||
pub line: String,
|
||||
}
|
||||
|
||||
impl CurrentLine {
|
||||
fn byte_index(&self) -> usize {
|
||||
self.line
|
||||
.grapheme_indices(true)
|
||||
.nth(self.line_col)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or_else(|| self.line.graphemes(true).count())
|
||||
}
|
||||
}
|
||||
|
||||
/// main entry point for terminal process
|
||||
/// called by main.rs
|
||||
pub async fn terminal(
|
||||
@ -64,10 +150,9 @@ pub async fn terminal(
|
||||
|
||||
let (win_cols, win_rows) = crossterm::terminal::size().expect("terminal: couldn't fetch size");
|
||||
|
||||
let prompt = Box::leak(format!("{} > ", our.name).into_boxed_str());
|
||||
let prompt_len: usize = prompt.len();
|
||||
let cursor_col: u16 = prompt_len as u16;
|
||||
let line_col: usize = cursor_col as usize;
|
||||
let (prompt, prompt_len) = utils::make_prompt(&our.name);
|
||||
let cursor_col: u16 = 0;
|
||||
let line_col: usize = 0;
|
||||
|
||||
let in_step_through = false;
|
||||
|
||||
@ -109,10 +194,13 @@ pub async fn terminal(
|
||||
command_history,
|
||||
win_cols,
|
||||
win_rows,
|
||||
prompt,
|
||||
line_col,
|
||||
cursor_col,
|
||||
current_line: prompt.to_string(),
|
||||
current_line: CurrentLine {
|
||||
prompt,
|
||||
prompt_len,
|
||||
line_col,
|
||||
cursor_col,
|
||||
line: "".to_string(),
|
||||
},
|
||||
in_step_through,
|
||||
search_mode,
|
||||
search_depth,
|
||||
@ -229,22 +317,10 @@ fn handle_printout(printout: Printout, state: &mut State) -> anyhow::Result<()>
|
||||
}),
|
||||
)?;
|
||||
for line in printout.content.lines() {
|
||||
execute!(stdout, Print(format!("{}\r\n", line)),)?;
|
||||
execute!(stdout, Print(format!("{line}\r\n")))?;
|
||||
}
|
||||
// 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, state.win_rows),
|
||||
Print(utils::truncate_in_place(
|
||||
&state.current_line,
|
||||
state.prompt.len(),
|
||||
state.win_cols,
|
||||
(state.line_col, state.cursor_col)
|
||||
)),
|
||||
cursor::MoveTo(state.cursor_col, state.win_rows),
|
||||
)?;
|
||||
// re-display the current input line
|
||||
state.display_current_input_line()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -262,12 +338,8 @@ async fn handle_event(
|
||||
command_history,
|
||||
win_cols,
|
||||
win_rows,
|
||||
prompt,
|
||||
line_col,
|
||||
cursor_col,
|
||||
current_line,
|
||||
in_step_through,
|
||||
search_mode,
|
||||
search_depth,
|
||||
logging_mode,
|
||||
verbose_mode,
|
||||
@ -297,23 +369,14 @@ async fn handle_event(
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() && !c.is_ascii_control())
|
||||
.collect::<String>();
|
||||
current_line.insert_str(*line_col, &pasted);
|
||||
*line_col = *line_col + pasted.len();
|
||||
*cursor_col = std::cmp::min(
|
||||
line_col.to_owned().try_into().unwrap_or(*win_cols),
|
||||
current_line
|
||||
.line
|
||||
.insert_str(current_line.byte_index(), &pasted);
|
||||
current_line.line_col = current_line.line_col + pasted.graphemes(true).count();
|
||||
current_line.cursor_col = std::cmp::min(
|
||||
current_line.line_col.try_into().unwrap_or(*win_cols),
|
||||
*win_cols,
|
||||
);
|
||||
execute!(
|
||||
stdout,
|
||||
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),
|
||||
)?;
|
||||
}
|
||||
//
|
||||
// CTRL+C, CTRL+D: turn off the node
|
||||
@ -380,6 +443,7 @@ async fn handle_event(
|
||||
)
|
||||
.send(&print_tx)
|
||||
.await;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// CTRL+J: toggle debug mode -- makes system-level event loop step-through
|
||||
@ -403,6 +467,7 @@ async fn handle_event(
|
||||
)
|
||||
.send(&print_tx)
|
||||
.await;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// CTRL+S: step through system-level event loop (when in step-through mode)
|
||||
@ -413,6 +478,7 @@ async fn handle_event(
|
||||
..
|
||||
}) => {
|
||||
let _ = debug_event_loop.send(DebugCommand::Step).await;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// CTRL+L: toggle logging mode
|
||||
@ -429,6 +495,7 @@ async fn handle_event(
|
||||
)
|
||||
.send(&print_tx)
|
||||
.await;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// UP / CTRL+P: go up one command in history
|
||||
@ -442,27 +509,20 @@ async fn handle_event(
|
||||
..
|
||||
}) => {
|
||||
// go up one command in history
|
||||
match command_history.get_prev(¤t_line[prompt.len()..]) {
|
||||
match command_history.get_prev(¤t_line.line) {
|
||||
Some(line) => {
|
||||
*current_line = format!("{} > {}", our.name, line);
|
||||
*line_col = current_line.len();
|
||||
current_line.line_col = line.graphemes(true).count();
|
||||
current_line.line = line;
|
||||
}
|
||||
None => {
|
||||
// the "no-no" ding
|
||||
print!("\x07");
|
||||
}
|
||||
}
|
||||
*cursor_col = std::cmp::min(current_line.len() as u16, *win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_rightward(
|
||||
current_line,
|
||||
prompt.len(),
|
||||
*win_cols
|
||||
)),
|
||||
)?;
|
||||
current_line.cursor_col =
|
||||
std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols);
|
||||
state.display_current_input_line()?;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// DOWN / CTRL+N: go down one command in history
|
||||
@ -479,25 +539,18 @@ async fn handle_event(
|
||||
// go down one command in history
|
||||
match command_history.get_next() {
|
||||
Some(line) => {
|
||||
*current_line = format!("{} > {}", our.name, line);
|
||||
*line_col = current_line.len();
|
||||
current_line.line_col = line.graphemes(true).count();
|
||||
current_line.line = line;
|
||||
}
|
||||
None => {
|
||||
// the "no-no" ding
|
||||
print!("\x07");
|
||||
}
|
||||
}
|
||||
*cursor_col = std::cmp::min(current_line.len() as u16, *win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_rightward(
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
*win_cols
|
||||
)),
|
||||
)?;
|
||||
current_line.cursor_col =
|
||||
std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols);
|
||||
state.display_current_input_line()?;
|
||||
return Ok(false);
|
||||
}
|
||||
//
|
||||
// CTRL+A: jump to beginning of line
|
||||
@ -507,19 +560,11 @@ async fn handle_event(
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
*line_col = prompt.len();
|
||||
*cursor_col = prompt.len() as u16;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
Print(utils::truncate_from_left(
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
*win_cols,
|
||||
*line_col
|
||||
)),
|
||||
cursor::MoveTo(*cursor_col, *win_rows),
|
||||
)?;
|
||||
if state.search_mode {
|
||||
return Ok(false);
|
||||
}
|
||||
current_line.line_col = 0;
|
||||
current_line.cursor_col = 0;
|
||||
}
|
||||
//
|
||||
// CTRL+E: jump to end of line
|
||||
@ -529,21 +574,12 @@ async fn handle_event(
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
*line_col = current_line.len();
|
||||
*cursor_col = std::cmp::min(
|
||||
line_col.to_owned().try_into().unwrap_or(*win_cols),
|
||||
*win_cols,
|
||||
);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
Print(utils::truncate_from_right(
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
*win_cols,
|
||||
*line_col
|
||||
)),
|
||||
)?;
|
||||
if state.search_mode {
|
||||
return Ok(false);
|
||||
}
|
||||
current_line.line_col = current_line.line.graphemes(true).count();
|
||||
current_line.cursor_col =
|
||||
std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols);
|
||||
}
|
||||
//
|
||||
// CTRL+R: enter search mode
|
||||
@ -554,20 +590,10 @@ async fn handle_event(
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
if *search_mode {
|
||||
if state.search_mode {
|
||||
*search_depth += 1;
|
||||
}
|
||||
*search_mode = true;
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
(*win_cols, *win_rows),
|
||||
(*line_col, *cursor_col),
|
||||
command_history,
|
||||
*search_depth,
|
||||
)?;
|
||||
state.search_mode = true;
|
||||
}
|
||||
//
|
||||
// CTRL+G: exit search mode
|
||||
@ -578,20 +604,8 @@ async fn handle_event(
|
||||
..
|
||||
}) => {
|
||||
// just show true current line as usual
|
||||
*search_mode = false;
|
||||
state.search_mode = false;
|
||||
*search_depth = 0;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
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),
|
||||
)?;
|
||||
}
|
||||
//
|
||||
// KEY: handle keypress events
|
||||
@ -602,165 +616,71 @@ async fn handle_event(
|
||||
// CHAR: write a single character
|
||||
//
|
||||
KeyCode::Char(c) => {
|
||||
current_line.insert(*line_col, c);
|
||||
if cursor_col < win_cols {
|
||||
*cursor_col += 1;
|
||||
current_line.line.insert(current_line.byte_index(), c);
|
||||
if current_line.cursor_col < *win_cols {
|
||||
current_line.cursor_col += 1;
|
||||
}
|
||||
*line_col += 1;
|
||||
if *search_mode {
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
(*win_cols, *win_rows),
|
||||
(*line_col, *cursor_col),
|
||||
command_history,
|
||||
*search_depth,
|
||||
)?;
|
||||
return Ok(false);
|
||||
}
|
||||
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),
|
||||
)?;
|
||||
current_line.line_col += 1;
|
||||
}
|
||||
//
|
||||
// BACKSPACE: delete a single character at cursor
|
||||
//
|
||||
KeyCode::Backspace => {
|
||||
if *line_col == prompt.len() {
|
||||
if current_line.line_col == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
if *cursor_col as usize == *line_col {
|
||||
*cursor_col -= 1;
|
||||
if current_line.cursor_col as usize > 0 {
|
||||
current_line.cursor_col -= 1;
|
||||
}
|
||||
*line_col -= 1;
|
||||
current_line.remove(*line_col);
|
||||
if *search_mode {
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
(*win_cols, *win_rows),
|
||||
(*line_col, *cursor_col),
|
||||
command_history,
|
||||
*search_depth,
|
||||
)?;
|
||||
return Ok(false);
|
||||
}
|
||||
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),
|
||||
)?;
|
||||
current_line.line_col -= 1;
|
||||
current_line.line.remove(current_line.byte_index());
|
||||
}
|
||||
//
|
||||
// DELETE: delete a single character at right of cursor
|
||||
//
|
||||
KeyCode::Delete => {
|
||||
if *line_col == current_line.len() {
|
||||
if current_line.line_col == current_line.line.graphemes(true).count() {
|
||||
return Ok(false);
|
||||
}
|
||||
current_line.remove(*line_col);
|
||||
if *search_mode {
|
||||
utils::execute_search(
|
||||
&our,
|
||||
&mut stdout,
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
(*win_cols, *win_rows),
|
||||
(*line_col, *cursor_col),
|
||||
command_history,
|
||||
*search_depth,
|
||||
)?;
|
||||
return Ok(false);
|
||||
}
|
||||
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),
|
||||
)?;
|
||||
current_line.line.remove(current_line.byte_index());
|
||||
}
|
||||
//
|
||||
// LEFT: move cursor one spot left
|
||||
//
|
||||
KeyCode::Left => {
|
||||
if *cursor_col as usize == prompt.len() {
|
||||
if *line_col == prompt.len() {
|
||||
if current_line.cursor_col as usize == 0 {
|
||||
if current_line.line_col == 0 {
|
||||
// at the very beginning of the current typed line
|
||||
return Ok(false);
|
||||
} else {
|
||||
// virtual scroll leftward through line
|
||||
*line_col -= 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
Print(utils::truncate_from_left(
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
*win_cols,
|
||||
*line_col
|
||||
)),
|
||||
cursor::MoveTo(*cursor_col, *win_rows),
|
||||
)?;
|
||||
current_line.line_col -= 1;
|
||||
}
|
||||
} else {
|
||||
// simply move cursor and line position left
|
||||
execute!(stdout, cursor::MoveLeft(1),)?;
|
||||
*cursor_col -= 1;
|
||||
*line_col -= 1;
|
||||
execute!(stdout, cursor::MoveLeft(1))?;
|
||||
current_line.cursor_col -= 1;
|
||||
current_line.line_col -= 1;
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
//
|
||||
// RIGHT: move cursor one spot right
|
||||
//
|
||||
KeyCode::Right => {
|
||||
if *line_col == current_line.len() {
|
||||
if current_line.line_col == current_line.line.graphemes(true).count() {
|
||||
// at the very end of the current typed line
|
||||
return Ok(false);
|
||||
};
|
||||
if *cursor_col < (*win_cols - 1) {
|
||||
if current_line.cursor_col < (*win_cols - 1) {
|
||||
// simply move cursor and line position right
|
||||
execute!(stdout, cursor::MoveRight(1))?;
|
||||
*cursor_col += 1;
|
||||
*line_col += 1;
|
||||
current_line.cursor_col += 1;
|
||||
current_line.line_col += 1;
|
||||
return Ok(false);
|
||||
} else {
|
||||
// virtual scroll rightward through line
|
||||
*line_col += 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
Print(utils::truncate_from_right(
|
||||
¤t_line,
|
||||
prompt.len(),
|
||||
*win_cols,
|
||||
*line_col
|
||||
)),
|
||||
)?;
|
||||
current_line.line_col += 1;
|
||||
}
|
||||
}
|
||||
//
|
||||
@ -768,11 +688,11 @@ async fn handle_event(
|
||||
//
|
||||
KeyCode::Enter => {
|
||||
// if we were in search mode, pull command from that
|
||||
let command = if !*search_mode {
|
||||
current_line[prompt.len()..].to_string()
|
||||
let command = if !state.search_mode {
|
||||
current_line.line.clone()
|
||||
} else {
|
||||
command_history
|
||||
.search(¤t_line[prompt.len()..], *search_depth)
|
||||
.search(¤t_line.line, *search_depth)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
};
|
||||
@ -780,16 +700,15 @@ async fn handle_event(
|
||||
stdout,
|
||||
cursor::MoveTo(0, *win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(&format!("{} > {}", our.name, command)),
|
||||
Print(¤t_line.prompt),
|
||||
Print(&command),
|
||||
Print("\r\n"),
|
||||
Print(&prompt),
|
||||
)?;
|
||||
*search_mode = false;
|
||||
state.search_mode = false;
|
||||
*search_depth = 0;
|
||||
*current_line = prompt.to_string();
|
||||
command_history.add(command.clone());
|
||||
*cursor_col = prompt.len() as u16;
|
||||
*line_col = prompt.len();
|
||||
current_line.cursor_col = 0;
|
||||
current_line.line_col = 0;
|
||||
command_history.add(command.to_string());
|
||||
KernelMessage::builder()
|
||||
.id(rand::random())
|
||||
.source((our.name.as_str(), TERMINAL_PROCESS_ID.clone()))
|
||||
@ -805,6 +724,7 @@ async fn handle_event(
|
||||
.unwrap()
|
||||
.send(&event_loop)
|
||||
.await;
|
||||
current_line.line = "".to_string();
|
||||
}
|
||||
_ => {
|
||||
// some keycode we don't care about, yet
|
||||
@ -815,5 +735,10 @@ async fn handle_event(
|
||||
// some terminal event we don't care about, yet
|
||||
}
|
||||
}
|
||||
if state.search_mode {
|
||||
state.search(&our.name)?;
|
||||
} else {
|
||||
state.display_current_input_line()?;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use std::{
|
||||
fs::File,
|
||||
io::{BufWriter, Stdout, Write},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub struct RawMode;
|
||||
impl RawMode {
|
||||
@ -123,6 +124,17 @@ pub fn splash(
|
||||
))
|
||||
}
|
||||
|
||||
/// produce command line prompt and its length
|
||||
pub fn make_prompt(our_name: &str) -> (&'static str, usize) {
|
||||
let prompt = Box::leak(format!("{} > ", our_name).into_boxed_str());
|
||||
(
|
||||
prompt,
|
||||
UnicodeSegmentation::graphemes(prompt, true)
|
||||
.collect::<Vec<_>>()
|
||||
.len(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cleanup(quit_msg: &str) {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
@ -202,7 +214,6 @@ impl CommandHistory {
|
||||
|
||||
/// if depth = 0, find most recent command in history that contains the
|
||||
/// provided string. otherwise, skip the first <depth> matches.
|
||||
/// 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() {
|
||||
@ -224,104 +235,38 @@ impl CommandHistory {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
let u_end = u_start + to_underline.graphemes(true).count();
|
||||
result.insert_str(u_end, "\x1b[24m");
|
||||
result.insert_str(u_start, "\x1b[4m");
|
||||
(result, u_end as u16)
|
||||
}
|
||||
|
||||
pub fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
let sans_prompt = &s[prompt_len..];
|
||||
s[..prompt_len].to_string() + &sans_prompt[(s.len() - width as usize)..]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term starting from line_col
|
||||
pub fn truncate_from_left(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[line_col..(width as usize - prompt_len + line_col)]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term leading up to line_col
|
||||
pub fn truncate_from_right(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[(prompt_len + (line_col - width as usize))..line_col]
|
||||
}
|
||||
|
||||
/// if line is wider than the terminal, truncate it intelligently,
|
||||
/// keeping the cursor in the same relative position.
|
||||
pub fn truncate_in_place(
|
||||
s: &str,
|
||||
prompt_len: usize,
|
||||
width: u16,
|
||||
(line_col, cursor_col): (usize, u16),
|
||||
) -> String {
|
||||
if s.len() <= width as usize {
|
||||
pub fn truncate_in_place(s: &str, term_width: usize, line_col: usize, cursor_col: u16) -> String {
|
||||
let graphemes_count = s.graphemes(true).count();
|
||||
if graphemes_count <= term_width {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
// always keep prompt at left
|
||||
let prompt = &s[..prompt_len];
|
||||
// print as much of the command fits left of col_in_command before cursor_col,
|
||||
// then fill out the rest up to width
|
||||
let end = width as usize + line_col - cursor_col as usize;
|
||||
if end > s.len() {
|
||||
return s.to_string();
|
||||
|
||||
// input line is wider than terminal, clip start/end/both while keeping cursor
|
||||
// in same relative position.
|
||||
if (cursor_col as usize) == line_col {
|
||||
// beginning of line is placed at left end, truncate everything past term_width
|
||||
s.graphemes(true).take(term_width).collect::<String>()
|
||||
} else if (cursor_col as usize) < line_col {
|
||||
// some amount of the line is to the left of the terminal, clip from the right
|
||||
s.graphemes(true)
|
||||
.skip(line_col - cursor_col as usize)
|
||||
.take(term_width)
|
||||
.collect::<String>()
|
||||
} else {
|
||||
// this cannot occur
|
||||
unreachable!()
|
||||
}
|
||||
prompt.to_string() + &s[(prompt_len + line_col - cursor_col as usize)..end]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user