main.py 8.7 KB

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