kitty/kitty/tabs.py
Kovid Goyal 0f020d5b37
When passing a directory or a non-executable file as the program to run to kitty open it with the shell, instead of just failing.
Allows using kitty as the program to open directories or shell scripts
from desktop environments.
2021-03-10 14:27:07 +05:30

813 lines
29 KiB
Python

#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import stat
import weakref
from collections import deque
from contextlib import suppress
from functools import partial
from operator import attrgetter
from typing import (
Any, Deque, Dict, Generator, Iterator, List, NamedTuple, Optional, Pattern,
Sequence, Tuple, Union, cast
)
from .borders import Borders
from .child import Child
from .cli_stub import CLIOptions
from .constants import appname, kitty_exe
from .fast_data_types import (
add_tab, attach_window, detach_window, get_boss, mark_tab_bar_dirty,
next_window_id, remove_tab, remove_window, ring_bell, set_active_tab,
set_active_window, swap_tabs, sync_os_window_title
)
from .layout.base import Layout, Rect
from .layout.interface import create_layout_object_for, evict_cached_layouts
from .options_stub import Options
from .tab_bar import TabBar, TabBarData
from .typing import EdgeLiteral, SessionTab, SessionType, TypedDict
from .utils import log_error, platform_window_id, resolved_shell
from .window import Watchers, Window, WindowDict
from .window_list import WindowList
class TabDict(TypedDict):
id: int
is_focused: bool
title: str
layout: str
windows: List[WindowDict]
active_window_history: List[int]
class SpecialWindowInstance(NamedTuple):
cmd: Optional[List[str]]
stdin: Optional[bytes]
override_title: Optional[str]
cwd_from: Optional[int]
cwd: Optional[str]
overlay_for: Optional[int]
env: Optional[Dict[str, str]]
watchers: Optional[Watchers]
def SpecialWindow(
cmd: Optional[List[str]],
stdin: Optional[bytes] = None,
override_title: Optional[str] = None,
cwd_from: Optional[int] = None,
cwd: Optional[str] = None,
overlay_for: Optional[int] = None,
env: Optional[Dict[str, str]] = None,
watchers: Optional[Watchers] = None
) -> SpecialWindowInstance:
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers)
def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) -> None:
with suppress(ValueError):
items.remove(item_id)
items.append(item_id)
if len(items) > maxlen:
items.popleft()
class Tab: # {{{
def __init__(
self,
tab_manager: 'TabManager',
session_tab: Optional['SessionTab'] = None,
special_window: Optional[SpecialWindowInstance] = None,
cwd_from: Optional[int] = None,
no_initial_window: bool = False
):
self.tab_manager_ref = weakref.ref(tab_manager)
self.os_window_id: int = tab_manager.os_window_id
self.id: int = add_tab(self.os_window_id)
if not self.id:
raise Exception('No OS window with id {} found, or tab counter has wrapped'.format(self.os_window_id))
self.opts, self.args = tab_manager.opts, tab_manager.args
self.name = getattr(session_tab, 'name', '')
self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts]
self.borders = Borders(self.os_window_id, self.id, self.opts)
self.windows = WindowList(self)
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i))
self._last_used_layout: Optional[str] = None
self._current_layout_name: Optional[str] = None
self.cwd = self.args.directory
if no_initial_window:
self._set_current_layout(self.enabled_layouts[0])
elif session_tab is None:
sl = self.enabled_layouts[0]
self._set_current_layout(sl)
if special_window is None:
self.new_window(cwd_from=cwd_from)
else:
self.new_special_window(special_window)
else:
if session_tab.cwd:
self.cwd = session_tab.cwd
l0 = session_tab.layout
self._set_current_layout(l0)
self.startup(session_tab)
def take_over_from(self, other_tab: 'Tab') -> None:
self.name, self.cwd = other_tab.name, other_tab.cwd
self.enabled_layouts = list(other_tab.enabled_layouts)
if other_tab._current_layout_name:
self._set_current_layout(other_tab._current_layout_name)
self._last_used_layout = other_tab._last_used_layout
for window in other_tab.windows:
detach_window(other_tab.os_window_id, other_tab.id, window.id)
self.windows = other_tab.windows
self.windows.change_tab(self)
other_tab.windows = WindowList(other_tab)
for window in self.windows:
window.change_tab(self)
attach_window(self.os_window_id, self.id, window.id)
self.relayout()
def _set_current_layout(self, layout_name: str) -> None:
self._last_used_layout = self._current_layout_name
self.current_layout = self.create_layout_object(layout_name)
self._current_layout_name = layout_name
self.mark_tab_bar_dirty()
def startup(self, session_tab: 'SessionTab') -> None:
for cmd in session_tab.windows:
if isinstance(cmd, SpecialWindowInstance):
self.new_special_window(cmd)
else:
from .launch import launch
launch(get_boss(), cmd.opts, cmd.args, target_tab=self, force_target_tab=True)
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
def serialize_state(self) -> Dict[str, Any]:
return {
'version': 1,
'id': self.id,
'window_list': self.windows.serialize_state(),
'current_layout': self._current_layout_name,
'last_used_layout': self._last_used_layout,
'name': self.name,
}
def active_window_changed(self) -> None:
w = self.active_window
set_active_window(self.os_window_id, self.id, 0 if w is None else w.id)
self.mark_tab_bar_dirty()
self.relayout_borders()
self.current_layout.update_visibility(self.windows)
def mark_tab_bar_dirty(self) -> None:
tm = self.tab_manager_ref()
if tm is not None:
tm.mark_tab_bar_dirty()
@property
def active_window(self) -> Optional[Window]:
return self.windows.active_window
@property
def active_window_for_cwd(self) -> Optional[Window]:
return self.windows.active_group_base
@property
def title(self) -> str:
return cast(str, getattr(self.active_window, 'title', appname))
def set_title(self, title: str) -> None:
self.name = title or ''
self.mark_tab_bar_dirty()
def title_changed(self, window: Window) -> None:
if window is self.active_window:
tm = self.tab_manager_ref()
if tm is not None:
tm.title_changed(self)
def on_bell(self, window: Window) -> None:
self.mark_tab_bar_dirty()
def relayout(self) -> None:
if self.windows:
self.current_layout(self.windows)
self.relayout_borders()
def relayout_borders(self) -> None:
tm = self.tab_manager_ref()
if tm is not None:
w = self.active_window
ly = self.current_layout
self.borders(
all_windows=self.windows,
current_layout=ly, extra_blank_rects=tm.blank_rects,
draw_window_borders=(ly.needs_window_borders and self.windows.num_visble_groups > 1) or ly.must_draw_borders
)
if w is not None:
w.change_titlebar_color()
def create_layout_object(self, name: str) -> Layout:
return create_layout_object_for(name, self.os_window_id, self.id)
def next_layout(self) -> None:
if len(self.enabled_layouts) > 1:
for i, layout_name in enumerate(self.enabled_layouts):
if layout_name == self.current_layout.full_name:
idx = i
break
else:
idx = -1
nl = self.enabled_layouts[(idx + 1) % len(self.enabled_layouts)]
self._set_current_layout(nl)
self.relayout()
def last_used_layout(self) -> None:
if len(self.enabled_layouts) > 1 and self._last_used_layout and self._last_used_layout != self._current_layout_name:
self._set_current_layout(self._last_used_layout)
self.relayout()
def goto_layout(self, layout_name: str, raise_exception: bool = False) -> None:
layout_name = layout_name.lower()
if layout_name not in self.enabled_layouts:
if raise_exception:
raise ValueError(layout_name)
log_error('Unknown or disabled layout: {}'.format(layout_name))
return
self._set_current_layout(layout_name)
self.relayout()
def resize_window_by(self, window_id: int, increment: float, is_horizontal: bool) -> Optional[str]:
increment_as_percent = self.current_layout.bias_increment_for_cell(is_horizontal) * increment
if self.current_layout.modify_size_of_window(self.windows, window_id, increment_as_percent, is_horizontal):
self.relayout()
return None
return 'Could not resize'
def resize_window(self, quality: str, increment: int) -> None:
if increment < 1:
raise ValueError(increment)
is_horizontal = quality in ('wider', 'narrower')
increment *= 1 if quality in ('wider', 'taller') else -1
w = self.active_window
if w is not None and self.resize_window_by(
w.id, increment, is_horizontal) is not None:
ring_bell()
def reset_window_sizes(self) -> None:
if self.current_layout.remove_all_biases():
self.relayout()
def layout_action(self, action_name: str, args: Sequence[str]) -> None:
ret = self.current_layout.layout_action(action_name, args, self.windows)
if ret is None:
ring_bell()
return
self.relayout()
def launch_child(
self,
use_shell: bool = False,
cmd: Optional[List[str]] = None,
stdin: Optional[bytes] = None,
cwd_from: Optional[int] = None,
cwd: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
allow_remote_control: bool = False
) -> Child:
check_for_suitability = True
if cmd is None:
if use_shell:
cmd = resolved_shell(self.opts)
check_for_suitability = False
else:
if self.args.args:
cmd = list(self.args.args)
else:
cmd = resolved_shell(self.opts)
check_for_suitability = False
cmd = self.args.args or resolved_shell(self.opts)
if check_for_suitability:
old_exe = cmd[0]
try:
is_executable = os.access(old_exe, os.X_OK)
except OSError:
pass
else:
try:
st = os.stat(old_exe)
except OSError:
pass
else:
if stat.S_ISDIR(st.st_mode):
cwd = old_exe
cmd = resolved_shell(self.opts)
elif not is_executable:
import shlex
with suppress(OSError):
with open(old_exe) as f:
cmd = [kitty_exe(), '+hold']
if f.read(2) == '#!':
line = f.read(4096).splitlines()[0]
cmd += shlex.split(line) + [old_exe]
else:
cmd += [resolved_shell(self.opts)[0], cmd[0]]
fenv: Dict[str, str] = {}
if env:
fenv.update(env)
fenv['KITTY_WINDOW_ID'] = str(next_window_id())
pwid = platform_window_id(self.os_window_id)
if pwid is not None:
fenv['WINDOWID'] = str(pwid)
ans = Child(cmd, cwd or self.cwd, self.opts, stdin, fenv, cwd_from, allow_remote_control=allow_remote_control)
ans.fork()
return ans
def _add_window(self, window: Window, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None:
self.current_layout.add_window(self.windows, window, location, overlay_for)
self.mark_tab_bar_dirty()
self.relayout()
def new_window(
self,
use_shell: bool = True,
cmd: Optional[List[str]] = None,
stdin: Optional[bytes] = None,
override_title: Optional[str] = None,
cwd_from: Optional[int] = None,
cwd: Optional[str] = None,
overlay_for: Optional[int] = None,
env: Optional[Dict[str, str]] = None,
location: Optional[str] = None,
copy_colors_from: Optional[Window] = None,
allow_remote_control: bool = False,
marker: Optional[str] = None,
watchers: Optional[Watchers] = None
) -> Window:
child = self.launch_child(
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, allow_remote_control=allow_remote_control)
window = Window(
self, child, self.opts, self.args, override_title=override_title,
copy_colors_from=copy_colors_from, watchers=watchers
)
# Must add child before laying out so that resize_pty succeeds
get_boss().add_child(window)
self._add_window(window, location=location, overlay_for=overlay_for)
if marker:
try:
window.set_marker(marker)
except Exception:
import traceback
traceback.print_exc()
return window
def new_special_window(
self,
special_window: SpecialWindowInstance,
location: Optional[str] = None,
copy_colors_from: Optional[Window] = None,
allow_remote_control: bool = False,
) -> Window:
return self.new_window(
use_shell=False, cmd=special_window.cmd, stdin=special_window.stdin,
override_title=special_window.override_title,
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
allow_remote_control=allow_remote_control, watchers=special_window.watchers
)
def close_window(self) -> None:
w = self.active_window
if w is not None:
self.remove_window(w)
def close_other_windows_in_tab(self) -> None:
if len(self.windows) > 1:
active_window = self.active_window
for window in tuple(self.windows):
if window is not active_window:
self.remove_window(window)
def remove_window(self, window: Window, destroy: bool = True) -> None:
self.windows.remove_window(window)
if destroy:
remove_window(self.os_window_id, self.id, window.id)
else:
detach_window(self.os_window_id, self.id, window.id)
self.mark_tab_bar_dirty()
self.relayout()
active_window = self.active_window
if active_window:
self.title_changed(active_window)
def detach_window(self, window: Window) -> Tuple[Window, ...]:
windows = list(self.windows.windows_in_group_of(window))
windows.sort(key=attrgetter('id')) # since ids increase in order of creation
for w in reversed(windows):
self.remove_window(w, destroy=False)
return tuple(windows)
def attach_window(self, window: Window) -> None:
window.change_tab(self)
attach_window(self.os_window_id, self.id, window.id)
self._add_window(window)
def set_active_window(self, x: Union[Window, int]) -> None:
self.windows.set_active_window_group_for(x)
def get_nth_window(self, n: int) -> Optional[Window]:
if self.windows:
return self.current_layout.nth_window(self.windows, n)
def nth_window(self, num: int = 0) -> None:
if self.windows:
if num < 0:
self.windows.make_previous_group_active(-num)
else:
self.current_layout.activate_nth_window(self.windows, num)
self.relayout_borders()
def _next_window(self, delta: int = 1) -> None:
if len(self.windows) > 1:
self.current_layout.next_window(self.windows, delta)
self.relayout_borders()
def next_window(self) -> None:
self._next_window()
def previous_window(self) -> None:
self._next_window(-1)
prev_window = previous_window
def most_recent_group(self, groups: Sequence[int]) -> Optional[int]:
groups_set = frozenset(groups)
for window_id in reversed(self.windows.active_window_history):
group = self.windows.group_for_window(window_id)
if group and group.id in groups_set:
return group.id
if groups:
return groups[0]
def neighboring_group_id(self, which: EdgeLiteral) -> Optional[int]:
neighbors = self.current_layout.neighbors(self.windows)
candidates = neighbors.get(which)
if candidates:
return self.most_recent_group(candidates)
def neighboring_window(self, which: EdgeLiteral) -> None:
neighbor = self.neighboring_group_id(which)
if neighbor:
self.windows.set_active_group(neighbor)
def move_window(self, delta: Union[EdgeLiteral, int] = 1) -> None:
if isinstance(delta, int):
if self.current_layout.move_window(self.windows, delta):
self.relayout()
elif isinstance(delta, str):
neighbor = self.neighboring_group_id(delta)
if neighbor:
if self.current_layout.move_window_to_group(self.windows, neighbor):
self.relayout()
def move_window_to_top(self) -> None:
n = self.windows.active_group_idx
if n > 0:
self.move_window(-n)
def move_window_forward(self) -> None:
self.move_window()
def move_window_backward(self) -> None:
self.move_window(-1)
def list_windows(self, active_window: Optional[Window]) -> Generator[WindowDict, None, None]:
for w in self:
yield w.as_dict(is_focused=w is active_window)
def matches(self, field: str, pat: Pattern) -> bool:
if field == 'id':
return bool(pat.pattern == str(self.id))
if field == 'title':
return pat.search(self.name or self.title) is not None
return False
def __iter__(self) -> Iterator[Window]:
return iter(self.windows)
def __len__(self) -> int:
return len(self.windows)
def __contains__(self, window: Window) -> bool:
return window in self.windows
def destroy(self) -> None:
evict_cached_layouts(self.id)
for w in self.windows:
w.destroy()
self.windows = WindowList(self)
def __repr__(self) -> str:
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))
def make_active(self) -> None:
tm = self.tab_manager_ref()
if tm is not None:
tm.set_active_tab(self)
# }}}
class TabManager: # {{{
def __init__(self, os_window_id: int, opts: Options, args: CLIOptions, wm_class: str, wm_name: str, startup_session: Optional[SessionType] = None):
self.os_window_id = os_window_id
self.wm_class = wm_class
self.wm_name = wm_name
self.last_active_tab_id = None
self.opts, self.args = opts, args
self.tab_bar_hidden = self.opts.tab_bar_style == 'hidden'
self.tabs: List[Tab] = []
self.active_tab_history: Deque[int] = deque()
self.tab_bar = TabBar(self.os_window_id, opts)
self._active_tab_idx = 0
if startup_session is not None:
for t in startup_session.tabs:
self._add_tab(Tab(self, session_tab=t))
self._set_active_tab(max(0, min(startup_session.active_tab_idx, len(self.tabs) - 1)))
@property
def active_tab_idx(self) -> int:
return self._active_tab_idx
@active_tab_idx.setter
def active_tab_idx(self, val: int) -> None:
try:
old_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
except Exception:
old_active_tab = None
else:
assert old_active_tab is not None
add_active_id_to_history(self.active_tab_history, old_active_tab.id)
self._active_tab_idx = max(0, min(val, len(self.tabs) - 1))
try:
new_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
except Exception:
new_active_tab = None
if old_active_tab is not new_active_tab:
if old_active_tab is not None:
w = old_active_tab.active_window
if w is not None:
w.focus_changed(False)
if new_active_tab is not None:
w = new_active_tab.active_window
if w is not None:
w.focus_changed(True)
def refresh_sprite_positions(self) -> None:
if not self.tab_bar_hidden:
self.tab_bar.screen.refresh_sprite_positions()
@property
def tab_bar_should_be_visible(self) -> bool:
return len(self.tabs) >= self.opts.tab_bar_min_tabs
def _add_tab(self, tab: Tab) -> None:
visible_before = self.tab_bar_should_be_visible
self.tabs.append(tab)
if not visible_before and self.tab_bar_should_be_visible:
self.tabbar_visibility_changed()
def _remove_tab(self, tab: Tab) -> None:
visible_before = self.tab_bar_should_be_visible
remove_tab(self.os_window_id, tab.id)
self.tabs.remove(tab)
if visible_before and not self.tab_bar_should_be_visible:
self.tabbar_visibility_changed()
def _set_active_tab(self, idx: int) -> None:
self.active_tab_idx = idx
set_active_tab(self.os_window_id, idx)
def tabbar_visibility_changed(self) -> None:
if not self.tab_bar_hidden:
self.tab_bar.layout()
self.resize(only_tabs=True)
def mark_tab_bar_dirty(self) -> None:
if self.tab_bar_should_be_visible and not self.tab_bar_hidden:
mark_tab_bar_dirty(self.os_window_id)
def update_tab_bar_data(self) -> None:
self.tab_bar.update(self.tab_bar_data)
def title_changed(self, tab: Tab) -> None:
self.mark_tab_bar_dirty()
if tab is self.active_tab:
sync_os_window_title(self.os_window_id)
def resize(self, only_tabs: bool = False) -> None:
if not only_tabs:
if not self.tab_bar_hidden:
self.tab_bar.layout()
self.mark_tab_bar_dirty()
for tab in self.tabs:
tab.relayout()
def set_active_tab_idx(self, idx: int) -> None:
self._set_active_tab(idx)
tab = self.active_tab
if tab is not None:
tab.relayout_borders()
self.mark_tab_bar_dirty()
def set_active_tab(self, tab: Tab) -> bool:
try:
idx = self.tabs.index(tab)
except Exception:
return False
self.set_active_tab_idx(idx)
return True
def next_tab(self, delta: int = 1) -> None:
if len(self.tabs) > 1:
self.set_active_tab_idx((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs))
def goto_tab(self, tab_num: int) -> None:
if tab_num >= len(self.tabs):
tab_num = max(0, len(self.tabs) - 1)
if tab_num >= 0:
self.set_active_tab_idx(tab_num)
else:
try:
old_active_tab_id = self.active_tab_history[tab_num]
except IndexError:
return
for idx, tab in enumerate(self.tabs):
if tab.id == old_active_tab_id:
self.set_active_tab_idx(idx)
break
def __iter__(self) -> Iterator[Tab]:
return iter(self.tabs)
def __len__(self) -> int:
return len(self.tabs)
def list_tabs(self, active_tab: Optional[Tab], active_window: Optional[Window]) -> Generator[TabDict, None, None]:
for tab in self:
yield {
'id': tab.id,
'is_focused': tab is active_tab,
'title': tab.name or tab.title,
'layout': str(tab.current_layout.name),
'windows': list(tab.list_windows(active_window)),
'active_window_history': list(tab.windows.active_window_history),
}
def serialize_state(self) -> Dict[str, Any]:
return {
'version': 1,
'id': self.os_window_id,
'tabs': [tab.serialize_state() for tab in self],
'active_tab_idx': self.active_tab_idx,
}
@property
def active_tab(self) -> Optional[Tab]:
return self.tabs[self.active_tab_idx] if self.tabs else None
@property
def active_window(self) -> Optional[Window]:
t = self.active_tab
if t is not None:
return t.active_window
@property
def number_of_windows(self) -> int:
count = 0
for tab in self:
for window in tab:
count += 1
return count
def tab_for_id(self, tab_id: int) -> Optional[Tab]:
for t in self.tabs:
if t.id == tab_id:
return t
def move_tab(self, delta: int = 1) -> None:
if len(self.tabs) > 1:
idx = self.active_tab_idx
nidx = (idx + len(self.tabs) + delta) % len(self.tabs)
step = 1 if idx < nidx else -1
for i in range(idx, nidx, step):
self.tabs[i], self.tabs[i + step] = self.tabs[i + step], self.tabs[i]
swap_tabs(self.os_window_id, i, i + step)
self._set_active_tab(nidx)
self.mark_tab_bar_dirty()
def new_tab(
self,
special_window: Optional[SpecialWindowInstance] = None,
cwd_from: Optional[int] = None,
as_neighbor: bool = False,
empty_tab: bool = False,
location: str = 'last'
) -> Tab:
idx = len(self.tabs)
orig_active_tab_idx = self.active_tab_idx
self._add_tab(Tab(self, no_initial_window=True) if empty_tab else Tab(self, special_window=special_window, cwd_from=cwd_from))
self._set_active_tab(idx)
if as_neighbor:
location = 'after'
if location == 'neighbor':
location = 'after'
if len(self.tabs) > 1 and location != 'last':
if location == 'first':
desired_idx = 0
else:
desired_idx = orig_active_tab_idx + (0 if location == 'before' else 1)
if idx != desired_idx:
for i in range(idx, desired_idx, -1):
self.tabs[i], self.tabs[i-1] = self.tabs[i-1], self.tabs[i]
swap_tabs(self.os_window_id, i, i-1)
self._set_active_tab(desired_idx)
idx = desired_idx
self.mark_tab_bar_dirty()
return self.tabs[idx]
def remove(self, tab: Tab) -> None:
self._remove_tab(tab)
next_active_tab = -1
while True:
try:
self.active_tab_history.remove(tab.id)
except ValueError:
break
if self.opts.tab_switch_strategy == 'previous':
while self.active_tab_history and next_active_tab < 0:
tab_id = self.active_tab_history.pop()
for idx, qtab in enumerate(self.tabs):
if qtab.id == tab_id:
next_active_tab = idx
break
elif self.opts.tab_switch_strategy == 'left':
next_active_tab = max(0, self.active_tab_idx - 1)
elif self.opts.tab_switch_strategy == 'right':
next_active_tab = min(self.active_tab_idx, len(self.tabs) - 1)
if next_active_tab < 0:
next_active_tab = max(0, min(self.active_tab_idx, len(self.tabs) - 1))
self._set_active_tab(next_active_tab)
self.mark_tab_bar_dirty()
tab.destroy()
@property
def tab_bar_data(self) -> List[TabBarData]:
at = self.active_tab
ans = []
for t in self.tabs:
title = (t.name or t.title or appname).strip()
needs_attention = False
has_activity_since_last_focus = False
for w in t:
if w.needs_attention:
needs_attention = True
if w.has_activity_since_last_focus:
has_activity_since_last_focus = True
ans.append(TabBarData(
title, t is at, needs_attention,
len(t), t.current_layout.name or '',
has_activity_since_last_focus
))
return ans
def activate_tab_at(self, x: int, is_double: bool = False) -> None:
i = self.tab_bar.tab_at(x)
if i is None:
if is_double:
self.new_tab()
else:
self.set_active_tab_idx(i)
@property
def blank_rects(self) -> Tuple[Rect, ...]:
return self.tab_bar.blank_rects if self.tab_bar_should_be_visible else ()
def destroy(self) -> None:
for t in self:
t.destroy()
self.tab_bar.destroy()
del self.tab_bar
del self.tabs
# }}}