#!/usr/bin/env python # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal import weakref from collections import deque, namedtuple from functools import partial from .borders import Borders from .child import Child from .config import build_ansi_color_table from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland from .fast_data_types import ( DECAWM, Screen, add_tab, glfw_post_empty_event, remove_tab, remove_window, set_active_tab, set_tab_bar_render_data, swap_tabs, viewport_for_window, x11_window_id ) from .layout import Rect, all_layouts from .session import resolved_shell from .utils import color_as_int from .window import Window, calculate_gl_geometry TabbarData = namedtuple('TabbarData', 'title is_active is_last') SpecialWindowInstance = namedtuple('SpecialWindow', 'cmd stdin override_title cwd_from cwd overlay_for') def SpecialWindow(cmd, stdin=None, override_title=None, cwd_from=None, cwd=None, overlay_for=None): return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for) class Tab: # {{{ def __init__(self, tab_manager, session_tab=None, special_window=None, cwd_from=None): self.tab_manager_ref = weakref.ref(tab_manager) self.os_window_id = tab_manager.os_window_id self.id = 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 = list(getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts) self.borders = Borders(self.os_window_id, self.id, self.opts) self.windows = deque() self.active_window_idx = 0 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)) if session_tab is None: self.cwd = self.args.directory sl = self.enabled_layouts[0] self.current_layout = self.create_layout_object(sl) if special_window is None: self.new_window(cwd_from=cwd_from) else: self.new_special_window(special_window) else: self.cwd = session_tab.cwd or self.args.directory l0 = session_tab.layout self.current_layout = self.create_layout_object(l0) self.startup(session_tab) def startup(self, session_tab): for cmd in session_tab.windows: if isinstance(cmd, (SpecialWindowInstance,)): self.new_special_window(cmd) else: self.new_window(cmd=cmd) self.set_active_window_idx(session_tab.active_window_idx) @property def active_window(self): return self.windows[self.active_window_idx] if self.windows else None @property def title(self): return getattr(self.active_window, 'title', appname) def set_title(self, title): self.name = title or '' tm = self.tab_manager_ref() if tm is not None: tm.title_changed(self.name) def title_changed(self, window): if window is self.active_window: tm = self.tab_manager_ref() if tm is not None: tm.title_changed(window.title) def visible_windows(self): for w in self.windows: if w.is_visible_in_layout: yield w def relayout(self): if self.windows: self.active_window_idx = self.current_layout(self.windows, self.active_window_idx) self.relayout_borders() def relayout_borders(self): tm = self.tab_manager_ref() if tm is not None: visible_windows = [w for w in self.windows if w.is_visible_in_layout] self.borders(visible_windows, self.active_window, self.current_layout, tm.blank_rects, self.current_layout.needs_window_borders and len(visible_windows) > 1) def create_layout_object(self, idx): return all_layouts[idx](self.os_window_id, self.id, self.opts, self.borders.border_width) def next_layout(self): if len(self.enabled_layouts) > 1: try: idx = self.enabled_layouts.index(self.current_layout.name) except Exception: idx = -1 nl = self.enabled_layouts[(idx + 1) % len(self.enabled_layouts)] self.current_layout = self.create_layout_object(nl) self.relayout() def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None): if cmd is None: if use_shell: cmd = resolved_shell(self.opts) else: cmd = self.args.args or resolved_shell(self.opts) env = {} if not is_macos and not is_wayland: try: env['WINDOWID'] = str(x11_window_id(self.os_window_id)) except Exception: import traceback traceback.print_exc() ans = Child(cmd, cwd or self.cwd, self.opts, stdin, env, cwd_from) ans.fork() return ans def new_window(self, use_shell=True, cmd=None, stdin=None, override_title=None, cwd_from=None, cwd=None, overlay_for=None): child = self.launch_child(use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd) window = Window(self, child, self.opts, self.args, override_title=override_title) if overlay_for is not None: overlaid = next(w for w in self.windows if w.id == overlay_for) window.overlay_for = overlay_for overlaid.overlay_window_id = window.id # Must add child before laying out so that resize_pty succeeds get_boss().add_child(window) self.active_window_idx = self.current_layout.add_window(self.windows, window, self.active_window_idx) self.relayout_borders() glfw_post_empty_event() return window def new_special_window(self, special_window): return self.new_window(False, *special_window) def close_window(self): if self.windows: self.remove_window(self.windows[self.active_window_idx]) def remove_window(self, window): self.active_window_idx = self.current_layout.remove_window(self.windows, window, self.active_window_idx) remove_window(self.os_window_id, self.id, window.id) self.relayout_borders() glfw_post_empty_event() def set_active_window_idx(self, idx): if idx != self.active_window_idx: self.active_window_idx = self.current_layout.set_active_window(self.windows, idx) self.relayout_borders() glfw_post_empty_event() def set_active_window(self, window): try: idx = self.windows.index(window) except ValueError: return self.set_active_window_idx(idx) def nth_window(self, num=0): if self.windows: self.active_window_idx = self.current_layout.nth_window(self.windows, num) self.relayout_borders() glfw_post_empty_event() def _next_window(self, delta=1): if len(self.windows) > 1: self.active_window_idx = self.current_layout.next_window(self.windows, self.active_window_idx, delta) self.relayout_borders() glfw_post_empty_event() def next_window(self): self._next_window() def previous_window(self): self._next_window(-1) def move_window(self, delta=1): self.active_window_idx = self.current_layout.move_window(self.windows, self.active_window_idx, delta) self.relayout() glfw_post_empty_event() def move_window_to_top(self): self.move_window(-self.active_window_idx) def move_window_forward(self): self.move_window() def move_window_backward(self): self.move_window(-1) def list_windows(self): for w in self: yield w.as_dict() def matches(self, field, pat): if field == 'id': return pat.pattern == str(self.id) if field == 'title': return pat.search(self.name or self.title) is not None return False def __iter__(self): yield from iter(self.windows) def __len__(self): return len(self.windows) def __contains__(self, window): return window in self.windows def destroy(self): for w in self.windows: w.destroy() self.windows = deque() def __repr__(self): return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self))) # }}} class TabBar: # {{{ def __init__(self, os_window_id, opts): self.os_window_id = os_window_id self.num_tabs = 1 self.cell_width = 1 self.data_buffer_size = 0 self.laid_out_once = False self.dirty = True self.screen = s = Screen(None, 1, 10) s.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) s.color_profile.set_configured_colors( color_as_int(opts.inactive_tab_foreground), color_as_int(opts.inactive_tab_background) ) self.blank_rects = () sep = opts.tab_separator self.trailing_spaces = self.leading_spaces = 0 while sep and sep[0] == ' ': sep = sep[1:] self.trailing_spaces += 1 while sep and sep[-1] == ' ': self.leading_spaces += 1 sep = sep[:-1] self.sep = sep self.active_font_style = opts.active_tab_font_style self.inactive_font_style = opts.inactive_tab_font_style def as_rgb(x): return (x << 8) | 2 self.active_bg = as_rgb(color_as_int(opts.active_tab_background)) self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground)) def layout(self): central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id) if tab_bar.width < 2: return self.cell_width = cell_width s = self.screen viewport_width = tab_bar.width ncells = viewport_width // cell_width s.resize(1, ncells) s.reset_mode(DECAWM) self.laid_out_once = True margin = (viewport_width - ncells * cell_width) // 2 self.window_geometry = g = WindowGeometry( margin, tab_bar.top, viewport_width - margin, tab_bar.bottom, s.columns, s.lines) if margin > 0: self.blank_rects = (Rect(0, g.top, g.left, g.bottom + 1), Rect(g.right - 1, g.top, viewport_width, g.bottom + 1)) else: self.blank_rects = () self.screen_geometry = sg = calculate_gl_geometry(g, vw, vh, cell_width, cell_height) set_tab_bar_render_data(self.os_window_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen) def update(self, data): if not self.laid_out_once: return s = self.screen s.cursor.x = 0 s.erase_in_line(2, False) max_title_length = (self.screen_geometry.xnum // max(1, len(data))) - 1 cr = [] for t in data: s.cursor.bg = self.active_bg if t.is_active else 0 s.cursor.fg = self.active_fg if t.is_active else 0 s.cursor.bold, s.cursor.italic = self.active_font_style if t.is_active else self.inactive_font_style before = s.cursor.x s.draw(' ' * self.leading_spaces + t.title + ' ' * self.trailing_spaces) extra = s.cursor.x - before - max_title_length if extra > 0: s.cursor.x -= extra + 1 s.draw('…') cr.append((before, s.cursor.x)) s.cursor.bold = s.cursor.italic = False s.cursor.fg = s.cursor.bg = 0 s.draw(self.sep) if s.cursor.x > s.columns - max_title_length and not t.is_last: s.draw('…') break s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab self.cell_ranges = cr glfw_post_empty_event() def destroy(self): self.screen.reset_callbacks() del self.screen def tab_at(self, x): x = (x - self.window_geometry.left) // self.cell_width for i, (a, b) in enumerate(self.cell_ranges): if a <= x <= b: return i # }}} class TabManager: # {{{ def __init__(self, os_window_id, opts, args, startup_session): self.os_window_id = os_window_id self.opts, self.args = opts, args self.tabs = [] self.tab_bar = TabBar(self.os_window_id, opts) self.active_tab_idx = 0 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))) self.update_tab_bar() def refresh_sprite_positions(self): self.tab_bar.screen.refresh_sprite_positions() def _add_tab(self, tab): before = len(self.tabs) self.tabs.append(tab) if len(self.tabs) > 1 and before < 2: self.tabbar_visibility_changed() def _remove_tab(self, tab): before = len(self.tabs) remove_tab(self.os_window_id, tab.id) self.tabs.remove(tab) if len(self.tabs) < 2 and before > 1: self.tabbar_visibility_changed() def _set_active_tab(self, idx): self.active_tab_idx = idx set_active_tab(self.os_window_id, idx) def tabbar_visibility_changed(self): self.tab_bar.layout() self.resize(only_tabs=True) glfw_post_empty_event() def update_tab_bar(self): if len(self.tabs) > 1: self.tab_bar.update(self.tab_bar_data) def resize(self, only_tabs=False): if not only_tabs: self.tab_bar.layout() self.update_tab_bar() for tab in self.tabs: tab.relayout() def set_active_tab_idx(self, idx): self._set_active_tab(idx) self.active_tab.relayout_borders() self.update_tab_bar() def set_active_tab(self, tab): try: idx = self.tabs.index(tab) except Exception: return self.set_active_tab_idx(idx) def next_tab(self, delta=1): 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): if tab_num < len(self.tabs) and 0 <= tab_num: self.set_active_tab_idx(tab_num) def __iter__(self): return iter(self.tabs) def __len__(self): return len(self.tabs) def list_tabs(self): for tab in self: yield { 'id': tab.id, 'title': tab.name or tab.title, 'windows': list(tab.list_windows()), } @property def active_tab(self): return self.tabs[self.active_tab_idx] if self.tabs else None @property def active_window(self): t = self.active_tab if t is not None: return t.active_window def tab_for_id(self, tab_id): for t in self.tabs: if t.id == tab_id: return t def move_tab(self, delta=1): if len(self.tabs) > 1: idx = self.active_tab_idx nidx = (idx + len(self.tabs) + delta) % len(self.tabs) self.tabs[idx], self.tabs[nidx] = self.tabs[nidx], self.tabs[idx] swap_tabs(self.os_window_id, idx, nidx) self._set_active_tab(nidx) self.update_tab_bar() def title_changed(self, new_title): self.update_tab_bar() def new_tab(self, special_window=None, cwd_from=None): idx = len(self.tabs) self._add_tab(Tab(self, special_window=special_window, cwd_from=cwd_from)) self._set_active_tab(idx) self.update_tab_bar() def remove(self, tab): self._remove_tab(tab) self._set_active_tab(max(0, min(self.active_tab_idx, len(self.tabs) - 1))) self.update_tab_bar() tab.destroy() @property def tab_bar_data(self): at = self.active_tab ans = [] for t in self.tabs: title = (t.name or t.title or appname).strip() ans.append(TabbarData(title, t is at, t is self.tabs[-1])) return ans def activate_tab_at(self, x): i = self.tab_bar.tab_at(x) if i is not None: self.set_active_tab_idx(i) @property def blank_rects(self): return self.tab_bar.blank_rects if len(self.tabs) > 1 else () def destroy(self): for t in self: t.destroy() self.tab_bar.destroy() del self.tab_bar del self.tabs # }}}