main.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
  3. import sys
  4. from base64 import standard_b64encode
  5. from gettext import gettext as _
  6. from typing import Any, Dict, List, Optional, Tuple
  7. from kitty.cli import parse_args
  8. from kitty.cli_stub import BroadcastCLIOptions
  9. from kitty.key_encoding import encode_key_event
  10. from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
  11. from kitty.remote_control import create_basic_command, encode_send
  12. from kitty.short_uuid import uuid4
  13. from kitty.typing import KeyEventType, ScreenSize
  14. from ..tui.handler import Handler
  15. from ..tui.line_edit import LineEdit
  16. from ..tui.loop import Loop
  17. from ..tui.operations import RESTORE_CURSOR, SAVE_CURSOR, styled
  18. def session_command(payload: Dict[str, Any], start: bool = True) -> bytes:
  19. payload = payload.copy()
  20. payload['data'] = 'session:' + ('start' if start else 'end')
  21. send = create_basic_command('send-text', payload, no_response=True)
  22. return encode_send(send)
  23. class Broadcast(Handler):
  24. def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None:
  25. self.opts = opts
  26. self.hide_input = False
  27. self.initial_strings = initial_strings
  28. self.payload = {'exclude_active': True, 'data': '', 'match': opts.match, 'match_tab': opts.match_tab, 'session_id': uuid4()}
  29. self.line_edit = LineEdit()
  30. self.session_started = False
  31. if not opts.match and not opts.match_tab:
  32. self.payload['all'] = True
  33. def initialize(self) -> None:
  34. self.write_broadcast_session()
  35. self.print('Type the text to broadcast below, press', styled(self.opts.end_session, fg='yellow'), 'to quit:')
  36. for x in self.initial_strings:
  37. self.write_broadcast_text(x)
  38. self.write(SAVE_CURSOR)
  39. def commit_line(self) -> None:
  40. self.write(RESTORE_CURSOR + SAVE_CURSOR)
  41. self.cmd.clear_to_end_of_screen()
  42. self.line_edit.write(self.write, screen_cols=self.screen_size.cols)
  43. def on_resize(self, screen_size: ScreenSize) -> None:
  44. super().on_resize(screen_size)
  45. self.commit_line()
  46. def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
  47. self.write_broadcast_text(text)
  48. if not self.hide_input:
  49. self.line_edit.on_text(text, in_bracketed_paste)
  50. self.commit_line()
  51. def on_interrupt(self) -> None:
  52. self.write_broadcast_text('\x03')
  53. self.line_edit.clear()
  54. self.commit_line()
  55. def on_eot(self) -> None:
  56. self.write_broadcast_text('\x04')
  57. def on_key(self, key_event: KeyEventType) -> None:
  58. if key_event.matches(self.opts.hide_input_toggle):
  59. self.hide_input ^= True
  60. self.cmd.set_cursor_visible(not self.hide_input)
  61. if self.hide_input:
  62. self.end_line()
  63. self.print('Input hidden, press', styled(self.opts.hide_input_toggle, fg='yellow'), 'to unhide:')
  64. self.end_line()
  65. return
  66. if key_event.matches(self.opts.end_session):
  67. self.quit_loop(0)
  68. return
  69. if not self.hide_input and self.line_edit.on_key(key_event):
  70. self.commit_line()
  71. if key_event.matches('enter'):
  72. self.write_broadcast_text('\r')
  73. self.end_line()
  74. return
  75. ek = encode_key_event(key_event)
  76. ek = standard_b64encode(ek.encode('utf-8')).decode('ascii')
  77. self.write_broadcast_data('kitty-key:' + ek)
  78. def end_line(self) -> None:
  79. self.print('')
  80. self.line_edit.clear()
  81. self.write(SAVE_CURSOR)
  82. def write_broadcast_text(self, text: str) -> None:
  83. self.write_broadcast_data('base64:' + standard_b64encode(text.encode('utf-8')).decode('ascii'))
  84. def write_broadcast_data(self, data: str) -> None:
  85. payload = self.payload.copy()
  86. payload['data'] = data
  87. send = create_basic_command('send-text', payload, no_response=True)
  88. self.write(encode_send(send))
  89. def write_broadcast_session(self, start: bool = True) -> None:
  90. self.session_started = start
  91. self.write(session_command(self.payload, start))
  92. OPTIONS = ('''
  93. --hide-input-toggle
  94. default=Ctrl+Alt+Esc
  95. Key to press that will toggle hiding of the input in the broadcast window itself.
  96. Useful while typing a password, prevents the password from being visible on the screen.
  97. --end-session
  98. default=Ctrl+Esc
  99. Key to press to end the broadcast session.
  100. ''' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
  101. help_text = 'Broadcast typed text to kitty windows. By default text is sent to all windows, unless one of the matching options is specified'
  102. usage = '[initial text to send ...]'
  103. def parse_broadcast_args(args: List[str]) -> Tuple[BroadcastCLIOptions, List[str]]:
  104. return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten broadcast', result_class=BroadcastCLIOptions)
  105. def main(args: List[str]) -> Optional[Dict[str, Any]]:
  106. try:
  107. opts, items = parse_broadcast_args(args[1:])
  108. except SystemExit as e:
  109. if e.code != 0:
  110. print(e.args[0], file=sys.stderr)
  111. input(_('Press Enter to quit'))
  112. return None
  113. sys.stdout.flush()
  114. loop = Loop()
  115. handler = Broadcast(opts, items)
  116. try:
  117. loop.loop(handler)
  118. finally:
  119. if handler.session_started:
  120. sys.stdout.buffer.write(session_command(handler.payload, False))
  121. sys.stdout.buffer.flush()
  122. return None
  123. if __name__ == '__main__':
  124. main(sys.argv)
  125. elif __name__ == '__doc__':
  126. cd = sys.cli_docs # type: ignore
  127. cd['usage'] = usage
  128. cd['options'] = OPTIONS
  129. cd['help_text'] = help_text
  130. cd['short_desc'] = 'Broadcast typed text to kitty windows'