From 8ea84c97d5c0132267d229625e0fd600f9bf49d7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 17 May 2018 23:26:41 +0530 Subject: [PATCH] Use an overlay window for window resizing Also implement a remote command to resize windows --- README.asciidoc | 13 ++- kittens/resize_window/__init__.py | 0 kittens/resize_window/main.py | 135 ++++++++++++++++++++++++++++++ kittens/tui/handler.py | 3 + kittens/tui/loop.py | 4 +- kittens/tui/operations.py | 5 ++ kitty/boss.py | 43 +++++----- kitty/cmds.py | 48 +++++++++++ kitty/keys.c | 15 ---- kitty/layout.py | 3 +- kitty/mouse.c | 1 - kitty/remote_control.py | 20 +++-- kitty/state.c | 17 ---- kitty/state.h | 4 - kitty/tabs.py | 9 +- kitty/window.py | 11 +-- 16 files changed, 246 insertions(+), 85 deletions(-) create mode 100644 kittens/resize_window/__init__.py create mode 100644 kittens/resize_window/main.py diff --git a/README.asciidoc b/README.asciidoc index abd5a8f38..a9827b4d7 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -345,14 +345,11 @@ also create shortcuts to select particular layouts, and choose which layouts you want to enable/disable, see link:kitty/kitty.conf[kitty.conf] for examples. You can resize windows inside layouts. Press {sc_start_resizing_window} to -enter resizing mode. Then use the `W/N` (Wider/Narrower) and `T/S` -(Taller/Shorter) keys to change the window size. Press the `0` key to reset the -layout to default sizes. Press the `Ctrl` modifier to double the step size. Any -other key will exit resize mode. In a given window layout only some operations -may be possible for a particular window. For example, in the Tall layout you -can make the first window wider/narrower, but not taller/shorter. Note that -what you are resizing is actually not a window, but a row/column in the layout, -all windows in that row/column will be resized. +enter resizing mode and follow the on-screen instructions. In a given window +layout only some operations may be possible for a particular window. For +example, in the Tall layout you can make the first window wider/narrower, but +not taller/shorter. Note that what you are resizing is actually not a window, +but a row/column in the layout, all windows in that row/column will be resized. Some layouts take options to control their behavior. For example, the `fat` and `tall` layouts accept the `bias` option to control how the available space is split up. To specify the diff --git a/kittens/resize_window/__init__.py b/kittens/resize_window/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/resize_window/main.py b/kittens/resize_window/main.py new file mode 100644 index 000000000..5124f10db --- /dev/null +++ b/kittens/resize_window/main.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + + +import sys + +from kitty.cli import parse_args +from kitty.cmds import cmap, parse_subcommand_cli +from kitty.constants import version +from kitty.key_encoding import CTRL, ESCAPE, RELEASE, N, S, T, W +from kitty.remote_control import encode_send, parse_rc_args + +from ..tui.handler import Handler +from ..tui.loop import Loop +from ..tui.operations import styled + +global_opts = None + + +class Resize(Handler): + + print_on_fail = None + + def __init__(self, opts): + self.opts = opts + + def initialize(self): + global global_opts + global_opts = parse_rc_args(['kitty', '@resize-window'])[0] + self.original_size = self.screen_size + self.cmd.set_cursor_visible(False) + self.cmd.set_line_wrapping(False) + self.draw_screen() + + def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, multiplier=1): + resize_window = cmap['resize-window'] + increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment + increment *= multiplier + if is_decrease: + increment *= -1 + axis = 'reset' if reset else ('horizontal' if is_horizontal else 'vertical') + cmdline = [resize_window.name, '--self', '--increment={}'.format(increment), '--axis=' + axis] + opts, items = parse_subcommand_cli(resize_window, cmdline) + payload = resize_window(global_opts, opts, items) + send = {'cmd': resize_window.name, 'version': version, 'payload': payload} + self.write(encode_send(send)) + + def on_kitty_cmd_response(self, response): + if not response.get('ok'): + err = response['error'] + if response.get('tb'): + err += '\n' + response['tb'] + self.print_on_fail = err + self.quit_loop(1) + return + res = response.get('data') + if res: + self.cmd.bell() + + def on_text(self, text, in_bracketed_paste=False): + text = text.upper() + if text in 'WNTSR': + self.do_window_resize(is_decrease=text in 'NS', is_horizontal=text in 'WN', reset=text == 'R') + elif text == 'Q': + self.quit_loop(0) + + def on_key(self, key_event): + if key_event.type is RELEASE: + return + if key_event.key is ESCAPE: + self.quit_loop(0) + elif key_event.key in (W, N, T, S) and key_event.mods & CTRL: + self.do_window_resize(is_decrease=key_event.key in (N, S), is_horizontal=key_event.key in (W, N), multiplier=2) + + def on_resize(self, new_size): + Handler.on_resize(self, new_size) + self.draw_screen() + + def draw_screen(self): + self.cmd.clear_screen() + print = self.print + print(styled('Resize this window', bold=True, fg='gray', fg_intense=True)) + print() + print('Press one of the following keys:') + print(' {}ider'.format(styled('W', fg='green'))) + print(' {}arrower'.format(styled('N', fg='green'))) + print(' {}aller'.format(styled('T', fg='green'))) + print(' {}horter'.format(styled('S', fg='green'))) + print(' {}eset'.format(styled('R', fg='red'))) + print() + print('Press {} to quit resize mode'.format(styled('Esc', italic=True))) + print('Hold down {} to double step size'.format(styled('Ctrl', italic=True))) + print() + print(styled('Sizes', bold=True, fg='white', fg_intense=True)) + print('Original: {} rows {} cols'.format(self.original_size.rows, self.original_size.cols)) + print('Current: {} rows {} cols'.format( + styled(self.screen_size.rows, fg='magenta'), styled(self.screen_size.cols, fg='magenta'))) + + +OPTIONS = r''' +--horizontal-increment +default=2 +type=int +The base horizontal increment. + + +--vertical-increment +default=2 +type=int +The base vertical increment. +'''.format + + +def main(args): + msg = 'Resize the current window' + try: + args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window') + except SystemExit as e: + if e.code != 0: + print(e.args[0], file=sys.stderr) + input('Press Enter to quit') + return + + loop = Loop() + handler = Resize(args) + loop.loop(handler) + if handler.print_on_fail: + print(handler.print_on_fail) + input('Press Enter to quit') + raise SystemExit(loop.return_code) + + +def handle_result(args, data, target_window_id, boss): + pass diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index e5a64de2c..a32b9e430 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -61,6 +61,9 @@ def on_wakeup(self): def on_job_done(self, job_id, job_result): pass + def on_kitty_cmd_response(self, response): + pass + def write(self, data): if isinstance(data, str): data = data.encode('utf-8') diff --git a/kittens/tui/loop.py b/kittens/tui/loop.py index 73ac89fee..e77e43850 100644 --- a/kittens/tui/loop.py +++ b/kittens/tui/loop.py @@ -236,7 +236,9 @@ def _on_text(self, text): self.handler.on_text(chunk, self.in_bracketed_paste) def _on_dcs(self, dcs): - pass + if dcs.startswith('@kitty-cmd'): + import json + self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):])) def _on_csi(self, csi): q = csi[-1] diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 5d4ae6445..59a20875a 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -58,6 +58,10 @@ def bell() -> str: return '\a' +def beep() -> str: + return '\a' + + def set_window_title(value) -> str: return ('\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\') @@ -94,6 +98,7 @@ def scroll_screen(amt=1) -> str: STANDARD_COLORS = {name: i for i, name in enumerate( 'black red green yellow blue magenta cyan gray'.split())} +STANDARD_COLORS['white'] = STANDARD_COLORS['gray'] UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate( 'straight double curly'.split())} diff --git a/kitty/boss.py b/kitty/boss.py index 4543023ac..12e4e4544 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -19,12 +19,11 @@ appname, config_dir, editor, set_boss, supports_primary_selection ) from .fast_data_types import ( - GLFW_KEY_0, GLFW_KEY_N, GLFW_KEY_S, GLFW_KEY_W, GLFW_MOD_CONTROL, ChildMonitor, create_os_window, current_os_window, destroy_global_data, - destroy_sprite_map, get_clipboard_string, glfw_post_empty_event, - layout_sprite_map, mark_os_window_for_close, set_clipboard_string, - set_dpi_from_os_window, set_in_sequence_mode, show_window, - toggle_fullscreen, viewport_for_window + destroy_sprite_map, get_clipboard_string, + glfw_post_empty_event, layout_sprite_map, mark_os_window_for_close, + set_clipboard_string, set_dpi_from_os_window, set_in_sequence_mode, + show_window, toggle_fullscreen, viewport_for_window ) from .fonts.render import prerender, resize_fonts, set_font_family from .keys import get_shortcut, shortcut_matches @@ -414,23 +413,24 @@ def process_sequence(self, key, scancode, action, mods): if matched_action is not None: self.dispatch_action(matched_action) - def handle_resize_keypress(self, key, mods, os_window_id, tab_id, window_id): - tm = self.os_window_map.get(os_window_id) - if tm is None: + def start_resizing_window(self): + w = self.active_window + if w is None: return - tab = tm.tab_for_id(tab_id) - if tab is None: - return - if key == GLFW_KEY_0: - tab.reset_window_sizes() - return - is_horizontal = key in (GLFW_KEY_W, GLFW_KEY_N) - increment = self.opts.window_resize_step_cells if is_horizontal else self.opts.window_resize_step_lines - if mods == GLFW_MOD_CONTROL: - increment *= 2 - if key in (GLFW_KEY_N, GLFW_KEY_S): - increment *= -1 - tab.resize_window_by(window_id, increment, is_horizontal) + overlay_window = self._run_kitten('resize_window', args=[ + '--horizontal-increment={}'.format(self.opts.window_resize_step_cells), + '--vertical-increment={}'.format(self.opts.window_resize_step_lines) + ]) + if overlay_window is not None: + overlay_window.allow_remote_control = True + + def resize_layout_window(self, window, increment, is_horizontal, reset=False): + tab = window.tabref() + if tab is None or not increment: + return False + if reset: + return tab.reset_window_sizes() + return tab.resize_window_by(window.id, increment, is_horizontal) def default_bg_changed_for(self, window_id): w = self.window_id_map.get(window_id) @@ -539,6 +539,7 @@ def _run_kitten(self, kitten, args=(), type_of_input='none'): env={'KITTY_COMMON_OPTS': json.dumps(copts), 'PYTHONWARNINGS': 'ignore'}, overlay_for=w.id)) overlay_window.action_on_close = partial(self.on_kitten_finish, w.id, end_kitten) + return overlay_window def run_kitten(self, type_of_input, kitten, *args): import shlex diff --git a/kitty/cmds.py b/kitty/cmds.py index dedec58cb..dc9fc06c3 100644 --- a/kitty/cmds.py +++ b/kitty/cmds.py @@ -275,6 +275,54 @@ def close_window(boss, window, payload): # }}} +# resize_window {{{ +@cmd( + 'Resize the specified window', + 'Resize the specified window. Note that not all layouts can resize all windows in all directions.', + options_spec=MATCH_WINDOW_OPTION + '''\n +--increment -i +type=int +default=2 +The number of cells to change the size by, can be negative to decrease the size. + + +--axis -a +type=choices +choices=horizontal,vertical,reset +default=horizontal +The axis along which to resize. If |_ horizontal|, it will make the window wider or narrower by the specified increment. +If |_ vertical|, it will make the window taller or shorter by the specified increment. The special value |_ reset| will +reset the layout to its default configuration. + + +--self +type=bool-set +If specified close the window this command is run in, rather than the active window. +''', + argspec='' +) +def cmd_resize_window(global_opts, opts, args): + return {'match': opts.match, 'increment': opts.increment, 'axis': opts.axis, 'self': opts.self} + + +def resize_window(boss, window, payload): + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + if not windows: + raise MatchError(match) + else: + windows = [window if window and payload['self'] else boss.active_window] + resized = False + if windows and windows[0]: + resized = boss.resize_layout_window( + windows[0], increment=payload['increment'], is_horizontal=payload['axis'] == 'horizontal', + reset=payload['axis'] == 'reset' + ) + return resized +# }}} + + # close_tab {{{ @cmd( 'Close the specified tab(s)', diff --git a/kitty/keys.c b/kitty/keys.c index 36f821a6d..2930d1364 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -81,25 +81,10 @@ is_ascii_control_char(char c) { return c == 0 || (1 <= c && c <= 31) || c == 127; } -static inline bool -handle_resize_key(int key, int action, int mods) { - if (action == GLFW_RELEASE) return true; - if (key == GLFW_KEY_T || key == GLFW_KEY_S || key == GLFW_KEY_W || key == GLFW_KEY_N || key == GLFW_KEY_0) { - call_boss(handle_resize_keypress, "iiKKK", key, mods, global_state.currently_resizing.os_window_id, global_state.currently_resizing.tab_id, global_state.currently_resizing.window_id); - return true; - } - if (key == GLFW_KEY_LEFT_CONTROL || key == GLFW_KEY_RIGHT_CONTROL) return true; - return false; -} - void on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) { Window *w = active_window(); if (!w) return; - if (global_state.currently_resizing.os_window_id) { - if (handle_resize_key(key, action, mods)) return; - terminate_resize_mode(); - } if (global_state.in_sequence_mode) { if ( action != GLFW_RELEASE && diff --git a/kitty/layout.py b/kitty/layout.py index 45bebd1f3..58d4760d7 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -114,7 +114,7 @@ def remove_all_biases(self): def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal=True): idx = idx_for_id(window_id, all_windows) if idx is None: - return + return False w = all_windows[idx] windows = process_overlaid_windows(all_windows)[1] idx = idx_for_id(w.id, windows) @@ -122,6 +122,7 @@ def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal idx = idx_for_id(w.overlay_window_id, windows) if idx is not None: return self.apply_bias(idx, increment, len(windows), is_horizontal) + return False def parse_layout_opts(self, layout_opts): if not layout_opts: diff --git a/kitty/mouse.c b/kitty/mouse.c index 90d125001..1dc0c9cb7 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -303,7 +303,6 @@ open_url(Window *w) { } HANDLER(handle_button_event) { - if (global_state.currently_resizing.os_window_id) { terminate_resize_mode(); } Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; bool is_release = !global_state.callback_os_window->mouse_button_pressed[button]; if (window_idx != t->active_window) { diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 14b3b6617..e49df4f38 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -39,10 +39,14 @@ def handle_cmd(boss, window, cmd): '''.format, appname=appname) +def encode_send(send): + send = ('@kitty-cmd' + json.dumps(send)).encode('ascii') + return b'\x1bP' + send + b'\x1b\\' + + def do_io(to, send, no_response): import socket - send = ('@kitty-cmd' + json.dumps(send)).encode('ascii') - send = b'\x1bP' + send + b'\x1b\\' + send = encode_send(send) if to: family, address = parse_address_spec(to)[:2] s = socket.socket(family) @@ -79,8 +83,10 @@ def more_needed(data): return response -def main(args): - all_commands = tuple(sorted(cmap)) +all_commands = tuple(sorted(cmap)) + + +def parse_rc_args(args): cmds = (' |G {}|\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands) msg = ( 'Control {appname} by sending it commands. Add' @@ -90,7 +96,11 @@ def main(args): '{appname} @ |_ command| -h' ).format(appname=appname, cmds='\n'.join(cmds)) - global_opts, items = parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname)) + return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname)) + + +def main(args): + global_opts, items = parse_rc_args(args) if not items: from kitty.shell import main diff --git a/kitty/state.c b/kitty/state.c index 6246b2961..a9f1caa4e 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -268,14 +268,6 @@ os_window_regions(OSWindow *os_window, Region *central, Region *tab_bar) { } } -void -terminate_resize_mode() { - global_state.currently_resizing.os_window_id = 0; - global_state.currently_resizing.tab_id = 0; - global_state.currently_resizing.window_id = 0; -} - - // Python API {{{ #define PYWRAP0(name) static PyObject* py##name(PYNOARG) @@ -594,14 +586,6 @@ PYWRAP1(set_display_state) { Py_RETURN_NONE; } -PYWRAP1(enter_resize_mode) { - global_state.currently_resizing.os_window_id = 0; - global_state.currently_resizing.tab_id = 0; - global_state.currently_resizing.window_id = 0; - PA("|KKK", &global_state.currently_resizing.os_window_id, &global_state.currently_resizing.tab_id, &global_state.currently_resizing.window_id); - Py_RETURN_NONE; -} - THREE_ID_OBJ(update_window_title) THREE_ID(remove_window) PYWRAP1(resolve_key_mods) { int mods; PA("ii", &kitty_mod, &mods); return PyLong_FromLong(resolve_mods(mods)); } @@ -648,7 +632,6 @@ static PyMethodDef module_methods[] = { MW(update_window_visibility, METH_VARARGS), MW(set_boss, METH_O), MW(set_display_state, METH_VARARGS), - MW(enter_resize_mode, METH_VARARGS), MW(destroy_global_data, METH_NOARGS), {NULL, NULL, 0, NULL} /* Sentinel */ diff --git a/kitty/state.h b/kitty/state.h index 9d71714e6..a72dac8d0 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -137,9 +137,6 @@ typedef struct { bool debug_gl, debug_font_fallback; bool has_pending_resizes; bool in_sequence_mode; - struct { - id_type os_window_id, tab_id, window_id; - } currently_resizing; } GlobalState; extern GlobalState global_state; @@ -186,4 +183,3 @@ void free_texture(uint32_t*); void send_image_to_gpu(uint32_t*, const void*, int32_t, int32_t, bool, bool); void send_sprite_to_gpu(unsigned int, unsigned int, unsigned int, pixel*); void set_titlebar_color(OSWindow *w, color_type color); -void terminate_resize_mode(); diff --git a/kitty/tabs.py b/kitty/tabs.py index becd60aab..8937c0257 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -12,9 +12,8 @@ from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland from .fast_data_types import ( DECAWM, Screen, add_tab, glfw_post_empty_event, mark_tab_bar_dirty, - next_window_id, pt_to_px, remove_tab, remove_window, ring_bell, - set_active_tab, set_tab_bar_render_data, swap_tabs, viewport_for_window, - x11_window_id + next_window_id, pt_to_px, remove_tab, remove_window, set_active_tab, + set_tab_bar_render_data, swap_tabs, viewport_for_window, x11_window_id ) from .layout import Rect, create_layout_object_for, evict_cached_layouts from .session import resolved_shell @@ -167,8 +166,8 @@ def resize_window_by(self, window_id, increment, is_horizontal): 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() - else: - ring_bell(self.os_window_id) + return '' + return 'Could not resize' def reset_window_sizes(self): if self.current_layout.remove_all_biases(): diff --git a/kitty/window.py b/kitty/window.py index ed2dad038..95ea1dc67 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -17,10 +17,10 @@ BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, OSC, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen, - add_window, compile_program, enter_resize_mode, get_clipboard_string, - glfw_post_empty_event, init_cell_program, init_cursor_program, - set_clipboard_string, set_titlebar_color, set_window_render_data, - update_window_title, update_window_visibility, viewport_for_window + add_window, compile_program, get_clipboard_string, glfw_post_empty_event, + init_cell_program, init_cursor_program, set_clipboard_string, + set_titlebar_color, set_window_render_data, update_window_title, + update_window_visibility, viewport_for_window ) from .keys import keyboard_mode_name from .rgb import to_color @@ -193,9 +193,6 @@ def send_text(self, *args): return True self.write_to_child(text) - def start_resizing_window(self): - enter_resize_mode(self.os_window_id, self.tab_id, self.id) - def write_to_child(self, data): if data: if get_boss().child_monitor.needs_write(self.id, data) is not True: