From b5bb50d742d8a4c1d36f346e463d81d425cf52ed Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 3 Jul 2025 13:13:21 +0200 Subject: [PATCH] builtin commandline: apply commandline+cursor to first top-level reader Historically, `fish -C "commandline echo"` was silently ignored. Make it do the expected thing. This won't affect subsequent readers because we only do it for top-level ones, and reader_pop() will clear the commandline state again. This improves consistency with the parent commit. We probably don't want to support arbitrary readline commands before the first reader is initialized, but setting the initial commandline seems useful: first, it would have helped me in the past for debugging fish. Second, it would allow one to rewrite an application launcher: foot --app-id my-foot-launcher -e fish -C ' set fish_history launcher bind escape exit bind ctrl-\[ exit - function fish_should_add_to_history - false - end - for enter in enter ctrl-j - bind $enter '\'' - history append -- "$(commandline)" - commandline "setsid $(commandline) /dev/null 2>&1 & disown && exit" - commandline -f execute - '\'' - end + commandline "setsid /dev/null 2>&1 & disown && exit" + commandline --cursor $(string length "setsid ") ' which is probably not desirable today because it will disable autosuggestions. Though that could be fixed eventually by making autosuggestions smarter. If we find a generally-useful use case, we should mention this in the changelog. Ref: https://github.com/fish-shell/fish-shell/pull/11570#discussion_r2144544053 --- src/builtins/read.rs | 10 ++++++---- src/reader.rs | 15 +++++++++++---- tests/pexpects/commandline.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/builtins/read.rs b/src/builtins/read.rs index 9f7f4e74d..cfeec69b8 100644 --- a/src/builtins/read.rs +++ b/src/builtins/read.rs @@ -39,7 +39,7 @@ struct Options { prompt: Option, prompt_str: Option, right_prompt: WString, - commandline: WString, + commandline: Option, // If a delimiter was given. Used to distinguish between the default // empty string and a given empty delimiter. delimiter: Option, @@ -100,7 +100,7 @@ fn parse_cmd_opts( opts.array = true; } 'c' => { - opts.commandline = w.woptarg.unwrap().to_owned(); + opts.commandline = Some(w.woptarg.unwrap().to_owned()); } 'd' => { opts.delimiter = Some(w.woptarg.unwrap().to_owned()); @@ -207,7 +207,7 @@ fn read_interactive( silent: bool, prompt: &wstr, right_prompt: &wstr, - commandline: &wstr, + commandline: &Option, inputfd: RawFd, ) -> BuiltinResult { let mut exit_res = Ok(SUCCESS); @@ -238,7 +238,9 @@ fn read_interactive( s.readonly_commandline = false; }) }); - commandline_set_buffer(parser, Some(commandline.to_owned()), None); + if let Some(commandline) = commandline { + commandline_set_buffer(parser, Some(commandline.clone()), None); + } let mline = { let _interactive = parser.push_scope(|s| s.is_interactive = true); diff --git a/src/reader.rs b/src/reader.rs index b9eeaac94..00b44b703 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -317,11 +317,12 @@ pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConf assert_is_main_thread(); let hist = History::with_name(history_name); hist.resolve_pending(); - let data = ReaderData::new(hist, conf); + let is_top_level = reader_data_stack().is_empty(); + let data = ReaderData::new(hist, conf, is_top_level); reader_data_stack().push(data); let data = current_data().unwrap(); data.command_line_changed(EditableLineTag::Commandline, AutosuggestionUpdate::Remove); - if reader_data_stack().len() == 1 { + if is_top_level { reader_interactive_init(parser); } Reader { data, parser } @@ -1201,12 +1202,18 @@ fn reader_received_sighup() -> bool { } impl ReaderData { - fn new(history: Arc, conf: ReaderConfig) -> Pin> { + fn new(history: Arc, conf: ReaderConfig, is_top_level: bool) -> Pin> { let input_data = InputData::new(conf.inputfd); + let mut command_line = EditableLine::default(); + if is_top_level { + let state = commandline_state_snapshot(); + command_line.push_edit(Edit::new(0..0, state.text.clone()), false); + command_line.set_position(state.cursor_pos); + } Pin::new(Box::new(Self { canary: Rc::new(()), conf, - command_line: Default::default(), + command_line, command_line_transient_edit: None, rendered_layout: Default::default(), autosuggestion: Default::default(), diff --git a/tests/pexpects/commandline.py b/tests/pexpects/commandline.py index a3ccec121..8e0956b9d 100644 --- a/tests/pexpects/commandline.py +++ b/tests/pexpects/commandline.py @@ -102,6 +102,18 @@ send(control("k")) sendline('echo "process extent is [$tmp]"') expect_str("process extent is [echo process # comment]") +sendline( + """$fish -C 'commandline "sq 2; exit"; commandline --cursor 1; commandline -i e'""" +) +expect_str("seq 2") +send("\r") +expect_str("1\r\n2\r\n") + +sendline("""$fish -C 'commandline 123; read'""") +expect_str("read> 123") +sendline("456; exit") +expect_str("123456") + # DISABLED because it keeps failing under ASAN # sendline(r"bind ctrl-b 'set tmp (commandline --current-process | count)'") # sendline(r'commandline "echo line1 \\" "# comment" "line2"')