broadcast kitten: Show a "fake" cursor in all windows being broadcast too
Fixes #4225
This commit is contained in:
parent
74a5d3a25e
commit
0830e66e76
@ -53,6 +53,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
- A new option :opt:`bell_path` to specify the path to a sound file
|
- A new option :opt:`bell_path` to specify the path to a sound file
|
||||||
to use as the bell sound
|
to use as the bell sound
|
||||||
|
|
||||||
|
- broadcast kitten: Show a "fake" cursor in all windows being broadcast too
|
||||||
|
(:iss:`4225`)
|
||||||
|
|
||||||
- Fix a regression that caused :option:`kitty --title` to not work when
|
- Fix a regression that caused :option:`kitty --title` to not work when
|
||||||
opening new OS windows using :option:`kitty --single-instance` (:iss:`3893`)
|
opening new OS windows using :option:`kitty --single-instance` (:iss:`3893`)
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from kitty.cli_stub import BroadcastCLIOptions
|
|||||||
from kitty.key_encoding import encode_key_event
|
from kitty.key_encoding import encode_key_event
|
||||||
from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
|
from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
|
||||||
from kitty.remote_control import create_basic_command, encode_send
|
from kitty.remote_control import create_basic_command, encode_send
|
||||||
|
from kitty.short_uuid import uuid4
|
||||||
from kitty.typing import KeyEventType, ScreenSize
|
from kitty.typing import KeyEventType, ScreenSize
|
||||||
|
|
||||||
from ..tui.handler import Handler
|
from ..tui.handler import Handler
|
||||||
@ -19,17 +20,26 @@ from ..tui.loop import Loop
|
|||||||
from ..tui.operations import RESTORE_CURSOR, SAVE_CURSOR, styled
|
from ..tui.operations import RESTORE_CURSOR, SAVE_CURSOR, styled
|
||||||
|
|
||||||
|
|
||||||
|
def session_command(payload: Dict[str, Any], start: bool = True) -> bytes:
|
||||||
|
payload = payload.copy()
|
||||||
|
payload['data'] = 'session:' + ('start' if start else 'end')
|
||||||
|
send = create_basic_command('send-text', payload, no_response=True)
|
||||||
|
return encode_send(send)
|
||||||
|
|
||||||
|
|
||||||
class Broadcast(Handler):
|
class Broadcast(Handler):
|
||||||
|
|
||||||
def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None:
|
def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None:
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.initial_strings = initial_strings
|
self.initial_strings = initial_strings
|
||||||
self.payload = {'exclude_active': True, 'data': '', 'match': opts.match, 'match_tab': opts.match_tab}
|
self.payload = {'exclude_active': True, 'data': '', 'match': opts.match, 'match_tab': opts.match_tab, 'session_id': uuid4()}
|
||||||
self.line_edit = LineEdit()
|
self.line_edit = LineEdit()
|
||||||
|
self.session_started = False
|
||||||
if not opts.match and not opts.match_tab:
|
if not opts.match and not opts.match_tab:
|
||||||
self.payload['all'] = True
|
self.payload['all'] = True
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
|
self.write_broadcast_session()
|
||||||
self.print('Type the text to broadcast below, press', styled('Ctrl+Esc', fg='yellow'), 'to quit:')
|
self.print('Type the text to broadcast below, press', styled('Ctrl+Esc', fg='yellow'), 'to quit:')
|
||||||
for x in self.initial_strings:
|
for x in self.initial_strings:
|
||||||
self.write_broadcast_text(x)
|
self.write_broadcast_text(x)
|
||||||
@ -83,6 +93,10 @@ class Broadcast(Handler):
|
|||||||
send = create_basic_command('send-text', payload, no_response=True)
|
send = create_basic_command('send-text', payload, no_response=True)
|
||||||
self.write(encode_send(send))
|
self.write(encode_send(send))
|
||||||
|
|
||||||
|
def write_broadcast_session(self, start: bool = True) -> None:
|
||||||
|
self.session_started = start
|
||||||
|
self.write(session_command(self.payload, start))
|
||||||
|
|
||||||
|
|
||||||
OPTIONS = (MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
|
OPTIONS = (MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
|
||||||
help_text = 'Broadcast typed text to all kitty windows. By default text is sent to all windows, unless one of the matching options is specified'
|
help_text = 'Broadcast typed text to all kitty windows. By default text is sent to all windows, unless one of the matching options is specified'
|
||||||
@ -105,7 +119,12 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
loop = Loop()
|
loop = Loop()
|
||||||
handler = Broadcast(opts, items)
|
handler = Broadcast(opts, items)
|
||||||
loop.loop(handler)
|
try:
|
||||||
|
loop.loop(handler)
|
||||||
|
finally:
|
||||||
|
if handler.session_started:
|
||||||
|
sys.stdout.buffer.write(session_command(handler.payload, False))
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -430,6 +430,10 @@ def add_timer(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def remove_timer(timer_id: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def monitor_pid(pid: int) -> None:
|
def monitor_pid(pid: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from functools import partial
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
from kitty.fast_data_types import KeyEvent as WindowSystemKeyEvent
|
from kitty.fast_data_types import (
|
||||||
|
KeyEvent as WindowSystemKeyEvent, add_timer, get_boss, remove_timer
|
||||||
|
)
|
||||||
from kitty.key_encoding import decode_key_event_as_window_system_key
|
from kitty.key_encoding import decode_key_event_as_window_system_key
|
||||||
from kitty.options.utils import parse_send_text_bytes
|
from kitty.options.utils import parse_send_text_bytes
|
||||||
|
|
||||||
@ -19,6 +22,30 @@ if TYPE_CHECKING:
|
|||||||
from kitty.cli_stub import SendTextRCOptions as CLIOptions
|
from kitty.cli_stub import SendTextRCOptions as CLIOptions
|
||||||
|
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
id: str
|
||||||
|
window_ids: Set[int]
|
||||||
|
timer_id: int = 0
|
||||||
|
|
||||||
|
def __init__(self, id: str):
|
||||||
|
self.id = id
|
||||||
|
self.window_ids = set()
|
||||||
|
|
||||||
|
|
||||||
|
sessions_map: Dict[str, Session] = {}
|
||||||
|
TIMEOUT_FOR_SESSION = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def clear_unfocused_cursors(sid: str, *a: Any) -> None:
|
||||||
|
s = sessions_map.pop(sid, None)
|
||||||
|
if s is not None:
|
||||||
|
boss = get_boss()
|
||||||
|
for wid in s.window_ids:
|
||||||
|
qw = boss.window_id_map.get(wid)
|
||||||
|
if qw is not None:
|
||||||
|
qw.screen.render_unfocused_cursor = 0
|
||||||
|
|
||||||
|
|
||||||
class SendText(RemoteCommand):
|
class SendText(RemoteCommand):
|
||||||
'''
|
'''
|
||||||
data+: The data being sent. Can be either: text: followed by text or base64: followed by standard base64 encoded bytes
|
data+: The data being sent. Can be either: text: followed by text or base64: followed by standard base64 encoded bytes
|
||||||
@ -26,6 +53,7 @@ class SendText(RemoteCommand):
|
|||||||
match_tab: A string indicating the tab to send text to
|
match_tab: A string indicating the tab to send text to
|
||||||
all: A boolean indicating all windows should be matched.
|
all: A boolean indicating all windows should be matched.
|
||||||
exclude_active: A boolean that prevents sending text to the active window
|
exclude_active: A boolean that prevents sending text to the active window
|
||||||
|
session_id: A string that identifies a "broadcast session"
|
||||||
'''
|
'''
|
||||||
short_desc = 'Send arbitrary text to specified windows'
|
short_desc = 'Send arbitrary text to specified windows'
|
||||||
desc = (
|
desc = (
|
||||||
@ -138,6 +166,7 @@ Do not send text to the active window, even if it is one of the matched windows.
|
|||||||
windows += tuple(tab)
|
windows += tuple(tab)
|
||||||
pdata: str = payload_get('data')
|
pdata: str = payload_get('data')
|
||||||
encoding, _, q = pdata.partition(':')
|
encoding, _, q = pdata.partition(':')
|
||||||
|
session = ''
|
||||||
if encoding == 'text':
|
if encoding == 'text':
|
||||||
data: Union[bytes, WindowSystemKeyEvent] = q.encode('utf-8')
|
data: Union[bytes, WindowSystemKeyEvent] = q.encode('utf-8')
|
||||||
elif encoding == 'base64':
|
elif encoding == 'base64':
|
||||||
@ -148,18 +177,49 @@ Do not send text to the active window, even if it is one of the matched windows.
|
|||||||
if candidate is None:
|
if candidate is None:
|
||||||
raise ValueError(f'Could not decode window system key: {q}')
|
raise ValueError(f'Could not decode window system key: {q}')
|
||||||
data = candidate
|
data = candidate
|
||||||
|
elif encoding == 'session':
|
||||||
|
session = q
|
||||||
else:
|
else:
|
||||||
raise TypeError(f'Invalid encoding for send-text data: {encoding}')
|
raise TypeError(f'Invalid encoding for send-text data: {encoding}')
|
||||||
exclude_active = payload_get('exclude_active')
|
exclude_active = payload_get('exclude_active')
|
||||||
for window in windows:
|
actual_windows = (w for w in windows if w is not None and (not exclude_active or w is not boss.active_window))
|
||||||
|
sid = payload_get('session_id', '')
|
||||||
|
|
||||||
|
def create_or_update_session() -> Session:
|
||||||
|
s = sessions_map.setdefault(sid, Session(sid))
|
||||||
|
if s.timer_id:
|
||||||
|
remove_timer(s.timer_id)
|
||||||
|
s.timer_id = 0
|
||||||
|
return s
|
||||||
|
|
||||||
|
if session == 'end':
|
||||||
|
s = create_or_update_session()
|
||||||
|
for w in actual_windows:
|
||||||
|
w.screen.render_unfocused_cursor = 0
|
||||||
|
s.window_ids.discard(w.id)
|
||||||
|
clear_unfocused_cursors(sid)
|
||||||
|
elif session == 'start':
|
||||||
|
s = create_or_update_session()
|
||||||
if window is not None:
|
if window is not None:
|
||||||
if not exclude_active or window is not boss.active_window:
|
window.actions_on_close.append(partial(clear_unfocused_cursors, sid))
|
||||||
if isinstance(data, WindowSystemKeyEvent):
|
s.timer_id = add_timer(partial(clear_unfocused_cursors, sid), TIMEOUT_FOR_SESSION, False)
|
||||||
kdata = window.encoded_key(data)
|
for w in actual_windows:
|
||||||
if kdata:
|
w.screen.render_unfocused_cursor = 1
|
||||||
window.write_to_child(kdata)
|
s.window_ids.add(w.id)
|
||||||
else:
|
else:
|
||||||
window.write_to_child(data)
|
if sid:
|
||||||
|
s = create_or_update_session()
|
||||||
|
s.timer_id = add_timer(partial(clear_unfocused_cursors, sid), TIMEOUT_FOR_SESSION, False)
|
||||||
|
for w in actual_windows:
|
||||||
|
if sid:
|
||||||
|
w.screen.render_unfocused_cursor = 1
|
||||||
|
s.window_ids.add(w.id)
|
||||||
|
if isinstance(data, WindowSystemKeyEvent):
|
||||||
|
kdata = w.encoded_key(data)
|
||||||
|
if kdata:
|
||||||
|
w.write_to_child(kdata)
|
||||||
|
else:
|
||||||
|
w.write_to_child(data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user