Skip to content

Commit cbca4d6

Browse files
committed
Implement tab_bar_filter
Useful to manage multiple sessions in a single kitty OS Window. Add some docs to sessions.rst describing this use case.
1 parent 7bd912c commit cbca4d6

File tree

10 files changed

+192
-110
lines changed

10 files changed

+192
-110
lines changed

docs/sessions.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,20 @@ When you run the session file in another kitty instance you will see both
229229
windows re-created, as expected with the correct working directories and
230230
running programs.
231231

232+
Managing multi tab sessions in a single OS Window
233+
----------------------------------------------------
234+
235+
The natural way to organise sessions in kitty is one per :term:`os_window`.
236+
However, if you prefer to manage multiple sessions in a single OS Window, you
237+
can configure the kitty tab bar to only show tabs that belong to the currently
238+
active session. To do so, use :opt:`tab_bar_filter` in :file:`kitty.conf` set::
239+
240+
tab_bar_filter session:~ or session:^$
241+
242+
This will restrict the tab bar to only showing tabs from the currently active
243+
session as well tabs that do not belong to any session. Furthermore, when you
244+
are in a window or tab that does not belong to any session, the tab bar will
245+
show the tabs from the most recent active session, to maintain context.
232246

233247
Keyword reference
234248
---------------------

