Use an overlay window for window resizing

Also implement a remote command to resize windows
This commit is contained in:
Kovid Goyal 2018-05-17 23:26:41 +05:30
parent 7c47bd547f
commit 8ea84c97d5
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
16 changed files with 246 additions and 85 deletions

View File

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

View File

View File

@ -0,0 +1,135 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

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

View File

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

View File

@ -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())}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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():

View File

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