terminal: fix: use unicode graphemes to manage input line

ALSO re-jigger 2 errors in terminal, app store
This commit is contained in:
dr-frmr 2024-09-05 20:36:26 -04:00
parent a97147452b
commit e0751cdbf1
No known key found for this signature in database
6 changed files with 228 additions and 362 deletions

1
Cargo.lock generated
View File

@ -3573,6 +3573,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-tungstenite 0.21.0",
"unicode-segmentation",
"url",
"walkdir",
"warp",

View File

@ -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"

View File

@ -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 => {

View File

@ -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())),
},
}
}

View File

@ -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(
&current_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(&current_line[prompt.len()..]) {
match command_history.get_prev(&current_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(
&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);
}
//
// 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(
&current_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(
&current_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,
&current_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, &current_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,
&current_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(
&current_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,
&current_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(
&current_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,
&current_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(
&current_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(
&current_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(
&current_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(&current_line[prompt.len()..], *search_depth)
.search(&current_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(&current_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)
}

View File

@ -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 = &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
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]
}