kitty/boss.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
default_save_as_session_opts,
130130
get_os_window_sizing_data,
131131
goto_session,
132+
most_recent_session,
132133
save_as_session,
133134
)
134135
from .shaders import load_shader_programs
@@ -546,6 +547,7 @@ def match_windows(self, match: str, self_window: Optional['Window'] = None, all_
546547
wids = {w.id for w in all_windows}
547548
window_id_limit = max(wids, default=-1) + 1
548549
active_session = self.active_session
550+
prev_active_session = most_recent_session()
549551

550552
def get_matches(location: str, query: str, candidates: set[int]) -> set[int]:
551553
if location == 'id' and query.startswith('-'):
@@ -555,7 +557,7 @@ def get_matches(location: str, query: str, candidates: set[int]) -> set[int]:
555557
return set()
556558
if q < 0:
557559
query = str(window_id_limit + q)
558-
return {wid for wid in candidates if self.window_id_map[wid].matches_query(location, query, tab, self_window, active_session)}
560+
return {wid for wid in candidates if self.window_id_map[wid].matches_query(location, query, tab, self_window, active_session, prev_active_session)}
559561

560562
for wid in search(match, (
561563
'id', 'title', 'pid', 'cwd', 'cmdline', 'num', 'env', 'var', 'recent', 'state', 'neighbor', 'session',
@@ -574,6 +576,8 @@ def match_tabs(self, match: str, all_tabs: Iterable[Tab] | None = None) -> Itera
574576
tim = {t.id: t for t in all_tabs}
575577
tab_id_limit = max(tim, default=-1) + 1
576578
window_id_limit = max(self.window_id_map, default=-1) + 1
579+
active_session = self.active_session
580+
prev_active_session = most_recent_session()
577581

578582
def get_matches(location: str, query: str, candidates: set[int]) -> set[int]:
579583
if location in ('id', 'window_id') and query.startswith('-'):
@@ -584,7 +588,7 @@ def get_matches(location: str, query: str, candidates: set[int]) -> set[int]:
584588
if q < 0:
585589
limit = tab_id_limit if location == 'id' else window_id_limit
586590
query = str(limit + q)
587-
return {wid for wid in candidates if tim[wid].matches_query(location, query, tm)}
591+
return {wid for wid in candidates if tim[wid].matches_query(location, query, tm, active_session, prev_active_session)}
588592

589593
found = False
590594
for tid in search(match, (

kitty/options/definition.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,18 @@
13861386
'''
13871387
)
13881388

1389+
opt('tab_bar_filter', '', long_text='''
1390+
A :ref:`search expression <search_syntax>`. Only tabs that match this expression
1391+
will be shown in the tab bar. The currently active tab is :italic:`always` shown,
1392+
regardless of whether it matches or not. When using this option, the tab bar may
1393+
be displayed with less tabs than specified in :opt:`tab_bar_min_tabs`, as evaluating
1394+
the filter is expensive and is done only at display time. This is most useful when
1395+
using :ref:`sessions <sessions>`. An expression of :code:`session:~ or session:^$`
1396+
will show only tabs that belong to the current session or no session. The various
1397+
tab navigation actions such as :ac:`goto_tab`, :ac:`next_tab`, :ac:`previous_tab`, etc.
1398+
are automatically restricted to work only on matching tabs.
1399+
''')
1400+
13891401
opt('tab_bar_align', 'left',
13901402
choices=('left', 'center', 'right'),
13911403
long_text='''

kitty/options/parse.py

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kitty/options/types.py

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kitty/rc/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ def __call__(self, key: str, opt_name: str | None = None, missing: Any = None) -
114114
115115
The field :code:`session` matches windows that were created in the specified session.
116116
Use the expression :code:`^$` to match windows that were not created in a session and
117-
:code:`.` to match the currently active session.
117+
:code:`.` to match the currently active session and :code:`~` to match either the currently
118+
active sesison or the last active session when no session is active.
118119
119120
When using the :code:`env` field to match on environment variables, you can specify only the environment variable name
120121
or a name and value, for example, :code:`env:MY_ENV_VAR=2`.
@@ -160,8 +161,9 @@ def __call__(self, key: str, opt_name: str | None = None, missing: Any = None) -
160161
active tab, one the previously active tab and so on.
161162
162163
The field :code:`session` matches tabs that were created in the specified session.
163-
Use the expression :code:`^$` to match tabs that were not created in a session and
164-
:code:`.` to match the currently active session.
164+
Use the expression :code:`^$` to match windows that were not created in a session and
165+
:code:`.` to match the currently active session and :code:`~` to match either the currently
166+
active sesison or the last active session when no session is active.
165167
166168
When using the :code:`env` field to match on environment variables, you can specify only the environment variable name
167169
or a name and value, for example, :code:`env:MY_ENV_VAR=2`. Tabs containing any window with the specified environment

kitty/session.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ def append_to_session_history(name: str) -> None:
396396
goto_session_history.append(name)
397397

398398

399+
def most_recent_session() -> str:
400+
return goto_session_history[-1] if goto_session_history else ''
401+
402+
399403
def switch_to_session(boss: BossType, session_name: str) -> bool:
400404
w = window_for_session_name(boss, session_name)
401405
if w is not None:

kitty/tab_bar.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -512,14 +512,27 @@ def draw_tab(
512512
return draw_tab
513513

514514

515+
class CellRange(NamedTuple):
516+
start: int
517+
end: int
518+
519+
520+
class TabExtent(NamedTuple):
521+
tab_id: int
522+
cell_range: CellRange
523+
524+
def shifted(self, shift: int) -> 'TabExtent':
525+
return TabExtent(self.tab_id, CellRange(self.cell_range.start + shift, self.cell_range.end + shift))
526+
527+
515528
class TabBar:
516529

517530
def __init__(self, os_window_id: int):
518531
self.os_window_id = os_window_id
519532
self.num_tabs = 1
520533
self.data_buffer_size = 0
521534
self.blank_rects: tuple[Border, ...] = ()
522-
self.cell_ranges: list[tuple[int, int]] = []
535+
self.tab_extents: Sequence[TabExtent] = ()
523536
self.laid_out_once = False
524537
self.apply_options()
525538

@@ -673,7 +686,7 @@ def update(self, data: Sequence[TabBarData]) -> None:
673686
last_tab = data[-1] if data else None
674687
ed = ExtraData()
675688

676-
def draw_tab(i: int, tab: TabBarData, cell_ranges: list[tuple[int, int]], max_tab_length: int) -> None:
689+
def draw_tab(i: int, tab: TabBarData, cell_ranges: list[TabExtent], max_tab_length: int) -> None:
677690
ed.prev_tab = data[i - 1] if i > 0 else None
678691
ed.next_tab = data[i + 1] if i + 1 < len(data) else None
679692
s.cursor.bg = as_rgb(self.draw_data.tab_bg(t))
@@ -682,7 +695,7 @@ def draw_tab(i: int, tab: TabBarData, cell_ranges: list[tuple[int, int]], max_ta
682695
before = s.cursor.x
683696
end = self.draw_func(self.draw_data, s, t, before, max_tab_length, i + 1, t is last_tab, ed)
684697
s.cursor.bg = s.cursor.fg = 0
685-
cell_ranges.append((before, end))
698+
cell_ranges.append(TabExtent(tab_id=tab.tab_id, cell_range=CellRange(before, end)))
686699
if not ed.for_layout and t is not last_tab and s.cursor.x > s.columns - max_tab_lengths[i+1]:
687700
# Stop if there is no space for next tab
688701
s.cursor.x = s.columns - 2
@@ -722,36 +735,36 @@ def draw_tab(i: int, tab: TabBarData, cell_ranges: list[tuple[int, int]], max_ta
722735

723736
s.cursor.x = 0
724737
s.erase_in_line(2, False)
725-
cr: list[tuple[int, int]] = []
738+
cr: list[TabExtent] = []
726739
ed.for_layout = False
727740
for i, t in enumerate(data):
728741
try:
729742
draw_tab(i, t, cr, max_tab_lengths[i])
730743
except StopIteration:
731744
break
732-
self.cell_ranges = cr
745+
self.tab_extents = cr
733746
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
734747
self.align()
735748
update_tab_bar_edge_colors(self.os_window_id)
736749

737750
def align_with_factor(self, factor: int = 1) -> None:
738-
if not self.cell_ranges:
751+
if not self.tab_extents:
739752
return
740-
end = self.cell_ranges[-1][1]
753+
end = self.tab_extents[-1].cell_range[1]
741754
if end < self.screen.columns - 1:
742755
shift = (self.screen.columns - end) // factor
743756
self.screen.cursor.x = 0
744757
self.screen.insert_characters(shift)
745-
self.cell_ranges = [(s + shift, e + shift) for (s, e) in self.cell_ranges]
758+
self.tab_extents = tuple(te.shifted(shift) for te in self.tab_extents)
746759

747760
def destroy(self) -> None:
748761
self.screen.reset_callbacks()
749762
del self.screen
750763

751-
def tab_at(self, x: int) -> int | None:
764+
def tab_id_at(self, x: int) -> int:
752765
if self.laid_out_once:
753766
x = (x - self.window_geometry.left) // self.cell_width
754-
for i, (a, b) in enumerate(self.cell_ranges):
755-
if a <= x <= b:
756-
return i
757-
return None
767+
for te in self.tab_extents:
768+
if te.cell_range.start <= x <= te.cell_range.end:
769+
return te.tab_id
770+
return 0

0 commit comments

Comments
 (0)