diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f3fd4c739..506dc31e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,19 @@ fish ?.?.? (released ???) ========================= +Notable improvements and fixes +------------------------------ + +Deprecations and removed features +--------------------------------- + +Interactive improvements +------------------------ +- When typing immediately after starting fish, the first prompt is now rendered correctly. + +For distributors and developers +------------------------------- + fish 4.2.1 (released November 13, 2025) ======================================= diff --git a/src/reader/reader.rs b/src/reader/reader.rs index 38d16464a..2d39822d2 100644 --- a/src/reader/reader.rs +++ b/src/reader/reader.rs @@ -392,7 +392,7 @@ pub fn reader_pop() { if let Some(new_reader) = current_data() { new_reader .screen - .reset_abandoning_line(termsize_last().width()); + .reset_abandoning_line(Some(termsize_last().width())); } else { Outputter::stdoutput().borrow_mut().reset_text_face(); *commandline_state_snapshot() = CommandlineState::new(); @@ -2338,12 +2338,9 @@ fn readline( // This means that `printf %s foo; fish` will overwrite the `foo`, // but that's a smaller problem than having the omitted newline char // appear constantly. - // - // I can't see a good way around this. - if !self.first_prompt { - self.screen.reset_abandoning_line(termsize_last().width()); - } + let trusted_width = (!self.first_prompt).then_some(termsize_last().width()); self.first_prompt = false; + self.screen.reset_abandoning_line(trusted_width); if !self.conf.event.is_empty() { event::fire_generic(self.parser, self.conf.event.to_owned(), vec![]); @@ -2836,7 +2833,8 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { Edit::new(0..self.command_line_len(), L!("").to_owned()), ); if c == rl::CancelCommandline { - self.screen.reset_abandoning_line(termsize_last().width()); + self.screen + .reset_abandoning_line(Some(termsize_last().width())); } // Post fish_cancel. @@ -3117,7 +3115,8 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { L!("fish_posterror").to_owned(), vec![self.command_line.text().to_owned()], ); - self.screen.reset_abandoning_line(termsize_last().width()); + self.screen + .reset_abandoning_line(Some(termsize_last().width())); } } rl::HistoryPrefixSearchBackward diff --git a/src/screen.rs b/src/screen.rs index b33bc971f..e2783e4de 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -672,7 +672,7 @@ pub fn offset_in_cmdline_given_cursor( /// abandoning the current line and going to the next line. /// If clear_to_eos is set, /// The screen width must be provided for the PROMPT_SP hack. - pub fn reset_abandoning_line(&mut self, screen_width: usize) { + pub fn reset_abandoning_line(&mut self, screen_width: Option) { self.actual.cursor.y = 0; self.actual.clear_lines(); self.actual_left_prompt = None; @@ -685,11 +685,16 @@ pub fn reset_abandoning_line(&mut self, screen_width: usize) { } } -fn abandon_line_string(screen_width: usize) -> Vec { +fn abandon_line_string(screen_width: Option) -> Vec { use std::iter::repeat_n; - // Do the PROMPT_SP hack. + + let Some(screen_width) = screen_width else { + return vec![b'\r']; + }; + let mut abandon_line_string = Vec::with_capacity(screen_width + 32); + // Do the PROMPT_SP hack. // Don't need to check for fish_wcwidth errors; this is done when setting up // omitted_newline_char in common.rs. let non_space_width = get_omitted_newline_width(); @@ -752,6 +757,13 @@ fn abandon_line_string(screen_width: usize) -> Vec { // pasting your terminal log becomes a pain. This commit clears that line, making it an // actual empty line. abandon_line_string.write_command(ClearToEndOfLine); + abandon_line_string.push(b'\r'); + // Clear entire line. Zsh doesn't do this. Fish added this with commit 4417a6ee: If you have + // a prompt preceded by a new line, you'll get a line full of spaces instead of an empty + // line above your prompt. This doesn't make a difference in normal usage, but copying and + // pasting your terminal log becomes a pain. This commit clears that line, making it an + // actual empty line. + abandon_line_string.write_command(ClearToEndOfLine); abandon_line_string } diff --git a/tests/checks/tmux-first-prompt.fish b/tests/checks/tmux-first-prompt.fish new file mode 100644 index 000000000..90b87f24a --- /dev/null +++ b/tests/checks/tmux-first-prompt.fish @@ -0,0 +1,15 @@ +#RUN: %fish %s +#REQUIRES: command -v tmux + +tmux_wait=false \ + isolated-tmux-start -C ' + function fish_prompt + command printf "%% " + end +' + +isolated-tmux send-keys 'echo hello' +tmux-sleep +tmux-sleep +isolated-tmux capture-pane -p +# CHECK: % echo hello diff --git a/tests/test_functions/isolated-tmux-start.fish b/tests/test_functions/isolated-tmux-start.fish index 1796ebac4..984ef321b 100644 --- a/tests/test_functions/isolated-tmux-start.fish +++ b/tests/test_functions/isolated-tmux-start.fish @@ -57,6 +57,10 @@ function isolated-tmux-start --wraps fish # Resize window so we can attach to tmux session without changing panel size. isolated-tmux resize-window $size + if test "$tmux_wait" = false + return + end + # Loop a bit, until we get an initial prompt. for i in (seq 50) if test -n "$(isolated-tmux capture-pane -p)"