mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
When a program running in kitty reports progress of a task display it as a percentage in the tab title
This commit is contained in:
@@ -84,10 +84,12 @@ consumption to do the same tasks.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.38.2 [future]
|
||||
0.39.0 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- diff kitten: Automatically use dark/light color scheme based on the color scheme of the parent terminal. Can be controlled via the new :opt:`kitten-diff.color_scheme` option. Note that this is a **behavior change** by default. (:iss:`8170`)
|
||||
- diff kitten: Automatically use dark/light color scheme based on the color scheme of the parent terminal. Can be controlled via the new :opt:`kitten-diff.color_scheme` option. Note that this is a **default behavior change** (:iss:`8170`)
|
||||
|
||||
- When a program running in kitty reports progress of a task display it as a percentage in the tab title. Controlled by the :opt:`tab_title_template` option
|
||||
|
||||
- When mapping a custom kitten allow using shell escaping for the kitten path (:iss:`8178`)
|
||||
|
||||
|
||||
@@ -1400,7 +1400,7 @@ A value of zero means that no limit is applied.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}"',
|
||||
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}"',
|
||||
option_type='tab_title_template',
|
||||
long_text='''
|
||||
A template to render the tab title. The default just renders the title with
|
||||
@@ -1431,6 +1431,12 @@ use :code:`{sup.index}`. All data available is:
|
||||
The maximum title length available.
|
||||
:code:`keyboard_mode`
|
||||
The name of the current :ref:`keyboard mode <modal_mappings>` or the empty string if no keyboard mode is active.
|
||||
:code:`tab.last_focused_progress_percent`
|
||||
If a command running in a window reports the progress for a task, show this progress as a percentage
|
||||
from the most recently focused window in the tab. Empty string if no progress is reported.
|
||||
:code:`tab.progress_percent`
|
||||
If a command running in a window reports the progress for a task, show this progress as a percentage
|
||||
from all windows in the tab, averaged. Empty string is no progress is reported.
|
||||
|
||||
Note that formatting is done by Python's string formatting machinery, so you can
|
||||
use, for instance, :code:`{layout_name[:2].upper()}` to show only the first two
|
||||
|
||||
3
kitty/options/types.py
generated
3
kitty/options/types.py
generated
@@ -607,7 +607,7 @@ class Options:
|
||||
tab_separator: str = ' ┇'
|
||||
tab_switch_strategy: choices_for_tab_switch_strategy = 'previous'
|
||||
tab_title_max_length: int = 0
|
||||
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}'
|
||||
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}'
|
||||
term: str = 'xterm-kitty'
|
||||
terminfo_type: choices_for_terminfo_type = 'path'
|
||||
text_composition_strategy: str = 'platform'
|
||||
@@ -1039,6 +1039,7 @@ defaults.mouse_map = [
|
||||
MouseMapping(button=1, mods=5, definition='mouse_show_command_output'),
|
||||
]
|
||||
|
||||
|
||||
nullable_colors = frozenset({
|
||||
'cursor'
|
||||
'cursor_text_color'
|
||||
|
||||
55
kitty/progress.py
Normal file
55
kitty/progress.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from .fast_data_types import monotonic
|
||||
from .utils import log_error
|
||||
|
||||
|
||||
class ProgressState(Enum):
|
||||
unset = 0
|
||||
set = 1
|
||||
error = 2
|
||||
indeterminate = 3
|
||||
paused = 4
|
||||
|
||||
|
||||
class Progress:
|
||||
|
||||
state: ProgressState = ProgressState.unset
|
||||
percent: int = 0
|
||||
last_update_at: float = 0.
|
||||
clear_timeout: float = 60.0
|
||||
finished_clear_timeout: float = 5.0
|
||||
|
||||
def update(self, st: int, percent: int = -1) -> None:
|
||||
self.last_update_at = monotonic()
|
||||
if st == 0:
|
||||
self.state = ProgressState.unset
|
||||
self.percent = 0
|
||||
elif st == 1:
|
||||
self.state = ProgressState.set
|
||||
self.percent = max(0, min(percent, 100))
|
||||
elif st == 2:
|
||||
self.state = ProgressState.error
|
||||
self.percent = 0
|
||||
elif st == 3:
|
||||
self.state = ProgressState.indeterminate
|
||||
self.percent = 0
|
||||
elif st == 4:
|
||||
self.state = ProgressState.paused
|
||||
if percent > -1:
|
||||
self.percent = max(0, min(percent, 100))
|
||||
else:
|
||||
log_error(f'Unknown OSC 9;4 state: {st}')
|
||||
|
||||
def clear_progress(self) -> bool:
|
||||
time_since_last_update = monotonic() - self.last_update_at
|
||||
threshold = self.finished_clear_timeout if self.percent == 100 and self.state is ProgressState.set else self.clear_timeout
|
||||
if time_since_last_update >= threshold:
|
||||
self.state = ProgressState.unset
|
||||
self.percent = 0
|
||||
return True
|
||||
return False
|
||||
@@ -29,6 +29,7 @@ from .fast_data_types import (
|
||||
viewport_for_window,
|
||||
wcswidth,
|
||||
)
|
||||
from .progress import ProgressState
|
||||
from .rgb import alpha_blend, color_as_sgr, color_from_int, to_color
|
||||
from .types import WindowGeometry, run_once
|
||||
from .typing import EdgeLiteral, PowerlineStyle
|
||||
@@ -48,6 +49,9 @@ class TabBarData(NamedTuple):
|
||||
active_bg: Optional[int]
|
||||
inactive_fg: Optional[int]
|
||||
inactive_bg: Optional[int]
|
||||
num_of_windows_with_progress: int
|
||||
total_progress: int
|
||||
last_focused_window_with_progress_id: int
|
||||
|
||||
|
||||
class DrawData(NamedTuple):
|
||||
@@ -219,6 +223,36 @@ class TabAccessor:
|
||||
tab = get_boss().tab_for_id(self.tab_id)
|
||||
return os.path.basename((tab.get_exe_of_active_window(oldest=True) if tab else '') or '')
|
||||
|
||||
@property
|
||||
def last_focused_progress_percent(self) -> str:
|
||||
tab = get_boss().tab_for_id(self.tab_id)
|
||||
if not tab or not tab.last_focused_window_with_progress_id:
|
||||
return ''
|
||||
w = get_boss().window_id_map.get(tab.last_focused_window_with_progress_id)
|
||||
if w is None:
|
||||
return ''
|
||||
if w.progress.state is ProgressState.error:
|
||||
return '\u26a0\ufe0f '
|
||||
if w.progress.state is ProgressState.unset:
|
||||
return ''
|
||||
if w.progress.state is ProgressState.indeterminate:
|
||||
return '🔄 '
|
||||
p = f'{w.progress.percent}% '
|
||||
if w.progress.state is ProgressState.paused:
|
||||
return f'⏸ {p}'
|
||||
return p
|
||||
|
||||
@property
|
||||
def progress_percent(self) -> str:
|
||||
tab = get_boss().tab_for_id(self.tab_id)
|
||||
if not tab or not tab.last_focused_window_with_progress_id:
|
||||
return ''
|
||||
if tab.num_of_windows_with_progress <= 1:
|
||||
return self.last_focused_progress_percent
|
||||
p = int(tab.total_progress / tab.num_of_windows_with_progress)
|
||||
return f'{p}% '
|
||||
|
||||
|
||||
|
||||
safe_builtins = {
|
||||
'max': max, 'min': min, 'str': str, 'repr': repr, 'abs': abs, 'len': len, 'chr': chr, 'ord': ord, 're': re,
|
||||
|
||||
@@ -52,6 +52,7 @@ from .fast_data_types import (
|
||||
)
|
||||
from .layout.base import Layout
|
||||
from .layout.interface import create_layout_object_for, evict_cached_layouts
|
||||
from .progress import ProgressState
|
||||
from .tab_bar import TabBar, TabBarData
|
||||
from .types import ac
|
||||
from .typing import EdgeLiteral, SessionTab, SessionType, TypedDict
|
||||
@@ -125,6 +126,9 @@ class Tab: # {{{
|
||||
inactive_fg: Optional[int] = None
|
||||
inactive_bg: Optional[int] = None
|
||||
confirm_close_window_id: int = 0
|
||||
num_of_windows_with_progress: int = 0
|
||||
total_progress: int = 0
|
||||
last_focused_window_with_progress_id: int = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -163,6 +167,23 @@ class Tab: # {{{
|
||||
self._set_current_layout(l0)
|
||||
self.startup(session_tab)
|
||||
|
||||
def update_progress(self) -> None:
|
||||
self.num_of_windows_with_progress = 0
|
||||
self.total_progress = 0
|
||||
self.last_focused_window_with_progress_id = 0
|
||||
focused_at = 0.
|
||||
for window in self:
|
||||
p = window.progress
|
||||
if p.state is ProgressState.unset:
|
||||
continue
|
||||
if p.state in (ProgressState.set, ProgressState.paused):
|
||||
self.total_progress += p.percent
|
||||
self.num_of_windows_with_progress += 1
|
||||
if window.last_focused_at > focused_at or (not window.last_focused_at and window.id > self.last_focused_window_with_progress_id):
|
||||
focused_at = window.last_focused_at
|
||||
self.last_focused_window_with_progress_id = window.id
|
||||
self.mark_tab_bar_dirty()
|
||||
|
||||
def has_single_window_visible(self) -> bool:
|
||||
if self.current_layout.only_active_window_visible:
|
||||
return True
|
||||
@@ -1245,7 +1266,8 @@ class TabManager: # {{{
|
||||
title, t is at, needs_attention, t.id,
|
||||
len(t), t.num_window_groups, t.current_layout.name or '',
|
||||
has_activity_since_last_focus, t.active_fg, t.active_bg,
|
||||
t.inactive_fg, t.inactive_bg
|
||||
t.inactive_fg, t.inactive_bg, t.num_of_windows_with_progress,
|
||||
t.total_progress, t.last_focused_window_with_progress_id,
|
||||
))
|
||||
return ans
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ from .rgb import to_color
|
||||
from .terminfo import get_capabilities
|
||||
from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
|
||||
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
|
||||
from .progress import Progress
|
||||
from .utils import (
|
||||
color_as_int,
|
||||
docs_url,
|
||||
@@ -627,6 +628,7 @@ class Window:
|
||||
self.keys_redirected_till_ready_from: int = 0
|
||||
self.last_focused_at = 0.
|
||||
self.is_focused: bool = False
|
||||
self.progress = Progress()
|
||||
self.last_resized_at = 0.
|
||||
self.started_at = monotonic()
|
||||
self.created_at = time_ns()
|
||||
@@ -1107,14 +1109,30 @@ class Window:
|
||||
return # unknown OSC 777
|
||||
raw_data = raw_data[len('notify;'):]
|
||||
if osc_code == 9 and raw_data.startswith('4;'):
|
||||
# This is probably the Windows Terminal "progress reporting" conflicting
|
||||
# This is probably the ConEmu "progress reporting" conflicting
|
||||
# implementation which sadly some thoughtless people have
|
||||
# implemented in unix CLI programs. So ignore it rather than
|
||||
# spamming the user with continuous notifications. See for example:
|
||||
# https://github.com/kovidgoyal/kitty/issues/8011
|
||||
# implemented in unix CLI programs.
|
||||
# See for example: https://github.com/kovidgoyal/kitty/issues/8011
|
||||
try:
|
||||
parts = tuple(map(int, raw_data.split(';')))[1:]
|
||||
except Exception:
|
||||
log_error(f'Ignoring malmormed OSC 9;4 progress report: {raw_data!r}')
|
||||
return
|
||||
self.progress.update(*parts[:2])
|
||||
if (tab := self.tabref()) is not None:
|
||||
tab.update_progress()
|
||||
self.clear_progress_if_needed()
|
||||
return
|
||||
get_boss().notification_manager.handle_notification_cmd(self.id, osc_code, raw_data)
|
||||
|
||||
def clear_progress_if_needed(self, timer_id: Optional[int] = None) -> None:
|
||||
# Clear stuck or completed progress
|
||||
if self.progress.clear_progress():
|
||||
if (tab := self.tabref()) is not None:
|
||||
tab.update_progress()
|
||||
else:
|
||||
add_timer(self.clear_progress_if_needed, 1.0, False)
|
||||
|
||||
def on_mouse_event(self, event: dict[str, Any]) -> bool:
|
||||
event['mods'] = event.get('mods', 0) & mod_mask
|
||||
ev = MouseEvent(**event)
|
||||
|
||||
Reference in New Issue
Block a user