fish_tab_title to set terminal tab title independent of window title

Some modern terminals allow creating tabs in a single window;
this functionality has a lot of overlap with what a window manager
already provides, so I'm not sure if it's a good idea.  Regardless,
a lot of people still use terminal tabs (or even multiple levels of
tabs via tmux!), so let's add a fish-native way to set the tab title
independent of the window title.

Closes #2692
This commit is contained in:
Johannes Altmanninger
2025-10-05 20:42:03 +02:00
parent ba7bc2be13
commit d6ed5f843e
11 changed files with 147 additions and 75 deletions

View File

@@ -16,6 +16,7 @@ Deprecations and removed features
Interactive improvements
------------------------
- The terminal's tab title can now be set without affecting the window title by defining the :doc:`fish_tab_title <cmds/fish_tab_title>` function (:issue:`2692`).
- :doc:`fish_config prompt {choose,save} <cmds/fish_config>` have been taught to reset :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` in addition to the other prompt functions (:issue:`11937`).
- Fish now hides the portion of a multiline prompt that is scrolled out of view due to a huge command line. This prevents duplicate lines after repainting with partially visible prompt (:issue:`11911`).
- On macOS, fish sets :envvar:`MANPATH` correctly also when that variable was already present in the environment (:issue:`10684`).

View File

@@ -0,0 +1,4 @@
fish_tab_title - define the terminal tab's title
================================================
.. include:: ./fish_title.inc.rst

View File

@@ -0,0 +1,56 @@
Synopsis
--------
.. synopsis::
fish_title
fish_tab_title
::
function fish_title
...
end
function fish_tab_title
...
end
Description
-----------
The ``fish_title`` function is executed before and after a new command is executed or put into the foreground and the output is used as a titlebar message.
The first argument to ``fish_title`` contains the most recently executed foreground command as a string, if any.
This requires that your terminal supports :ref:`programmable titles <term-compat-osc-0>` and the feature is turned on.
To disable setting the title, use an empty function (see below).
To set the terminal tab title to something other than the terminal window title,
define the ``fish_tab_title`` function, which works like ``fish_title`` but overrides that one.
Example
-------
A simple title::
function fish_title
set -q argv[1]; or set argv fish
# Looks like ~/d/fish: git log
# or /e/apt: fish
echo (fish_prompt_pwd_dir_length=1 prompt_pwd): $argv;
end
Do not change the title::
function fish_title
end
Change the tab title only::
function fish_tab_title
echo fish $fish_pid
end

View File

@@ -1,44 +1,4 @@
fish_title - define the terminal's title
========================================
Synopsis
--------
.. synopsis::
fish_title
::
function fish_title
...
end
Description
-----------
The ``fish_title`` function is executed before and after a new command is executed or put into the foreground and the output is used as a titlebar message.
The first argument to fish_title contains the most recently executed foreground command as a string, if any.
This requires that your terminal supports programmable titles and the feature is turned on.
To disable setting the title, use an empty function (see below).
Example
-------
A simple title::
function fish_title
set -q argv[1]; or set argv fish
# Looks like ~/d/fish: git log
# or /e/apt: fish
echo (fish_prompt_pwd_dir_length=1 prompt_pwd): $argv;
end
Do not change the title::
function fish_title
end
.. include:: ./fish_title.inc.rst

View File

@@ -58,6 +58,7 @@ Known functions are a customization point. You can change them to change how you
- :doc:`fish_prompt <cmds/fish_prompt>` and :doc:`fish_right_prompt <cmds/fish_right_prompt>` and :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` to print your prompt.
- :doc:`fish_command_not_found <cmds/fish_command_not_found>` to tell fish what to do when a command is not found.
- :doc:`fish_title <cmds/fish_title>` to change the terminal's title.
- :doc:`fish_tab_title <cmds/fish_tab_title>` to change the terminal tab's title.
- :doc:`fish_greeting <cmds/fish_greeting>` to show a greeting when fish starts.
- :doc:`fish_should_add_to_history <cmds/fish_should_add_to_history>` to determine if a command should be added to history

