main.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
  3. import re
  4. import sys
  5. from binascii import hexlify, unhexlify
  6. from contextlib import suppress
  7. from typing import Dict, Optional, Type
  8. from kitty.constants import appname, str_version
  9. from kitty.options.types import Options
  10. from kitty.terminfo import names
  11. class Query:
  12. name: str = ''
  13. ans: str = ''
  14. help_text: str = ''
  15. override_query_name: str = ''
  16. @property
  17. def query_name(self) -> str:
  18. return self.override_query_name or f'kitty-query-{self.name}'
  19. def __init__(self) -> None:
  20. self.encoded_query_name = hexlify(self.query_name.encode('utf-8')).decode('ascii')
  21. self.pat = re.compile(f'\x1bP([01])\\+r{self.encoded_query_name}(.*?)\x1b\\\\'.encode('ascii'))
  22. def query_code(self) -> str:
  23. return f"\x1bP+q{self.encoded_query_name}\x1b\\"
  24. def decode_response(self, res: bytes) -> str:
  25. return unhexlify(res).decode('utf-8')
  26. def more_needed(self, buffer: bytes) -> bool:
  27. m = self.pat.search(buffer)
  28. if m is None:
  29. return True
  30. if m.group(1) == b'1':
  31. q = m.group(2)
  32. if q.startswith(b'='):
  33. with suppress(Exception):
  34. self.ans = self.decode_response(memoryview(q)[1:])
  35. return False
  36. def output_line(self) -> str:
  37. return self.ans
  38. @staticmethod
  39. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  40. raise NotImplementedError()
  41. all_queries: Dict[str, Type[Query]] = {}
  42. def query(cls: Type[Query]) -> Type[Query]:
  43. all_queries[cls.name] = cls
  44. return cls
  45. @query
  46. class TerminalName(Query):
  47. name: str = 'name'
  48. override_query_name: str = 'name'
  49. help_text: str = f'Terminal name (e.g. :code:`{names[0]}`)'
  50. @staticmethod
  51. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  52. return appname
  53. @query
  54. class TerminalVersion(Query):
  55. name: str = 'version'
  56. help_text: str = f'Terminal version (e.g. :code:`{str_version}`)'
  57. @staticmethod
  58. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  59. return str_version
  60. @query
  61. class AllowHyperlinks(Query):
  62. name: str = 'allow_hyperlinks'
  63. help_text: str = 'The config option :opt:`allow_hyperlinks` in :file:`kitty.conf` for allowing hyperlinks can be :code:`yes`, :code:`no` or :code:`ask`'
  64. @staticmethod
  65. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  66. return 'ask' if opts.allow_hyperlinks == 0b11 else ('yes' if opts.allow_hyperlinks else 'no')
  67. @query
  68. class FontFamily(Query):
  69. name: str = 'font_family'
  70. help_text: str = 'The current font\'s PostScript name'
  71. @staticmethod
  72. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  73. from kitty.fast_data_types import current_fonts
  74. cf = current_fonts(os_window_id)
  75. return cf['medium'].postscript_name()
  76. @query
  77. class BoldFont(Query):
  78. name: str = 'bold_font'
  79. help_text: str = 'The current bold font\'s PostScript name'
  80. @staticmethod
  81. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  82. from kitty.fast_data_types import current_fonts
  83. cf = current_fonts(os_window_id)
  84. return cf['bold'].postscript_name()
  85. @query
  86. class ItalicFont(Query):
  87. name: str = 'italic_font'
  88. help_text: str = 'The current italic font\'s PostScript name'
  89. @staticmethod
  90. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  91. from kitty.fast_data_types import current_fonts
  92. cf = current_fonts(os_window_id)
  93. return cf['italic'].postscript_name()
  94. @query
  95. class BiFont(Query):
  96. name: str = 'bold_italic_font'
  97. help_text: str = 'The current bold-italic font\'s PostScript name'
  98. @staticmethod
  99. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  100. from kitty.fast_data_types import current_fonts
  101. cf = current_fonts(os_window_id)
  102. return cf['bi'].postscript_name()
  103. @query
  104. class FontSize(Query):
  105. name: str = 'font_size'
  106. help_text: str = 'The current font size in pts'
  107. @staticmethod
  108. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  109. from kitty.fast_data_types import current_fonts
  110. cf = current_fonts(os_window_id)
  111. return f'{cf["font_sz_in_pts"]:g}'
  112. @query
  113. class DpiX(Query):
  114. name: str = 'dpi_x'
  115. help_text: str = 'The current DPI on the x-axis'
  116. @staticmethod
  117. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  118. from kitty.fast_data_types import current_fonts
  119. cf = current_fonts(os_window_id)
  120. return f'{cf["logical_dpi_x"]:g}'
  121. @query
  122. class DpiY(Query):
  123. name: str = 'dpi_y'
  124. help_text: str = 'The current DPI on the y-axis'
  125. @staticmethod
  126. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  127. from kitty.fast_data_types import current_fonts
  128. cf = current_fonts(os_window_id)
  129. return f'{cf["logical_dpi_y"]:g}'
  130. @query
  131. class Foreground(Query):
  132. name: str = 'foreground'
  133. help_text: str = 'The current foreground color as a 24-bit # color code'
  134. @staticmethod
  135. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  136. from kitty.fast_data_types import Color, get_boss
  137. boss = get_boss()
  138. w = boss.window_id_map.get(window_id)
  139. if w is None:
  140. return opts.foreground.as_sharp
  141. col = w.screen.color_profile.default_fg
  142. r, g, b = col >> 16, (col >> 8) & 0xff, col & 0xff
  143. return Color(r, g, b).as_sharp
  144. @query
  145. class Background(Query):
  146. name: str = 'background'
  147. help_text: str = 'The current background color as a 24-bit # color code'
  148. @staticmethod
  149. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  150. from kitty.fast_data_types import Color, get_boss
  151. boss = get_boss()
  152. w = boss.window_id_map.get(window_id)
  153. if w is None:
  154. return opts.background.as_sharp
  155. col = w.screen.color_profile.default_bg
  156. r, g, b = col >> 16, (col >> 8) & 0xff, col & 0xff
  157. return Color(r, g, b).as_sharp
  158. @query
  159. class ClipboardControl(Query):
  160. name: str = 'clipboard_control'
  161. help_text: str = 'The config option :opt:`clipboard_control` in :file:`kitty.conf` for allowing reads/writes to/from the clipboard'
  162. @staticmethod
  163. def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
  164. return ' '.join(opts.clipboard_control)
  165. def get_result(name: str, window_id: int, os_window_id: int) -> Optional[str]:
  166. from kitty.fast_data_types import get_options
  167. q = all_queries.get(name)
  168. if q is None:
  169. return None
  170. return q.get_result(get_options(), window_id, os_window_id)
  171. def options_spec() -> str:
  172. return '''\
  173. --wait-for
  174. type=float
  175. default=10
  176. The amount of time (in seconds) to wait for a response from the terminal, after
  177. querying it.
  178. '''
  179. help_text = '''\
  180. Query the terminal this kitten is run in for various capabilities. This sends
  181. escape codes to the terminal and based on its response prints out data about
  182. supported capabilities. Note that this is a blocking operation, since it has to
  183. wait for a response from the terminal. You can control the maximum wait time via
  184. the :code:`--wait-for` option.
  185. The output is lines of the form::
  186. query: data
  187. If a particular :italic:`query` is unsupported by the running kitty version, the
  188. :italic:`data` will be blank.
  189. Note that when calling this from another program, be very careful not to perform
  190. any I/O on the terminal device until this kitten exits.
  191. Available queries are:
  192. {}
  193. '''.format('\n'.join(
  194. f':code:`{name}`:\n {c.help_text}\n' for name, c in all_queries.items()))
  195. usage = '[query1 query2 ...]'
  196. if __name__ == '__main__':
  197. raise SystemExit('Should be run as kitten hints')
  198. elif __name__ == '__doc__':
  199. cd = sys.cli_docs # type: ignore
  200. cd['usage'] = usage
  201. cd['options'] = options_spec
  202. cd['help_text'] = help_text
  203. cd['short_desc'] = 'Query the terminal for various capabilities'