diff --git a/docs/changelog.rst b/docs/changelog.rst index 8a1b35f92..422184291 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -76,6 +76,8 @@ Detailed list of changes - Add NERD fonts builtin so that users don't have to install them to use NERD symbols in kitty. The builtin font is used only if the symbols are not available in some system font +- launch command: A new :option:`launch --bias` option to adjust the size of newly created windows declaratively (:iss:`7634`) + - Sessions: A new command ``focus_matching_window`` to shift focus to a specific window, useful when creating complex layouts with splits (:disc:`7635`) - Wayland: Allow fractional scales less than one (:pull:`7549`) diff --git a/docs/overview.rst b/docs/overview.rst index f8a21de68..5e8a2bb79 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -186,9 +186,8 @@ option in :file:`kitty.conf`. An example, showing all available commands: # Create the second column by splitting the first window vertically launch --location=vsplit # Create the third window in the second column by splitting the second window horizontally - launch --location=hsplit - # Make the third window shorter so that the split is not even - resize_window shorter 5 + # Make it take 40% of the height instead of 50% + launch --location=hsplit --bias=40 # Go back to focusing the first window, so that we can split it focus_matching_window var:window=first # Create the final window in the first column diff --git a/kitty/launch.py b/kitty/launch.py index 00eb5ccd4..ce6097200 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -185,6 +185,37 @@ active window. The default is to place the window in a layout dependent manner, typically, after the currently active window. +--bias +type=float +default=0 +The bias used to alter the size of the window. +It controls what fraction of available space the window takes. The exact meaning +of bias depends on the current layout. + +* Splits layout: The bias is interpreted as a percentage between 0 and 100. +When splitting a window into two, the new window will take up the specified fraction +of the space alloted to the original window and the original window will take up +the remainder of the space. + +* Vertical/horizontal layout: The bias is interpreted as adding/subtracting from the +normal size of the window. It should be a number between -90 and 90. This number is +the percentage of cells in the full OS window that should be added to the window size. +So for example, if a window would normally have been 50 cells in the layout inside an +OS Window that is 80 cells high and --bias -10 is used it will become *approximately* +42 cells high. Note that cell counts are approximations, you cannot use this method to +create windows of fixed cell sizes. + +* Tall layout: If the window being created is the *first* window in a column, then +the bias is interpreted as a percentage, as for the splits layout, splitting the OS +Window width between columns. If the window is a second or subsequent window in a column +the bias is interpreted as adding/subtracting from the window size as for the vertical +layout above. + +* Fat layout: Same as tall layout except it goes by rows instead of columns. + +The bias option was introduced in kitty version 0.36.0. + + --allow-remote-control type=bool-set Programs running in this window can control kitty (even if remote control is not @@ -433,6 +464,7 @@ class LaunchKwds(TypedDict): overlay_for: Optional[int] stdin: Optional[bytes] hold: bool + bias: Optional[float] def apply_colors(window: Window, spec: Sequence[str]) -> None: @@ -520,11 +552,14 @@ def _launch( 'overlay_for': None, 'stdin': None, 'hold': False, + 'bias': None, } spacing = {} if opts.spacing: from .rc.set_spacing import parse_spacing_settings, patch_window_edges spacing = parse_spacing_settings(opts.spacing) + if opts.bias: + kw['bias'] = max(-100, min(opts.bias, 100)) if opts.cwd: if opts.cwd == 'current': if active: diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 7b669628e..7137cad36 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -190,7 +190,7 @@ def layout_single_window( return window_geometry_from_layouts(x, y) -def safe_increment_bias(old_val: float, increment: float) -> float: +def safe_increment_bias(old_val: float, increment: float = 0) -> float: return max(0.1, min(old_val + increment, 0.9)) @@ -234,11 +234,11 @@ class Layout: self.full_name = f'{self.name}:{layout_opts}' if layout_opts else self.name self.remove_all_biases() - def bias_increment_for_cell(self, all_windows: WindowList, window_id: int, is_horizontal: bool) -> float: + def bias_increment_for_cell(self, all_windows: WindowList, is_horizontal: bool) -> float: self._set_dimensions() - return self.calculate_bias_increment_for_a_single_cell(all_windows, window_id, is_horizontal) + return self.calculate_bias_increment_for_a_single_cell(all_windows, is_horizontal) - def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, window_id: int, is_horizontal: bool) -> float: + def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, is_horizontal: bool) -> float: if is_horizontal: return (lgd.cell_width + 1) / lgd.central.width return (lgd.cell_height + 1) / lgd.central.height @@ -289,7 +289,7 @@ class Layout: def add_window( self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, - overlay_for: Optional[int] = None, put_overlay_behind: bool = False + overlay_for: Optional[int] = None, put_overlay_behind: bool = False, bias: Optional[float] = None, ) -> None: if overlay_for is not None: underlay = all_windows.id_map.get(overlay_for) @@ -299,9 +299,9 @@ class Layout: return if location == 'neighbor': location = 'after' - self.add_non_overlay_window(all_windows, window, location) + self.add_non_overlay_window(all_windows, window, location, bias) - def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: Optional[str]) -> None: + def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: Optional[str], bias: Optional[float] = None) -> None: next_to: Optional[WindowType] = None before = False next_to = all_windows.active_window @@ -316,6 +316,21 @@ class Layout: elif location == 'last': next_to = None all_windows.add_window(window, next_to=next_to, before=before) + if bias is not None: + idx = all_windows.group_idx_for_window(window) + if idx is not None: + self._set_dimensions() + self._bias_slot(all_windows, idx, bias) + + def _bias_slot(self, all_windows: WindowList, idx: int, bias: float) -> bool: + fractional_bias = max(10, min(abs(bias), 90)) / 100 + h, v = self.calculate_bias_increment_for_a_single_cell(all_windows, True), self.calculate_bias_increment_for_a_single_cell(all_windows, False) + nh, nv = lgd.central.width / lgd.cell_width, lgd.central.height / lgd.cell_height + f = max(-90, min(bias, 90)) / 100. + return self.bias_slot(all_windows, idx, fractional_bias, h * nh *f, v * nv * f) + + def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool: + return False def update_visibility(self, all_windows: WindowList) -> None: active_window = all_windows.active_window diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index 67a687d60..2b1070e1c 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -141,14 +141,19 @@ class Pair: pair = self pair.horizontal = horizontal self.one, self.two = q + final_pair = pair + else: pair = Pair(horizontal=horizontal) if self.one == existing_window_id: self.one = pair else: self.two = pair - tuple(map(pair.balanced_add, q)) - return pair + for wid in q: + qp = pair.balanced_add(wid) + if wid == new_window_id: + final_pair = qp + return final_pair def apply_window_geometry( self, window_id: int, @@ -483,7 +488,8 @@ class Splits(Layout): self, all_windows: WindowList, window: WindowType, - location: Optional[str] + location: Optional[str], + bias: Optional[float] = None, ) -> None: horizontal = self.default_axis_is_horizontal after = True @@ -494,6 +500,8 @@ class Splits(Layout): elif location in ('before', 'first'): after = False aw = all_windows.active_window + if bias: + bias = max(0, min(abs(bias), 100)) / 100 if aw is not None: ag = all_windows.active_group assert ag is not None @@ -505,9 +513,14 @@ class Splits(Layout): wheight = aw.geometry.bottom - aw.geometry.top horizontal = wwidth >= wheight target_group = all_windows.add_window(window, next_to=aw, before=not after) - pair.split_and_add(group_id, target_group.id, horizontal, after) + parent_pair = pair.split_and_add(group_id, target_group.id, horizontal, after) + if bias is not None: + parent_pair.bias = bias if parent_pair.one == target_group.id else (1 - bias) return all_windows.add_window(window) + p = self.pairs_root.balanced_add(window.id) + if bias is not None: + p.bias = bias def modify_size_of_window( self, diff --git a/kitty/layout/tall.py b/kitty/layout/tall.py index 3e0565183..e0133815d 100644 --- a/kitty/layout/tall.py +++ b/kitty/layout/tall.py @@ -10,7 +10,17 @@ from kitty.types import Edges from kitty.typing import EdgeLiteral, WindowType from kitty.window_list import WindowGroup, WindowList -from .base import BorderLine, Layout, LayoutData, LayoutDimension, LayoutOpts, NeighborsMap, lgd, normalize_biases, safe_increment_bias +from .base import ( + BorderLine, + Layout, + LayoutData, + LayoutDimension, + LayoutOpts, + NeighborsMap, + lgd, + normalize_biases, + safe_increment_bias, +) from .vertical import borders @@ -76,6 +86,18 @@ class TallLayoutOpts(LayoutOpts): return tuple(repeat(b / self.full_size, self.full_size)) + (1.0 - b,) +def set_bias(biases: Sequence[float], idx: int, target: float) -> List[float]: + remainder = 1 - target + previous_remainder = sum(x for i, x in enumerate(biases) if i != idx) + ans = [1. for i in range(len(biases))] + for i in range(len(biases)): + if i == idx: + ans[i] = target + else: + ans[i] = remainder * biases[i] / previous_remainder + return ans + + class Tall(Layout): name = 'tall' @@ -99,6 +121,17 @@ class Tall(Layout): bias = biased_map if num > 1 else None return self.perp_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias, offset=self.num_full_size_windows) + def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool: + if idx < len(self.main_bias): + before_main_bias = self.main_bias + self.main_bias = set_bias(self.main_bias, idx, fractional_bias) + return self.main_bias != before_main_bias + + before_layout = tuple(self.variable_layout(all_windows, self.biased_map)) + self.biased_map[idx - self.num_full_size_windows] = cell_increment_bias_v if self.main_is_horizontal else cell_increment_bias_h + after_layout = tuple(self.variable_layout(all_windows, self.biased_map)) + return before_layout == after_layout + def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: num_windows = all_windows.num_groups if self.main_is_horizontal == is_horizontal: @@ -115,11 +148,11 @@ class Tall(Layout): if idx < self.num_full_size_windows or num_of_short_windows < 2: return False idx -= self.num_full_size_windows - before_layout = list(self.variable_layout(all_windows, self.biased_map)) + before_layout = tuple(self.variable_layout(all_windows, self.biased_map)) before = self.biased_map.get(idx, 0.) candidate = self.biased_map.copy() candidate[idx] = after = before + increment - if before_layout == list(self.variable_layout(all_windows, candidate)): + if before_layout == tuple(self.variable_layout(all_windows, candidate)): return False self.biased_map = candidate return before != after diff --git a/kitty/layout/vertical.py b/kitty/layout/vertical.py index 417cfeb4d..0549d83d7 100644 --- a/kitty/layout/vertical.py +++ b/kitty/layout/vertical.py @@ -93,6 +93,12 @@ class Vertical(Layout): self.biased_map = candidate return True + def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool: + before_layout = tuple(self.variable_layout(all_windows, self.biased_map)) + self.biased_map[idx] = cell_increment_bias_h if self.main_is_horizontal else cell_increment_bias_v + after_layout = tuple(self.variable_layout(all_windows, self.biased_map)) + return before_layout == after_layout + def generate_layout_data(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData], None, None]: ylayout = self.variable_layout(all_windows, self.biased_map) for wg, yl in zip(all_windows.iter_all_layoutable_groups(), ylayout): diff --git a/kitty/rc/launch.py b/kitty/rc/launch.py index 66b297bed..134713c35 100644 --- a/kitty/rc/launch.py +++ b/kitty/rc/launch.py @@ -50,6 +50,7 @@ class Launch(RemoteCommand): os_window_state/choices.normal.fullscreen.maximized.minimized: The initial state for OS Window color/list.str: list of color specifications such as foreground=red watcher/list.str: list of paths to watcher files + bias/float: The bias with which to create the new window in the current layout ''' short_desc = 'Run an arbitrary process in a new window/tab' diff --git a/kitty/tabs.py b/kitty/tabs.py index 498b33378..a2f449e1a 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -410,7 +410,7 @@ class Tab: # {{{ self.goto_layout(layout_name) 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(self.windows, window_id, is_horizontal) * increment + increment_as_percent = self.current_layout.bias_increment_for_cell(self.windows, is_horizontal) * increment if self.current_layout.modify_size_of_window(self.windows, window_id, increment_as_percent, is_horizontal): self.relayout() return None @@ -511,8 +511,11 @@ class Tab: # {{{ ans.fork() return ans - def _add_window(self, window: Window, location: Optional[str] = None, overlay_for: Optional[int] = None, overlay_behind: bool = False) -> None: - self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind) + def _add_window( + self, window: Window, location: Optional[str] = None, overlay_for: Optional[int] = None, + overlay_behind: bool = False, bias: Optional[float] = None + ) -> None: + self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind, bias=bias) self.mark_tab_bar_dirty() self.relayout() @@ -535,6 +538,7 @@ class Tab: # {{{ is_clone_launch: str = '', remote_control_passwords: Optional[Dict[str, Sequence[str]]] = None, hold: bool = False, + bias: Optional[float] = None, ) -> Window: child = self.launch_child( use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, @@ -548,7 +552,7 @@ class Tab: # {{{ ) # 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, overlay_behind=overlay_behind) + self._add_window(window, location=location, overlay_for=overlay_for, overlay_behind=overlay_behind, bias=bias) if marker: try: window.set_marker(marker)