mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-04-19 06:31:13 -03:00
Compare commits
4 Commits
8dbbe71bc6
...
47a3757f73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47a3757f73 | ||
|
|
f278c29733 | ||
|
|
2bab8b5830 | ||
|
|
1dac221684 |
@@ -9,6 +9,8 @@ Deprecations and removed features
|
||||
|
||||
Interactive improvements
|
||||
------------------------
|
||||
- :doc:`prompt_pwd <cmds/prompt_pwd>` now strips control characters.
|
||||
- Background color and underline color specified in :envvar:`fish_color_valid_path` are now respected (:issue:`12622`).
|
||||
|
||||
Improved terminal support
|
||||
-------------------------
|
||||
@@ -16,14 +18,16 @@ Improved terminal support
|
||||
Other improvements
|
||||
------------------
|
||||
- History is no longer corrupted with NUL bytes when fish receives SIGTERM or SIGHUP (:issue:`10300`).
|
||||
- ``fish_update_completions`` now handles groff ``\X'...'`` device control escapes, fixing completion generation for man pages produced by help2man 1.50 and later (such as coreutils 9.10).
|
||||
- :doc:`fish_update_completions <cmds/fish_update_completions>` now handles groff ``\X'...'`` device control escapes, fixing completion generation for man pages produced by help2man 1.50 and later (such as coreutils 9.10).
|
||||
|
||||
For distributors and developers
|
||||
-------------------------------
|
||||
- When the default global config directory (``$PREFIX/etc/fish``) exists but has been overridden with ``-DCMAKE_INSTALL_SYSCONFDIR``, fish will now respect that override (:issue:`10748`).
|
||||
- When the default global config directory (``$PREFIX/etc/fish``) exists but has been overridden via ``-DCMAKE_INSTALL_SYSCONFDIR``, fish will now respect that override (:issue:`10748`).
|
||||
|
||||
Regression fixes:
|
||||
-----------------
|
||||
- Vi mode ``dl`` (:issue:`12461`).
|
||||
- (from 4.6) Backspace after newline (:issue:`12583`).
|
||||
|
||||
fish 4.6.0 (released March 28, 2026)
|
||||
====================================
|
||||
|
||||
@@ -237,7 +237,7 @@ Optional Commands
|
||||
``\e]0; Pt \e\\``
|
||||
- ts
|
||||
- Set terminal window title (OSC 0). Used in :doc:`fish_title <cmds/fish_title>`.
|
||||
* - ``\e]2; Pt \e\\``
|
||||
* - ``\e]1; Pt \e\\``
|
||||
- ts
|
||||
- Set terminal tab title (OSC 1). Used in :doc:`fish_tab_title <cmds/fish_tab_title>`.
|
||||
* - ``\e]7;file:// Pt / Pt \e\\``
|
||||
|
||||
@@ -26,7 +26,8 @@ function prompt_pwd --description 'short CWD for the prompt'
|
||||
or set -l fish_prompt_pwd_full_dirs 1
|
||||
|
||||
for path in $argv
|
||||
set -l tmp (__fish_unexpand_tilde $path)
|
||||
# Strip control characters to avoid injecting terminal escape sequences into the prompt.
|
||||
set -l tmp (__fish_unexpand_tilde $path | string replace -ra '[[:cntrl:]]' '')
|
||||
|
||||
if test "$fish_prompt_pwd_dir_length" -eq 0
|
||||
echo $tmp
|
||||
|
||||
@@ -117,7 +117,7 @@ fn generate_output_string(seq: &[Key], user: bool, bind: &InputMapping) -> WStri
|
||||
if key.modifiers == Modifiers::ALT {
|
||||
out.push_utfstr(&char_to_symbol('\x1b', i == 0));
|
||||
out.push_utfstr(&char_to_symbol(
|
||||
if key.codepoint == key::Escape {
|
||||
if key.codepoint == key::ESCAPE {
|
||||
'\x1b'
|
||||
} else {
|
||||
key.codepoint
|
||||
|
||||
20
src/input.rs
20
src/input.rs
@@ -361,19 +361,19 @@ pub fn init_input() {
|
||||
};
|
||||
|
||||
add(vec![], "self-insert");
|
||||
add(vec![Key::from_raw(key::Enter)], "execute");
|
||||
add(vec![Key::from_raw(key::Tab)], "complete");
|
||||
add(vec![Key::from_raw(key::ENTER)], "execute");
|
||||
add(vec![Key::from_raw(key::TAB)], "complete");
|
||||
add(vec![ctrl('c')], "cancel-commandline");
|
||||
add(vec![ctrl('d')], "exit");
|
||||
add(vec![ctrl('e')], "bind");
|
||||
add(vec![ctrl('s')], "pager-toggle-search");
|
||||
add(vec![ctrl('u')], "backward-kill-line");
|
||||
add(vec![Key::from_raw(key::Backspace)], "backward-delete-char");
|
||||
add(vec![Key::from_raw(key::BACKSPACE)], "backward-delete-char");
|
||||
// Arrows - can't have functions, so *-or-search isn't available.
|
||||
add(vec![Key::from_raw(key::Up)], "up-line");
|
||||
add(vec![Key::from_raw(key::Down)], "down-line");
|
||||
add(vec![Key::from_raw(key::Right)], "forward-char");
|
||||
add(vec![Key::from_raw(key::Left)], "backward-char");
|
||||
add(vec![Key::from_raw(key::UP)], "up-line");
|
||||
add(vec![Key::from_raw(key::DOWN)], "down-line");
|
||||
add(vec![Key::from_raw(key::RIGHT)], "forward-char");
|
||||
add(vec![Key::from_raw(key::LEFT)], "backward-char");
|
||||
// Emacs style
|
||||
add(vec![ctrl('p')], "up-line");
|
||||
add(vec![ctrl('n')], "down-line");
|
||||
@@ -585,11 +585,11 @@ fn try_peek_sequence(
|
||||
!seq.is_empty(),
|
||||
"Empty sequence passed to try_peek_sequence"
|
||||
);
|
||||
let mut prev = Key::from_raw(key::Invalid);
|
||||
let mut prev = Key::from_raw(key::INVALID);
|
||||
for key in seq {
|
||||
// If we just read an escape, we need to add a timeout for the next char,
|
||||
// to distinguish between the actual escape key and an "alt"-modifier.
|
||||
let escaped = *style != KeyNameStyle::Plain && prev == Key::from_raw(key::Escape);
|
||||
let escaped = *style != KeyNameStyle::Plain && prev == Key::from_raw(key::ESCAPE);
|
||||
let Some(spec) = self.next_is_char(style, *key, escaped) else {
|
||||
return false;
|
||||
};
|
||||
@@ -650,7 +650,7 @@ struct MatchedMapping<'a> {
|
||||
if self.try_peek_sequence(&m.key_name_style, &m.seq, &mut quality) {
|
||||
// // A binding for just escape should also be deferred
|
||||
// // so escape sequences take precedence.
|
||||
let is_escape = m.seq == vec![Key::from_raw(key::Escape)];
|
||||
let is_escape = m.seq == vec![Key::from_raw(key::ESCAPE)];
|
||||
let is_perfect_match = quality
|
||||
.iter()
|
||||
.all(|key_match| *key_match == KeyMatchQuality::Exact);
|
||||
|
||||
@@ -200,13 +200,13 @@ pub(crate) fn codepoint_text(&self) -> Option<char> {
|
||||
if modifiers.is_some() {
|
||||
return None;
|
||||
}
|
||||
if c == key::Space {
|
||||
if c == key::SPACE {
|
||||
return Some(' ');
|
||||
}
|
||||
if c == key::Enter {
|
||||
if c == key::ENTER {
|
||||
return Some('\n');
|
||||
}
|
||||
if c == key::Tab {
|
||||
if c == key::TAB {
|
||||
return Some('\t');
|
||||
}
|
||||
if fish_is_pua(c) || u32::from(c) <= 27 {
|
||||
@@ -817,10 +817,10 @@ fn readch(&mut self) -> CharEvent {
|
||||
continue;
|
||||
}
|
||||
let mut seq = WString::new();
|
||||
if key.is_some_and(|key| key.key == Key::from_raw(key::Invalid)) {
|
||||
if key.is_some_and(|key| key.key == Key::from_raw(key::INVALID)) {
|
||||
continue;
|
||||
}
|
||||
assert!(key.is_none_or(|key| key.codepoint != key::Invalid));
|
||||
assert!(key.is_none_or(|key| key.codepoint != key::INVALID));
|
||||
// At this point, the bytes in `buffer` should be parsed as a UTF-8 sequence,
|
||||
// or, if they are not valid UTF-8, ignored. On incomplete sequences, another
|
||||
// byte is read and decoding is tried again in the next iteration.
|
||||
@@ -953,15 +953,15 @@ fn parse_escape_sequence(
|
||||
assert!(buffer.len() <= 2);
|
||||
let recursive_invocation = buffer.len() == 2;
|
||||
let Some(next) = self.read_sequence_byte(buffer) else {
|
||||
return Some(KeyEvent::from_raw(key::Escape));
|
||||
return Some(KeyEvent::from_raw(key::ESCAPE));
|
||||
};
|
||||
let invalid = KeyEvent::from_raw(key::Invalid);
|
||||
let invalid = KeyEvent::from_raw(key::INVALID);
|
||||
if recursive_invocation && next == b'\x1b' {
|
||||
return Some(
|
||||
match self.parse_escape_sequence(buffer, have_escape_prefix) {
|
||||
Some(mut nested_sequence) => {
|
||||
if nested_sequence.key == invalid.key {
|
||||
return Some(KeyEvent::from_raw(key::Escape));
|
||||
return Some(KeyEvent::from_raw(key::ESCAPE));
|
||||
}
|
||||
nested_sequence.modifiers.alt = true;
|
||||
nested_sequence
|
||||
@@ -1085,13 +1085,13 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
b'A' => masked_key(key::Up),
|
||||
b'B' => masked_key(key::Down),
|
||||
b'C' => masked_key(key::Right),
|
||||
b'D' => masked_key(key::Left),
|
||||
b'A' => masked_key(key::UP),
|
||||
b'B' => masked_key(key::DOWN),
|
||||
b'C' => masked_key(key::RIGHT),
|
||||
b'D' => masked_key(key::LEFT),
|
||||
b'E' => masked_key('5'), // Numeric keypad
|
||||
b'F' => masked_key(key::End), // PC/xterm style
|
||||
b'H' => masked_key(key::Home), // PC/xterm style
|
||||
b'F' => masked_key(key::END), // PC/xterm style
|
||||
b'H' => masked_key(key::HOME), // PC/xterm style
|
||||
b'M' | b'm' => {
|
||||
flog!(reader, "mouse event");
|
||||
// Generic X10 or modified VT200 sequence, or extended (SGR/1006) mouse
|
||||
@@ -1169,14 +1169,14 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
}
|
||||
b'S' => masked_key(function_key(4)),
|
||||
b'~' => match params[0][0] {
|
||||
1 => masked_key(key::Home), // VT220/tmux style
|
||||
2 => masked_key(key::Insert),
|
||||
3 => masked_key(key::Delete),
|
||||
4 => masked_key(key::End), // VT220/tmux style
|
||||
5 => masked_key(key::PageUp),
|
||||
6 => masked_key(key::PageDown),
|
||||
7 => masked_key(key::Home), // rxvt style
|
||||
8 => masked_key(key::End), // rxvt style
|
||||
1 => masked_key(key::HOME), // VT220/tmux style
|
||||
2 => masked_key(key::INSERT),
|
||||
3 => masked_key(key::DELETE),
|
||||
4 => masked_key(key::END), // VT220/tmux style
|
||||
5 => masked_key(key::PAGE_UP),
|
||||
6 => masked_key(key::PAGE_DOWN),
|
||||
7 => masked_key(key::HOME), // rxvt style
|
||||
8 => masked_key(key::END), // rxvt style
|
||||
11..=15 => masked_key(
|
||||
char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(),
|
||||
),
|
||||
@@ -1236,8 +1236,8 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
|
||||
// Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here.
|
||||
let key = match params[0][0] {
|
||||
57361 => key::PrintScreen,
|
||||
57363 => key::Menu,
|
||||
57361 => key::PRINT_SCREEN,
|
||||
57363 => key::MENU,
|
||||
57399 => '0',
|
||||
57400 => '1',
|
||||
57401 => '2',
|
||||
@@ -1253,18 +1253,18 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
57411 => '*',
|
||||
57412 => '-',
|
||||
57413 => '+',
|
||||
57414 => key::Enter,
|
||||
57414 => key::ENTER,
|
||||
57415 => '=',
|
||||
57417 => key::Left,
|
||||
57418 => key::Right,
|
||||
57419 => key::Up,
|
||||
57420 => key::Down,
|
||||
57421 => key::PageUp,
|
||||
57422 => key::PageDown,
|
||||
57423 => key::Home,
|
||||
57424 => key::End,
|
||||
57425 => key::Insert,
|
||||
57426 => key::Delete,
|
||||
57417 => key::LEFT,
|
||||
57418 => key::RIGHT,
|
||||
57419 => key::UP,
|
||||
57420 => key::DOWN,
|
||||
57421 => key::PAGE_UP,
|
||||
57422 => key::PAGE_DOWN,
|
||||
57423 => key::HOME,
|
||||
57424 => key::END,
|
||||
57425 => key::INSERT,
|
||||
57426 => key::DELETE,
|
||||
cp => {
|
||||
let Some(key) = char::from_u32(cp) else {
|
||||
return invalid_sequence(buffer);
|
||||
@@ -1284,7 +1284,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
Some(base_layout_key),
|
||||
)
|
||||
}
|
||||
b'Z' => KeyEvent::from(shift(key::Tab)),
|
||||
b'Z' => KeyEvent::from(shift(key::TAB)),
|
||||
b'I' => {
|
||||
self.push_front(CharEvent::Implicit(ImplicitEvent::FocusIn));
|
||||
return None;
|
||||
@@ -1310,15 +1310,15 @@ fn parse_ss3(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
let (modifiers, _caps_lock) = parse_mask(raw_mask.saturating_sub(1));
|
||||
#[rustfmt::skip]
|
||||
let key = match code {
|
||||
b' ' => KeyEvent::new(modifiers, key::Space),
|
||||
b'A' => KeyEvent::new(modifiers, key::Up),
|
||||
b'B' => KeyEvent::new(modifiers, key::Down),
|
||||
b'C' => KeyEvent::new(modifiers, key::Right),
|
||||
b'D' => KeyEvent::new(modifiers, key::Left),
|
||||
b'F' => KeyEvent::new(modifiers, key::End),
|
||||
b'H' => KeyEvent::new(modifiers, key::Home),
|
||||
b'I' => KeyEvent::new(modifiers, key::Tab),
|
||||
b'M' => KeyEvent::new(modifiers, key::Enter),
|
||||
b' ' => KeyEvent::new(modifiers, key::SPACE),
|
||||
b'A' => KeyEvent::new(modifiers, key::UP),
|
||||
b'B' => KeyEvent::new(modifiers, key::DOWN),
|
||||
b'C' => KeyEvent::new(modifiers, key::RIGHT),
|
||||
b'D' => KeyEvent::new(modifiers, key::LEFT),
|
||||
b'F' => KeyEvent::new(modifiers, key::END),
|
||||
b'H' => KeyEvent::new(modifiers, key::HOME),
|
||||
b'I' => KeyEvent::new(modifiers, key::TAB),
|
||||
b'M' => KeyEvent::new(modifiers, key::ENTER),
|
||||
b'P' => KeyEvent::new(modifiers, function_key(1)),
|
||||
b'Q' => KeyEvent::new(modifiers, function_key(2)),
|
||||
b'R' => KeyEvent::new(modifiers, function_key(3)),
|
||||
|
||||
94
src/key.rs
94
src/key.rs
@@ -25,25 +25,25 @@ macro_rules! define_special_keys {
|
||||
}
|
||||
|
||||
define_special_keys! {
|
||||
Backspace: 0
|
||||
Delete: 1
|
||||
Escape: 2
|
||||
Enter: 3
|
||||
Up: 4
|
||||
Down: 5
|
||||
Left: 6
|
||||
Right: 7
|
||||
PageUp: 8
|
||||
PageDown: 9
|
||||
Home: 10
|
||||
End: 11
|
||||
Insert: 12
|
||||
Tab: 13
|
||||
Space: 14
|
||||
Menu: 15
|
||||
PrintScreen: 16
|
||||
BACKSPACE: 0
|
||||
DELETE: 1
|
||||
ESCAPE: 2
|
||||
ENTER: 3
|
||||
UP: 4
|
||||
DOWN: 5
|
||||
LEFT: 6
|
||||
RIGHT: 7
|
||||
PAGE_UP: 8
|
||||
PAGE_DOWN: 9
|
||||
HOME: 10
|
||||
END: 11
|
||||
INSERT: 12
|
||||
TAB: 13
|
||||
SPACE: 14
|
||||
MENU: 15
|
||||
PRINT_SCREEN: 16
|
||||
|
||||
Invalid: 255
|
||||
INVALID: 255
|
||||
}
|
||||
|
||||
pub(crate) const MAX_FUNCTION_KEY: u8 = 12;
|
||||
@@ -55,23 +55,23 @@ pub(crate) fn function_key(n: u8) -> char {
|
||||
pub(crate) const KEY_NAMES: &[(char, &wstr)] = &[
|
||||
('-', L!("minus")),
|
||||
(',', L!("comma")),
|
||||
(Backspace, L!("backspace")),
|
||||
(Delete, L!("delete")),
|
||||
(Escape, L!("escape")),
|
||||
(Enter, L!("enter")),
|
||||
(Up, L!("up")),
|
||||
(Down, L!("down")),
|
||||
(Left, L!("left")),
|
||||
(Right, L!("right")),
|
||||
(PageUp, L!("pageup")),
|
||||
(PageDown, L!("pagedown")),
|
||||
(Home, L!("home")),
|
||||
(End, L!("end")),
|
||||
(Insert, L!("insert")),
|
||||
(Tab, L!("tab")),
|
||||
(Space, L!("space")),
|
||||
(Menu, L!("menu")),
|
||||
(PrintScreen, L!("printscreen")),
|
||||
(BACKSPACE, L!("backspace")),
|
||||
(DELETE, L!("delete")),
|
||||
(ESCAPE, L!("escape")),
|
||||
(ENTER, L!("enter")),
|
||||
(UP, L!("up")),
|
||||
(DOWN, L!("down")),
|
||||
(LEFT, L!("left")),
|
||||
(RIGHT, L!("right")),
|
||||
(PAGE_UP, L!("pageup")),
|
||||
(PAGE_DOWN, L!("pagedown")),
|
||||
(HOME, L!("home")),
|
||||
(END, L!("end")),
|
||||
(INSERT, L!("insert")),
|
||||
(TAB, L!("tab")),
|
||||
(SPACE, L!("space")),
|
||||
(MENU, L!("menu")),
|
||||
(PRINT_SCREEN, L!("printscreen")),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -187,25 +187,25 @@ fn ascii_control(c: char) -> char {
|
||||
|
||||
pub(crate) fn canonicalize_keyed_control_char(c: char) -> char {
|
||||
if c == ascii_control('m') {
|
||||
return Enter;
|
||||
return ENTER;
|
||||
}
|
||||
if c == ascii_control('i') {
|
||||
return Tab;
|
||||
return TAB;
|
||||
}
|
||||
if c == ' ' {
|
||||
return Space;
|
||||
return SPACE;
|
||||
}
|
||||
if let Some(tm) = get_terminal_mode_on_startup() {
|
||||
if c == char::from(tm.c_cc[VERASE]) {
|
||||
return Backspace;
|
||||
return BACKSPACE;
|
||||
}
|
||||
}
|
||||
if c == char::from(127) {
|
||||
// when it's not backspace
|
||||
return Delete;
|
||||
return DELETE;
|
||||
}
|
||||
if c == '\x1b' {
|
||||
return Escape;
|
||||
return ESCAPE;
|
||||
}
|
||||
c
|
||||
}
|
||||
@@ -214,7 +214,7 @@ pub(crate) fn canonicalize_unkeyed_control_char(c: u8) -> char {
|
||||
if c == 0 {
|
||||
// For legacy terminals we have to make a decision here; they send NUL on Ctrl-2,
|
||||
// Ctrl-Shift-2 or Ctrl-Backtick, but the most straightforward way is Ctrl-Space.
|
||||
return Space;
|
||||
return SPACE;
|
||||
}
|
||||
// Represent Ctrl-letter combinations in lower-case, to be clear
|
||||
// that Shift is not involved.
|
||||
@@ -350,11 +350,11 @@ pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
|
||||
if had_literal_escape {
|
||||
had_literal_escape = false;
|
||||
if key.modifiers.alt {
|
||||
canonical.push(Key::from_raw(Escape));
|
||||
canonical.push(Key::from_raw(ESCAPE));
|
||||
} else {
|
||||
key.modifiers.alt = true;
|
||||
if key.codepoint == '\x1b' {
|
||||
key.codepoint = Escape;
|
||||
key.codepoint = ESCAPE;
|
||||
}
|
||||
}
|
||||
} else if key.codepoint == '\x1b' {
|
||||
@@ -364,7 +364,7 @@ pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
|
||||
canonical.push(key);
|
||||
}
|
||||
if had_literal_escape {
|
||||
canonical.push(Key::from_raw(Escape));
|
||||
canonical.push(Key::from_raw(ESCAPE));
|
||||
}
|
||||
canonical
|
||||
}
|
||||
@@ -482,9 +482,9 @@ mod tests {
|
||||
fn test_parse_key() {
|
||||
assert_eq!(
|
||||
parse_keys(L!("escape")),
|
||||
Ok(vec![Key::from_raw(key::Escape)])
|
||||
Ok(vec![Key::from_raw(key::ESCAPE)])
|
||||
);
|
||||
assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::Escape)]));
|
||||
assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::ESCAPE)]));
|
||||
assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')]));
|
||||
assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')]));
|
||||
assert!(parse_keys(L!("f0")).is_err());
|
||||
|
||||
@@ -14,3 +14,7 @@ prompt_pwd -D 0 /usr/share/fish/prompts
|
||||
|
||||
prompt_pwd -d1 -D 3 /usr/local/share/fish/prompts
|
||||
# CHECK: /u/l/share/fish/prompts
|
||||
|
||||
# Ensure control characters in paths are stripped
|
||||
prompt_pwd -d 0 /foo/(printf '\e]0;OHNO\a')bar
|
||||
# CHECK: /foo/]0;OHNObar
|
||||
|
||||
Reference in New Issue
Block a user