View File

@@ -6,7 +6,7 @@
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
import glob
from glob import glob
import os.path
import subprocess
import sys
@@ -262,7 +262,7 @@ man_pages = [
),
("faq", "fish-faq", "", [author], 1),
]
for path in sorted(glob.glob("cmds/*")):
for path in sorted(set(glob("cmds/*.rst")) - set(glob("cmds/*.inc.rst"))):
docname = os.path.splitext(path)[0]
cmd = os.path.basename(docname)
man_pages.append((docname, cmd, get_command_description(path, cmd), "", 1))

View File

@@ -254,7 +254,9 @@ save this in config.fish or :ref:`a function file <syntax-function-autoloading>`
Programmable title
------------------
When using most terminals, it is possible to set the text displayed in the titlebar of the terminal window. Fish does this by running the :doc:`fish_title <cmds/fish_title>` function. It is executed before and after a command and the output is used as a titlebar message.
Most terminals allow setting the text displayed in the titlebar of the terminal window.
Fish does this by running the :doc:`fish_title <cmds/fish_title>` function.
It is executed before and after a command and the output is used as a titlebar message.
The :doc:`status current-command <cmds/status>` builtin will always return the name of the job to be put into the foreground (or ``fish`` if control is returning to the shell) when the :doc:`fish_title <cmds/fish_title>` function is called. The first argument will contain the most recently executed foreground command as a string.

View File

@@ -263,10 +263,16 @@ Optional Commands
-
- Disable bracketed paste.
- XTerm
* - ``\e]0; Pt \x07``
* - .. _term-compat-osc-0:
``\e]0; Pt \x07``
- ts
- Set window title (OSC 0).
- Set terminal window title (OSC 0). Used in :doc:`fish_title <cmds/fish_title>`.
- XTerm
* - ``\e]2; Pt \x07``
- ts
- Set terminal tab title (OSC 2). Used in :doc:`fish_tab_title <cmds/fish_tab_title>`.
- iTerm2
* - ``\e]7;file:// Pt / Pt \x07``
-
- Report working directory (OSC 7).

View File

