From d40019a141e306a90679104acb267cdf412c8e63 Mon Sep 17 00:00:00 2001 From: Raphael Gaschignard Date: Thu, 1 Feb 2024 02:32:19 +0900 Subject: [PATCH] Tidy up the REPL main loop (#11655) While working on #11288 I was having some trouble tracking the main REPL loop, so I've sent in a bunch of tiny refactorings on this branch. These are almost all of the "move code from one place to another" variety, and each commit is meant to be independent, _except for the last one_, which is trying to be a bit more clever to handle the decision of autocd'ing vs running a command. Feel free to just go through each commit and cherry pick the ones that look good. This leads to `evaluate_repl` going from ending on line 715 to ending on line 395. Again, this is mostly just moving code around, but I think this set of changes will make other changes around juggling the stack to avoid cloning easier to review. --- crates/nu-cli/src/repl.rs | 660 ++++++++++++++++++++++++-------------- 1 file changed, 411 insertions(+), 249 deletions(-) diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 0b01710c22..b2a87eb87d 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -17,8 +17,8 @@ use nu_protocol::{ config::NuCursorShape, engine::{EngineState, Stack, StateWorkingSet}, eval_const::create_nu_constant, - report_error, report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, - Span, Spanned, Value, NU_VARIABLE_ID, + report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, + Value, NU_VARIABLE_ID, }; use nu_utils::utils::perf; use reedline::{ @@ -30,7 +30,7 @@ use std::{ io::{self, IsTerminal, Write}, path::PathBuf, sync::atomic::Ordering, - time::Instant, + time::{Duration, Instant}, }; use sysinfo::System; @@ -44,6 +44,9 @@ const PRE_EXECUTE_MARKER: &str = "\x1b]133;C\x1b\\"; // const CMD_FINISHED_MARKER: &str = "\x1b]133;D;{}\x1b\\"; const RESET_APPLICATION_MODE: &str = "\x1b[?1l"; +/// +/// The main REPL loop, including spinning up the prompt itself. +/// pub fn evaluate_repl( engine_state: &mut EngineState, stack: &mut Stack, @@ -57,15 +60,7 @@ pub fn evaluate_repl( let config = engine_state.get_config(); let use_color = config.use_ansi_coloring; - // Guard against invocation without a connected terminal. - // reedline / crossterm event polling will fail without a connected tty - if !std::io::stdin().is_terminal() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!", - )) - .into_diagnostic(); - } + confirm_stdin_is_terminal()?; let mut entry_num = 0; @@ -74,8 +69,7 @@ pub fn evaluate_repl( let start_time = std::time::Instant::now(); // Translate environment variables from Strings to Values if let Some(e) = convert_env_values(engine_state, stack) { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &e); + report_error_new(engine_state, &e); } perf( "translate env vars", @@ -112,23 +106,7 @@ pub fn evaluate_repl( if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); - // Setup history_isolation aka "history per session" - let history_session_id = if history.isolation { - Reedline::create_history_session_id() - } else { - None - }; - - if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format) - { - line_editor = update_line_editor_history( - engine_state, - path, - history, - line_editor, - history_session_id, - )? - }; + line_editor = setup_history(nushell_path, engine_state, line_editor, history)?; perf( "setup history", @@ -169,9 +147,7 @@ pub fn evaluate_repl( ); } - if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() { - warn!("Terminal doesn't support use_kitty_protocol config"); - } + kitty_protocol_healthcheck(engine_state); loop { let loop_start_time = std::time::Instant::now(); @@ -282,8 +258,7 @@ pub fn evaluate_repl( start_time = std::time::Instant::now(); line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &e); + report_error_new(engine_state, &e); Reedline::create() }); perf( @@ -335,26 +310,7 @@ pub fn evaluate_repl( start_time = std::time::Instant::now(); // Changing the line editor based on the found keybindings - line_editor = match create_keybindings(config) { - Ok(keybindings) => match keybindings { - KeybindingsMode::Emacs(keybindings) => { - let edit_mode = Box::new(Emacs::new(keybindings)); - line_editor.with_edit_mode(edit_mode) - } - KeybindingsMode::Vi { - insert_keybindings, - normal_keybindings, - } => { - let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings)); - line_editor.with_edit_mode(edit_mode) - } - }, - Err(e) => { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &e); - line_editor - } - }; + line_editor = setup_keybindings(engine_state, line_editor); perf( "keybindings", start_time, @@ -427,17 +383,9 @@ pub fn evaluate_repl( engine_state.history_config().map(|h| h.file_format), Some(HistoryFileFormat::Sqlite) ); - if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context() - { - line_editor - .update_last_command_context(&|mut c| { - c.start_timestamp = Some(chrono::Utc::now()); - c.hostname = hostname.clone(); - c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); - c - }) - .into_diagnostic()?; // todo: don't stop repl if error here? + if history_supports_meta { + prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor)?; } // Right before we start running the code the user gave us, fire the `pre_execution` @@ -464,127 +412,27 @@ pub fn evaluate_repl( run_ansi_sequence(PRE_EXECUTE_MARKER)?; } + // Actual command execution logic starts from here let start_time = Instant::now(); - let tokens = lex(s.as_bytes(), 0, &[], &[], false); - // Check if this is a single call to a directory, if so auto-cd - let cwd = nu_engine::env::current_dir_str(engine_state, stack)?; - let mut orig = s.clone(); - if orig.starts_with('`') { - orig = trim_quotes_str(&orig).to_string() + match parse_operation(s.clone(), engine_state, stack)? { + ReplOperation::AutoCd { cwd, target, span } => { + do_auto_cd(target, cwd, stack, engine_state, span); + } + ReplOperation::RunCommand(cmd) => { + line_editor = do_run_cmd( + &cmd, + stack, + engine_state, + line_editor, + shell_integration, + entry_num, + )?; + } + // as the name implies, we do nothing in this case + ReplOperation::DoNothing => {} } - let path = nu_path::expand_path_with(&orig, &cwd); - - if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 { - // We have an auto-cd - let (path, span) = { - if !path.exists() { - let working_set = StateWorkingSet::new(engine_state); - - report_error( - &working_set, - &ShellError::DirectoryNotFound { - dir: path.to_string_lossy().to_string(), - span: tokens.0[0].span, - }, - ); - } - let path = nu_path::canonicalize_with(path, &cwd) - .expect("internal error: cannot canonicalize known path"); - (path.to_string_lossy().to_string(), tokens.0[0].span) - }; - - stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown())); - - //FIXME: this only changes the current scope, but instead this environment variable - //should probably be a block that loads the information from the state in the overlay - stack.add_env_var("PWD".into(), Value::string(path.clone(), Span::unknown())); - let cwd = Value::string(cwd, span); - - let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); - let mut shells = if let Some(v) = shells { - v.as_list() - .map(|x| x.to_vec()) - .unwrap_or_else(|_| vec![cwd]) - } else { - vec![cwd] - }; - - let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); - let current_shell = if let Some(v) = current_shell { - v.as_int().unwrap_or_default() as usize - } else { - 0 - }; - - let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL"); - let last_shell = if let Some(v) = last_shell { - v.as_int().unwrap_or_default() as usize - } else { - 0 - }; - - shells[current_shell] = Value::string(path, span); - - stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span)); - stack.add_env_var( - "NUSHELL_LAST_SHELL".into(), - Value::int(last_shell as i64, span), - ); - } else if !s.trim().is_empty() { - trace!("eval source: {}", s); - - let mut cmds = s.split_whitespace(); - if let Some("exit") = cmds.next() { - let mut working_set = StateWorkingSet::new(engine_state); - let _ = parse(&mut working_set, None, s.as_bytes(), false); - - if working_set.parse_errors.is_empty() { - match cmds.next() { - Some(s) => { - if let Ok(n) = s.parse::() { - drop(line_editor); - std::process::exit(n); - } - } - None => { - drop(line_editor); - std::process::exit(0); - } - } - } - } - - if shell_integration { - if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { - let path = cwd.as_string()?; - - // Try to abbreviate string for windows title - let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { - path.replace(&p.as_path().display().to_string(), "~") - } else { - path - }; - let binary_name = s.split_whitespace().next(); - - if let Some(binary_name) = binary_name { - run_ansi_sequence(&format!( - "\x1b]2;{maybe_abbrev_path}> {binary_name}\x07" - ))?; - } - } - } - - eval_source( - engine_state, - stack, - s.as_bytes(), - &format!("entry #{entry_num}"), - PipelineData::empty(), - false, - ); - } let cmd_duration = start_time.elapsed(); stack.add_env_var( @@ -592,77 +440,21 @@ pub fn evaluate_repl( Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()), ); - if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context() - { - line_editor - .update_last_command_context(&|mut c| { - c.duration = Some(cmd_duration); - c.exit_status = stack - .get_env_var(engine_state, "LAST_EXIT_CODE") - .and_then(|e| e.as_i64().ok()); - c - }) - .into_diagnostic()?; // todo: don't stop repl if error here? + if history_supports_meta { + fill_in_result_related_history_metadata( + &s, + engine_state, + cmd_duration, + stack, + &mut line_editor, + )?; } if shell_integration { - run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; - if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { - let path = cwd.as_string()?; - - // Supported escape sequences of Microsoft's Visual Studio Code (vscode) - // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences - if stack.get_env_var(engine_state, "TERM_PROGRAM") - == Some(Value::test_string("vscode")) - { - // If we're in vscode, run their specific ansi escape sequence. - // This is helpful for ctrl+g to change directories in the terminal. - run_ansi_sequence(&format!("\x1b]633;P;Cwd={}\x1b\\", path))?; - } else { - // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir) - run_ansi_sequence(&format!( - "\x1b]7;file://{}{}{}\x1b\\", - percent_encoding::utf8_percent_encode( - &hostname.unwrap_or_else(|| "localhost".to_string()), - percent_encoding::CONTROLS - ), - if path.starts_with('/') { "" } else { "/" }, - percent_encoding::utf8_percent_encode( - &path, - percent_encoding::CONTROLS - ) - ))?; - } - - // Try to abbreviate string for windows title - let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { - path.replace(&p.as_path().display().to_string(), "~") - } else { - path - }; - - // Set window title too - // https://tldp.org/HOWTO/Xterm-Title-3.html - // ESC]0;stringBEL -- Set icon name and window title to string - // ESC]1;stringBEL -- Set icon name to string - // ESC]2;stringBEL -- Set window title to string - run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}\x07"))?; - } - run_ansi_sequence(RESET_APPLICATION_MODE)?; + do_shell_integration_finalize_command(hostname, engine_state, stack)?; } - let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); - line_editor.run_edit_commands(&[ - EditCommand::Clear, - EditCommand::InsertString(repl.buffer.to_string()), - EditCommand::MoveToPosition { - position: repl.cursor_pos, - select: false, - }, - ]); - repl.buffer = "".to_string(); - repl.cursor_pos = 0; - drop(repl); + flush_engine_state_repl_buffer(engine_state, &mut line_editor); } Ok(Signal::CtrlC) => { // `Reedline` clears the line content. New prompt is shown @@ -714,6 +506,364 @@ pub fn evaluate_repl( Ok(()) } +/// +/// Put in history metadata not related to the result of running the command +/// +fn prepare_history_metadata( + s: &str, + hostname: &Option, + engine_state: &EngineState, + line_editor: &mut Reedline, +) -> Result<()> { + if !s.is_empty() && line_editor.has_last_command_context() { + line_editor + .update_last_command_context(&|mut c| { + c.start_timestamp = Some(chrono::Utc::now()); + c.hostname = hostname.clone(); + + c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); + c + }) + .into_diagnostic()?; // todo: don't stop repl if error here? + } + Ok(()) +} + +/// +/// Fills in history item metadata based on the execution result (notably duration and exit code) +/// +fn fill_in_result_related_history_metadata( + s: &str, + engine_state: &EngineState, + cmd_duration: Duration, + stack: &mut Stack, + line_editor: &mut Reedline, +) -> Result<()> { + if !s.is_empty() && line_editor.has_last_command_context() { + line_editor + .update_last_command_context(&|mut c| { + c.duration = Some(cmd_duration); + c.exit_status = stack + .get_env_var(engine_state, "LAST_EXIT_CODE") + .and_then(|e| e.as_i64().ok()); + c + }) + .into_diagnostic()?; // todo: don't stop repl if error here? + } + Ok(()) +} + +/// The kinds of operations you can do in a single loop iteration of the REPL +enum ReplOperation { + /// "auto-cd": change directory by typing it in directly + AutoCd { + /// the current working directory + cwd: String, + /// the target + target: PathBuf, + /// span information for debugging + span: Span, + }, + /// run a command + RunCommand(String), + /// do nothing (usually through an empty string) + DoNothing, +} + +/// +/// Parses one "REPL line" of input, to try and derive intent. +/// Notably, this is where we detect whether the user is attempting an +/// "auto-cd" (writing a relative path directly instead of `cd path`) +/// +/// Returns the ReplOperation we believe the user wants to do +/// +fn parse_operation( + s: String, + engine_state: &EngineState, + stack: &Stack, +) -> Result { + let tokens = lex(s.as_bytes(), 0, &[], &[], false); + // Check if this is a single call to a directory, if so auto-cd + let cwd = nu_engine::env::current_dir_str(engine_state, stack)?; + let mut orig = s.clone(); + if orig.starts_with('`') { + orig = trim_quotes_str(&orig).to_string() + } + + let path = nu_path::expand_path_with(&orig, &cwd); + if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 { + Ok(ReplOperation::AutoCd { + cwd, + target: path, + span: tokens.0[0].span, + }) + } else if !s.trim().is_empty() { + Ok(ReplOperation::RunCommand(s)) + } else { + Ok(ReplOperation::DoNothing) + } +} + +/// +/// Execute an "auto-cd" operation, changing the current working directory. +/// +fn do_auto_cd( + path: PathBuf, + cwd: String, + stack: &mut Stack, + engine_state: &mut EngineState, + span: Span, +) { + let path = { + if !path.exists() { + report_error_new( + engine_state, + &ShellError::DirectoryNotFound { + dir: path.to_string_lossy().to_string(), + span, + }, + ); + } + let path = nu_path::canonicalize_with(path, &cwd) + .expect("internal error: cannot canonicalize known path"); + path.to_string_lossy().to_string() + }; + + stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown())); + + //FIXME: this only changes the current scope, but instead this environment variable + //should probably be a block that loads the information from the state in the overlay + stack.add_env_var("PWD".into(), Value::string(path.clone(), Span::unknown())); + let cwd = Value::string(cwd, span); + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let mut shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let current_shell = if let Some(v) = current_shell { + v.as_int().unwrap_or_default() as usize + } else { + 0 + }; + + let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL"); + let last_shell = if let Some(v) = last_shell { + v.as_int().unwrap_or_default() as usize + } else { + 0 + }; + + shells[current_shell] = Value::string(path, span); + + stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span)); + stack.add_env_var( + "NUSHELL_LAST_SHELL".into(), + Value::int(last_shell as i64, span), + ); +} + +/// +/// Run a command as received from reedline. This is where we are actually +/// running a thing! +/// +fn do_run_cmd( + s: &str, + stack: &mut Stack, + engine_state: &mut EngineState, + // we pass in the line editor so it can be dropped in the case of a process exit + // (in the normal case we don't want to drop it so return it as-is otherwise) + line_editor: Reedline, + shell_integration: bool, + entry_num: usize, +) -> Result { + trace!("eval source: {}", s); + + let mut cmds = s.split_whitespace(); + if let Some("exit") = cmds.next() { + let mut working_set = StateWorkingSet::new(engine_state); + let _ = parse(&mut working_set, None, s.as_bytes(), false); + + if working_set.parse_errors.is_empty() { + match cmds.next() { + Some(s) => { + if let Ok(n) = s.parse::() { + drop(line_editor); + std::process::exit(n); + } + } + None => { + drop(line_editor); + std::process::exit(0); + } + } + } + } + + if shell_integration { + if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { + let path = cwd.as_string()?; + + // Try to abbreviate string for windows title + let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { + path.replace(&p.as_path().display().to_string(), "~") + } else { + path + }; + let binary_name = s.split_whitespace().next(); + + if let Some(binary_name) = binary_name { + run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"))?; + } + } + } + + eval_source( + engine_state, + stack, + s.as_bytes(), + &format!("entry #{entry_num}"), + PipelineData::empty(), + false, + ); + + Ok(line_editor) +} + +/// +/// Output some things and set environment variables so shells with the right integration +/// can have more information about what is going on (after we have run a command) +/// +fn do_shell_integration_finalize_command( + hostname: Option, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result<()> { + run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; + if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { + let path = cwd.as_string()?; + + // Supported escape sequences of Microsoft's Visual Studio Code (vscode) + // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences + if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) { + // If we're in vscode, run their specific ansi escape sequence. + // This is helpful for ctrl+g to change directories in the terminal. + run_ansi_sequence(&format!("\x1b]633;P;Cwd={}\x1b\\", path))?; + } else { + // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir) + run_ansi_sequence(&format!( + "\x1b]7;file://{}{}{}\x1b\\", + percent_encoding::utf8_percent_encode( + &hostname.unwrap_or_else(|| "localhost".to_string()), + percent_encoding::CONTROLS + ), + if path.starts_with('/') { "" } else { "/" }, + percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS) + ))?; + } + + // Try to abbreviate string for windows title + let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { + path.replace(&p.as_path().display().to_string(), "~") + } else { + path + }; + + // Set window title too + // https://tldp.org/HOWTO/Xterm-Title-3.html + // ESC]0;stringBEL -- Set icon name and window title to string + // ESC]1;stringBEL -- Set icon name to string + // ESC]2;stringBEL -- Set window title to string + run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}\x07"))?; + } + run_ansi_sequence(RESET_APPLICATION_MODE)?; + Ok(()) +} + +/// +/// Clear the screen and output anything remaining in the EngineState buffer. +/// +fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &mut Reedline) { + let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); + line_editor.run_edit_commands(&[ + EditCommand::Clear, + EditCommand::InsertString(repl.buffer.to_string()), + EditCommand::MoveToPosition { + position: repl.cursor_pos, + select: false, + }, + ]); + repl.buffer = "".to_string(); + repl.cursor_pos = 0; +} + +/// +/// Setup history management for Reedline +/// +fn setup_history( + nushell_path: &str, + engine_state: &mut EngineState, + line_editor: Reedline, + history: HistoryConfig, +) -> Result { + // Setup history_isolation aka "history per session" + let history_session_id = if history.isolation { + Reedline::create_history_session_id() + } else { + None + }; + + if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format) { + return update_line_editor_history( + engine_state, + path, + history, + line_editor, + history_session_id, + ); + }; + Ok(line_editor) +} + +/// +/// Setup Reedline keybindingds based on the provided config +/// +fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline { + return match create_keybindings(engine_state.get_config()) { + Ok(keybindings) => match keybindings { + KeybindingsMode::Emacs(keybindings) => { + let edit_mode = Box::new(Emacs::new(keybindings)); + line_editor.with_edit_mode(edit_mode) + } + KeybindingsMode::Vi { + insert_keybindings, + normal_keybindings, + } => { + let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings)); + line_editor.with_edit_mode(edit_mode) + } + }, + Err(e) => { + report_error_new(engine_state, &e); + line_editor + } + }; +} + +/// +/// Make sure that the terminal supports the kitty protocol if the config is asking for it +/// +fn kitty_protocol_healthcheck(engine_state: &EngineState) { + if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() { + warn!("Terminal doesn't support use_kitty_protocol config"); + } +} + fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) { let session_id = line_editor .get_history_session_id() @@ -754,6 +904,18 @@ fn update_line_editor_history( Ok(line_editor) } +fn confirm_stdin_is_terminal() -> Result<()> { + // Guard against invocation without a connected terminal. + // reedline / crossterm event polling will fail without a connected tty + if !std::io::stdin().is_terminal() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!", + )) + .into_diagnostic(); + } + Ok(()) +} fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option { match shape { NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),