main.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
  3. import sys
  4. from typing import Any, Callable, Dict, List, Tuple
  5. from kitty.cli import parse_args
  6. from kitty.cli_stub import PanelCLIOptions
  7. from kitty.constants import appname, is_macos, is_wayland
  8. from kitty.fast_data_types import (
  9. GLFW_EDGE_BOTTOM,
  10. GLFW_EDGE_LEFT,
  11. GLFW_EDGE_NONE,
  12. GLFW_EDGE_RIGHT,
  13. GLFW_EDGE_TOP,
  14. GLFW_FOCUS_EXCLUSIVE,
  15. GLFW_FOCUS_NOT_ALLOWED,
  16. GLFW_FOCUS_ON_DEMAND,
  17. GLFW_LAYER_SHELL_BACKGROUND,
  18. GLFW_LAYER_SHELL_OVERLAY,
  19. GLFW_LAYER_SHELL_PANEL,
  20. GLFW_LAYER_SHELL_TOP,
  21. glfw_primary_monitor_size,
  22. make_x11_window_a_dock_window,
  23. )
  24. from kitty.os_window_size import WindowSizeData, edge_spacing
  25. from kitty.types import LayerShellConfig
  26. from kitty.typing import EdgeLiteral
  27. OPTIONS = r'''
  28. --lines
  29. type=int
  30. default=1
  31. The number of lines shown in the panel. Ignored for background and vertical panels.
  32. --columns
  33. type=int
  34. default=1
  35. The number of columns shown in the panel. Ignored for background and horizontal panels.
  36. --margin-top
  37. type=int
  38. default=0
  39. Request a given top margin to the compositor.
  40. Only works on a Wayland compositor that supports the wlr layer shell protocol.
  41. --margin-left
  42. type=int
  43. default=0
  44. Request a given left margin to the compositor.
  45. Only works on a Wayland compositor that supports the wlr layer shell protocol.
  46. --margin-bottom
  47. type=int
  48. default=0
  49. Request a given bottom margin to the compositor.
  50. Only works on a Wayland compositor that supports the wlr layer shell protocol.
  51. --margin-right
  52. type=int
  53. default=0
  54. Request a given right margin to the compositor.
  55. Only works on a Wayland compositor that supports the wlr layer shell protocol.
  56. --edge
  57. choices=top,bottom,left,right,background,none
  58. default=top
  59. Which edge of the screen to place the panel on. Note that some window managers
  60. (such as i3) do not support placing docked windows on the left and right edges.
  61. The value :code:`background` means make the panel the "desktop wallpaper". This
  62. is only supported on Wayland, not X11 and note that when using sway if you set
  63. a background in your sway config it will cover the background drawn using this
  64. kitten.
  65. The value :code:`none` anchors the panel to the top left corner by default
  66. and the panel should be placed using margins parameters.
  67. --layer
  68. choices=background,bottom,top,overlay
  69. default=bottom
  70. On a Wayland compositor that supports the wlr layer shell protocol, specifies the layer
  71. on which the panel should be drawn. This parameter is ignored and set to
  72. :code:`background` if :option:`--edge` is set to :code:`background`.
  73. --config -c
  74. type=list
  75. Path to config file to use for kitty when drawing the panel.
  76. --override -o
  77. type=list
  78. Override individual kitty configuration options, can be specified multiple times.
  79. Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font_size=20
  80. --output-name
  81. On Wayland, the panel can only be displayed on a single monitor (output) at a time. This allows
  82. you to specify which output is used, by name. If not specified the compositor will choose an
  83. output automatically, typically the last output the user interacted with or the primary monitor.
  84. --class
  85. dest=cls
  86. default={appname}-panel
  87. condition=not is_macos
  88. Set the class part of the :italic:`WM_CLASS` window property. On Wayland, it sets the app id.
  89. --name
  90. condition=not is_macos
  91. Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
  92. --focus-policy
  93. choices=not-allowed,exclusive,on-demand
  94. default=not-allowed
  95. On a Wayland compositor that supports the wlr layer shell protocol, specify the focus policy for keyboard
  96. interactivity with the panel. Please refer to the wlr layer shell protocol documentation for more details.
  97. --exclusive-zone
  98. type=int
  99. default=-1
  100. On a Wayland compositor that supports the wlr layer shell protocol, request a given exclusive zone for the panel.
  101. Please refer to the wlr layer shell documentation for more details on the meaning of exclusive and its value.
  102. If :option:`--edge` is set to anything else than :code:`none`, this flag will not have any effect unless
  103. the flag :option:`--override-exclusive-zone` is also set.
  104. If :option:`--edge` is set to :code:`background`, this option has no effect.
  105. --override-exclusive-zone
  106. type=bool-set
  107. On a Wayland compositor that supports the wlr layer shell protocol, override the default exclusive zone.
  108. This has effect only if :option:`--edge` is set to :code:`top`, :code:`left`, :code:`bottom` or :code:`right`.
  109. --debug-rendering
  110. type=bool-set
  111. For internal debugging use.
  112. '''.format(appname=appname).format
  113. args = PanelCLIOptions()
  114. help_text = 'Use a command line program to draw a GPU accelerated panel on your X11 desktop'
  115. usage = 'program-to-run'
  116. def parse_panel_args(args: List[str]) -> Tuple[PanelCLIOptions, List[str]]:
  117. return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions)
  118. Strut = Tuple[int, int, int, int, int, int, int, int, int, int, int, int]
  119. def create_strut(
  120. win_id: int,
  121. left: int = 0, right: int = 0, top: int = 0, bottom: int = 0, left_start_y: int = 0, left_end_y: int = 0,
  122. right_start_y: int = 0, right_end_y: int = 0, top_start_x: int = 0, top_end_x: int = 0,
  123. bottom_start_x: int = 0, bottom_end_x: int = 0
  124. ) -> Strut:
  125. return left, right, top, bottom, left_start_y, left_end_y, right_start_y, right_end_y, top_start_x, top_end_x, bottom_start_x, bottom_end_x
  126. def create_top_strut(win_id: int, width: int, height: int) -> Strut:
  127. return create_strut(win_id, top=height, top_end_x=width)
  128. def create_bottom_strut(win_id: int, width: int, height: int) -> Strut:
  129. return create_strut(win_id, bottom=height, bottom_end_x=width)
  130. def create_left_strut(win_id: int, width: int, height: int) -> Strut:
  131. return create_strut(win_id, left=width, left_end_y=height)
  132. def create_right_strut(win_id: int, width: int, height: int) -> Strut:
  133. return create_strut(win_id, right=width, right_end_y=height)
  134. window_width = window_height = 0
  135. def setup_x11_window(win_id: int) -> None:
  136. if is_wayland():
  137. return
  138. func = globals()[f'create_{args.edge}_strut']
  139. strut = func(win_id, window_width, window_height)
  140. make_x11_window_a_dock_window(win_id, strut)
  141. def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
  142. def es(which: EdgeLiteral) -> float:
  143. return edge_spacing(which, opts)
  144. def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
  145. if not is_macos and not is_wayland():
  146. # Not sure what the deal with scaling on X11 is
  147. xscale = yscale = 1
  148. global window_width, window_height
  149. monitor_width, monitor_height = glfw_primary_monitor_size()
  150. if args.edge in {'top', 'bottom'}:
  151. spacing = es('top') + es('bottom')
  152. window_height = int(cell_height * args.lines / yscale + (dpi_y / 72) * spacing + 1)
  153. window_width = monitor_width
  154. elif args.edge == 'background':
  155. window_width, window_height = monitor_width, monitor_height
  156. else:
  157. spacing = es('left') + es('right')
  158. window_width = int(cell_width * args.lines / xscale + (dpi_x / 72) * spacing + 1)
  159. window_height = monitor_height
  160. return window_width, window_height
  161. return initial_window_size
  162. def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig:
  163. ltype = {'background': GLFW_LAYER_SHELL_BACKGROUND,
  164. 'bottom': GLFW_LAYER_SHELL_PANEL,
  165. 'top': GLFW_LAYER_SHELL_TOP,
  166. 'overlay': GLFW_LAYER_SHELL_OVERLAY}.get(opts.layer, GLFW_LAYER_SHELL_PANEL)
  167. ltype = GLFW_LAYER_SHELL_BACKGROUND if opts.edge == 'background' else ltype
  168. edge = {
  169. 'top': GLFW_EDGE_TOP, 'bottom': GLFW_EDGE_BOTTOM, 'left': GLFW_EDGE_LEFT, 'right': GLFW_EDGE_RIGHT, 'none': GLFW_EDGE_NONE
  170. }.get(opts.edge, GLFW_EDGE_TOP)
  171. focus_policy = {
  172. 'not-allowed': GLFW_FOCUS_NOT_ALLOWED, 'exclusive': GLFW_FOCUS_EXCLUSIVE, 'on-demand': GLFW_FOCUS_ON_DEMAND
  173. }.get(opts.focus_policy, GLFW_FOCUS_NOT_ALLOWED)
  174. return LayerShellConfig(type=ltype,
  175. edge=edge,
  176. x_size_in_cells=max(1, opts.columns),
  177. y_size_in_cells=max(1, opts.lines),
  178. requested_top_margin=max(0, opts.margin_top),
  179. requested_left_margin=max(0, opts.margin_left),
  180. requested_bottom_margin=max(0, opts.margin_bottom),
  181. requested_right_margin=max(0, opts.margin_right),
  182. focus_policy=focus_policy,
  183. requested_exclusive_zone=opts.exclusive_zone,
  184. override_exclusive_zone=opts.override_exclusive_zone,
  185. output_name=opts.output_name or '')
  186. def main(sys_args: List[str]) -> None:
  187. global args
  188. if is_macos:
  189. raise SystemExit('Currently the panel kitten is not supported on macOS')
  190. args, items = parse_panel_args(sys_args[1:])
  191. if not items:
  192. raise SystemExit('You must specify the program to run')
  193. sys.argv = ['kitty']
  194. if args.debug_rendering:
  195. sys.argv.append('--debug-rendering')
  196. for config in args.config:
  197. sys.argv.extend(('--config', config))
  198. sys.argv.extend(('--class', args.cls))
  199. if args.name:
  200. sys.argv.extend(('--name', args.name))
  201. for override in args.override:
  202. sys.argv.extend(('--override', override))
  203. sys.argv.append('--override=linux_display_server=auto')
  204. sys.argv.extend(items)
  205. from kitty.main import main as real_main
  206. from kitty.main import run_app
  207. run_app.cached_values_name = 'panel'
  208. run_app.layer_shell_config = layer_shell_config(args)
  209. run_app.first_window_callback = setup_x11_window
  210. run_app.initial_window_size_func = initial_window_size_func
  211. real_main()
  212. if __name__ == '__main__':
  213. main(sys.argv)
  214. elif __name__ == '__doc__':
  215. cd: dict = sys.cli_docs # type: ignore
  216. cd['usage'] = usage
  217. cd['options'] = OPTIONS
  218. cd['help_text'] = help_text
  219. cd['short_desc'] = help_text