launch command: A new --bias option to adjust the size of newly created windows declaratively

Still need to implement it for the Grid layout.
Fixes #7634
This commit is contained in:
Kovid Goyal 2024-07-20 12:36:47 +05:30
parent ae8da889c4
commit 681048f1ca
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 129 additions and 21 deletions

View file

@ -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`)

View file

@ -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

View file

@ -185,6 +185,37 @@ def options_spec() -> str:
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:

View file

@ -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 @@ def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> Non
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 @@ def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
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 @@ def add_window(
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 @@ def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, lo
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

View file

@ -141,14 +141,19 @@ def split_and_add(self, existing_window_id: int, new_window_id: int, horizontal:
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 @@ def add_non_overlay_window(
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 @@ def add_non_overlay_window(
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 @@ def add_non_overlay_window(
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,

View file

@ -10,7 +10,17 @@
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 @@ def build_bias_list(self) -> Tuple[float, ...]:
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 @@ def variable_layout(self, all_windows: WindowList, biased_map: Dict[int, float])
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 @@ def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_hor
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

View file

@ -93,6 +93,12 @@ def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_hor
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):

View file

@ -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'

View file

@ -410,7 +410,7 @@ def toggle_layout(self, layout_name: str) -> None:
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 @@ def launch_child(
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 @@ def new_window(
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 @@ def new_window(
)
# 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)