@@ -127,8 +127,8 @@
use crate::terminal::Output;
use crate::terminal::Outputter;
use crate::terminal::TerminalCommand::{
ClearScreen, DecrstAlternateScreenBuffer, DecsetAlternateScreenBuffer, DecsetShowCursor,
Osc0WindowTitle, Osc133CommandFinished, Osc133CommandStart, QueryCursorPosition,
self, ClearScreen, DecrstAlternateScreenBuffer, DecsetAlternateScreenBuffer, DecsetShowCursor,
Osc0WindowTitle, Osc2TabTitle, Osc133CommandFinished, Osc133CommandStart, QueryCursorPosition,
QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute, QueryXtgettcap,
QueryXtversion,
};
@@ -4638,46 +4638,83 @@ pub fn fish_is_unwinding_for_exit() -> bool {
/// Write the title to the titlebar. This function is called just before a new application starts
/// executing and just after it finishes.
///
/// \param cmd Command line string passed to \c fish_title if is defined.
/// \param parser The parser to use for autoloading fish_title.
/// \param cmd Command line string passed to the title functions that are defined.
/// \param parser The parser to use for autoloading title functions.
/// \param reset_cursor_position If set, issue a \r so the line driver knows where we are
pub fn reader_write_title(
cmd: &wstr,
parser: &Parser,
reset_cursor_position: bool, /* = true */
) {
fn write_title<'a>(
parser: &Parser,
out: &mut BufferedOutputter,
cmd: &wstr,
osc: fn(&'a [WString]) -> TerminalCommand<'a>,
function_name: &wstr,
fallback_title: Option<&wstr>,
title_buffer: &'a mut Vec<WString>,
) -> bool {
let mut title_function_call;
let mut title_command = fallback_title;
if function::exists(function_name, parser) {
title_function_call = function_name.to_owned();
if !cmd.is_empty() {
title_function_call.push(' ');
title_function_call.push_utfstr(&escape_string(
cmd,
EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | EscapeFlags::NO_TILDE),
));
}
title_command = Some(&title_function_call);
}
let Some(title_command) = title_command else {
return false;
};
let _ = exec_subshell(
title_command,
parser,
Some(title_buffer),
/*apply_exit_status=*/ false,
);
if !title_buffer.is_empty() {
out.write_command(osc(&*title_buffer));
return true;
}
false
}
let _scoped = parser.push_scope(|s| {
s.is_interactive = false;
s.suppress_fish_trace = true;
});
let mut fish_title_command = DEFAULT_TITLE.to_owned();
if function::exists(L!("fish_title"), parser) {
fish_title_command = L!("fish_title").to_owned();
if !cmd.is_empty() {
fish_title_command.push(' ');
fish_title_command.push_utfstr(&escape_string(
cmd,
EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | EscapeFlags::NO_TILDE),
));
}
}
let mut out = BufferedOutputter::new(Outputter::stdoutput());
let mut written = false;
let mut lst = vec![];
let _ = exec_subshell(
&fish_title_command,
written |= write_title(
parser,
Some(&mut lst),
/*apply_exit_status=*/ false,
&mut out,
cmd,
Osc0WindowTitle,
L!("fish_title"),
Some(DEFAULT_TITLE),
&mut lst,
);
written |= write_title(
parser,
&mut out,
cmd,
Osc2TabTitle,
L!("fish_tab_title"),
/*default_title=*/ None,
&mut lst,
);
let mut out = BufferedOutputter::new(Outputter::stdoutput());
if !lst.is_empty() {
out.write_command(Osc0WindowTitle(&lst));
}
out.reset_text_face();
if reset_cursor_position && !lst.is_empty() {
if reset_cursor_position && written {
// Put the cursor back at the beginning of the line (issue #2453).
out.write_bytes(b"\r");
}

View File

@@ -102,6 +102,7 @@ pub(crate) enum TerminalCommand<'a> {
// Note that OSC 7 and OSC 52 are written from fish script, and OSC 8 is written in our
// man pages (via "man_show_urls").
Osc0WindowTitle(&'a [WString]),
Osc2TabTitle(&'a [WString]),
Osc133CommandStart(&'a wstr),
Osc133PromptStart,
Osc133CommandFinished(libc::c_int),
@@ -175,7 +176,8 @@ fn write(out: &mut impl Output, sequence: &'static [u8]) -> bool {
ModifyOtherKeysDisable => write(self, b"\x1b[>4;0m"),
ApplicationKeypadModeEnable => write(self, b"\x1b="),
ApplicationKeypadModeDisable => write(self, b"\x1b>"),
Osc0WindowTitle(title) => osc_0_window_title(self, title),
Osc0WindowTitle(title) => osc_0_or_2_terminal_title(self, false, title),
Osc2TabTitle(title) => osc_0_or_2_terminal_title(self, true, title),
Osc133PromptStart => osc_133_prompt_start(self),
Osc133CommandStart(command) => osc_133_command_start(self, command),
Osc133CommandFinished(s) => osc_133_command_finished(self, s),
@@ -369,8 +371,8 @@ fn query_kitty_progressive_enhancements(out: &mut impl Output) -> bool {
true
}
fn osc_0_window_title(out: &mut impl Output, title: &[WString]) -> bool {
out.write_bytes(b"\x1b]0;");
fn osc_0_or_2_terminal_title(out: &mut impl Output, is_2: bool, title: &[WString]) -> bool {
out.write_bytes(if is_2 { b"\x1b]2;" } else { b"\x1b]0;" });
for title_line in title {
out.write_bytes(&wcs2bytes(title_line));
}

View File

@@ -17,6 +17,9 @@ set -g fish_greeting ''
function fish_title
end
if functions -q fish_tab_title
echo >&2 'ERROR: fish_tab_title unexpectedly defined in test environment'
end
function _marker -d '_marker string - prints @MARKER:$string@'
echo "@MARKER:$argv[1]@"