gitpane 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,695 @@
1
+ """Use the Wayland compositor's activation protocol on Qt's own connection.
2
+
3
+ Mutter lets a started program bring its window to the front only when the token it received names a surface of the
4
+ program that asked for it together with an input serial still valid for that surface: the serial that gave that
5
+ surface the keyboard, a key still held, or the last button press of the pointer while the pointer is still on it. It
6
+ checks the token when the started program uses it, not when GitPane asks for it, so the token is asked for when the
7
+ user clicks and stays valid as long as GitPane keeps the keyboard.
8
+
9
+ GitPane also brings one of its own windows back to the front with such a token: it asks for a token named on the
10
+ surface that has the keyboard, then hands it at once to the compositor's activate request for the window to bring back.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import ctypes
16
+ import functools
17
+ import logging
18
+ import os
19
+ import select
20
+ import time
21
+ from dataclasses import dataclass
22
+ from typing import TYPE_CHECKING
23
+
24
+ from PySide6.QtGui import QGuiApplication, QNativeInterface
25
+
26
+ from gitpane.config import ACTIVATION_TOKEN_MAX_LENGTH, MILLISECONDS_PER_SECOND, WAYLAND_ACTIVATION_TOKEN_TIMEOUT_MS
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Callable
30
+
31
+ from PySide6.QtGui import QWindow
32
+
33
+ # Workaround: Qt asks for activation tokens itself but keeps that request private, and PySide6 gives no access to it.
34
+ # A program GitPane starts for a click only comes to the front under GNOME Wayland when it receives a token naming a
35
+ # GitPane surface and an input serial the compositor still accepts for it, and Mutter refuses a token asked for on a
36
+ # second connection, which has no surface with the focus. This binds xdg_activation_v1 again on Qt's connection, on an
37
+ # event queue of its own as Qt itself does for xdg-foreign, and waits a bounded time for each click. The serial is the
38
+ # one that gave GitPane the keyboard, read from a short-lived keyboard object of GitPane's own: Qt only keeps the serial
39
+ # of the last press, which names a context menu's surface once the menu has closed, and the pointer may have left the
40
+ # window by then, so that press alone would leave the started program behind GitPane.
41
+
42
+ WAYLAND_CLIENT_LIBRARY_NAME = "libwayland-client.so.0"
43
+ ACTIVATION_GLOBAL_NAME = "xdg_activation_v1"
44
+ TOKEN_INTERFACE_NAME = "xdg_activation_token_v1"
45
+ SURFACE_INTERFACE_NAME = "wl_surface"
46
+ SEAT_INTERFACE_NAME = "wl_seat"
47
+ ACTIVATION_VERSION = 1
48
+ # Interfaces of the core protocol, taken from the Wayland client library itself rather than described again here.
49
+ LIBRARY_INTERFACE_SYMBOL_BY_NAME = {
50
+ "wl_registry": "wl_registry_interface",
51
+ "wl_callback": "wl_callback_interface",
52
+ SURFACE_INTERFACE_NAME: "wl_surface_interface",
53
+ SEAT_INTERFACE_NAME: "wl_seat_interface",
54
+ "wl_keyboard": "wl_keyboard_interface",
55
+ }
56
+
57
+ # Request numbers of the core protocol (wayland.xml): the position of each request in its interface.
58
+ DISPLAY_SYNC_OPCODE = 0
59
+ DISPLAY_GET_REGISTRY_OPCODE = 1
60
+ REGISTRY_BIND_OPCODE = 0
61
+ SEAT_GET_KEYBOARD_OPCODE = 1
62
+ KEYBOARD_RELEASE_OPCODE = 0
63
+ # wl_keyboard.release exists from this version of the seat on; an older seat keeps its keyboard objects for good.
64
+ KEYBOARD_RELEASE_SINCE_VERSION = 3
65
+ # Request numbers of xdg-activation-v1: the position of each request in ACTIVATION_INTERFACE and TOKEN_INTERFACE below.
66
+ ACTIVATION_DESTROY_OPCODE = 0
67
+ ACTIVATION_GET_TOKEN_OPCODE = 1
68
+ ACTIVATION_ACTIVATE_OPCODE = 2
69
+ TOKEN_SET_SERIAL_OPCODE = 0
70
+ TOKEN_SET_APP_ID_OPCODE = 1
71
+ TOKEN_SET_SURFACE_OPCODE = 2
72
+ TOKEN_COMMIT_OPCODE = 3
73
+ TOKEN_DESTROY_OPCODE = 4
74
+ # Flags of wl_proxy_marshal_array_flags: WL_MARSHAL_FLAG_DESTROY destroys the proxy once its destructor request is sent.
75
+ NO_MARSHAL_FLAG = 0
76
+ MARSHAL_DESTROY_FLAG = 1
77
+ # Characters accepted in a token received from the compositor: visible ASCII only, so it can go into an environment
78
+ # variable and a D-Bus argument as it is.
79
+ FIRST_TOKEN_CHARACTER = "!"
80
+ LAST_TOKEN_CHARACTER = "~"
81
+ # A C array whose address is handed to the library holds at least one element, even for a message without arguments.
82
+ MINIMUM_ARRAY_LENGTH = 1
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ProtocolMessage:
87
+ """One request or event of a Wayland interface, as the protocol file writes it."""
88
+
89
+ name: str
90
+ signature: str
91
+ # Interface named by each argument of the signature, None for an argument that names none.
92
+ type_names: tuple[str | None, ...] = ()
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class ProtocolInterface:
97
+ """A Wayland interface the client library does not carry, described from its protocol file."""
98
+
99
+ name: str
100
+ version: int
101
+ requests: tuple[ProtocolMessage, ...]
102
+ events: tuple[ProtocolMessage, ...] = ()
103
+
104
+
105
+ # xdg-activation-v1, version 1, from the wayland-protocols file staging/xdg-activation/xdg-activation-v1.xml.
106
+ TOKEN_INTERFACE = ProtocolInterface(
107
+ name=TOKEN_INTERFACE_NAME,
108
+ version=ACTIVATION_VERSION,
109
+ requests=(
110
+ ProtocolMessage("set_serial", "uo", (None, SEAT_INTERFACE_NAME)),
111
+ # Never sent: the identifier names the program that will start, and GNOME Shell would show GitPane as starting.
112
+ ProtocolMessage("set_app_id", "s", (None,)),
113
+ ProtocolMessage("set_surface", "o", (SURFACE_INTERFACE_NAME,)),
114
+ ProtocolMessage("commit", ""),
115
+ ProtocolMessage("destroy", ""),
116
+ ),
117
+ events=(ProtocolMessage("done", "s", (None,)),),
118
+ )
119
+ ACTIVATION_INTERFACE = ProtocolInterface(
120
+ name=ACTIVATION_GLOBAL_NAME,
121
+ version=ACTIVATION_VERSION,
122
+ requests=(
123
+ ProtocolMessage("destroy", ""),
124
+ ProtocolMessage("get_activation_token", "n", (TOKEN_INTERFACE_NAME,)),
125
+ ProtocolMessage("activate", "so", (None, SURFACE_INTERFACE_NAME)),
126
+ ),
127
+ )
128
+ # Described in this order because the activation interface names the token interface.
129
+ OWN_PROTOCOL_INTERFACES = (TOKEN_INTERFACE, ACTIVATION_INTERFACE)
130
+
131
+
132
+ class WaylandMessage(ctypes.Structure):
133
+ """struct wl_message of the Wayland client library."""
134
+
135
+ _fields_ = [("name", ctypes.c_char_p), ("signature", ctypes.c_char_p), ("types", ctypes.POINTER(ctypes.c_void_p))]
136
+
137
+
138
+ class WaylandInterface(ctypes.Structure):
139
+ """struct wl_interface of the Wayland client library."""
140
+
141
+ _fields_ = [("name", ctypes.c_char_p), ("version", ctypes.c_int), ("method_count", ctypes.c_int), ("methods", ctypes.POINTER(WaylandMessage)),
142
+ ("event_count", ctypes.c_int), ("events", ctypes.POINTER(WaylandMessage))]
143
+
144
+
145
+ class WaylandArgument(ctypes.Union):
146
+ """union wl_argument of the Wayland client library, for the argument kinds this module sends."""
147
+
148
+ _fields_ = [("i", ctypes.c_int32), ("u", ctypes.c_uint32), ("s", ctypes.c_char_p), ("o", ctypes.c_void_p), ("n", ctypes.c_uint32)]
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class KeyboardFocus:
153
+ """The GitPane surface that has the keyboard, with the serial the compositor gave when it got it."""
154
+
155
+ surface: int
156
+ serial: int
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class _ActivationBinding:
161
+ """GitPane's own event queue on Qt's connection, with the activation global bound on it."""
162
+
163
+ queue: int
164
+ display_wrapper: int
165
+ activation: int
166
+
167
+
168
+ # Answers of the compositor, written by the event handlers below and read by the function waiting for them. Keyed by
169
+ # the address of the proxy the event came for; only the window's thread dispatches GitPane's queue.
170
+ _activation_global_name_by_registry: dict[int, int] = {}
171
+ _finished_callback_set: set[int] = set()
172
+ _token_by_proxy: dict[int, bytes] = {}
173
+ _keyboard_focus_by_proxy: dict[int, KeyboardFocus | None] = {}
174
+ # Activation binding of each Wayland connection, None when GitPane cannot have one there; see _activation_binding.
175
+ _activation_binding_by_display: dict[int, _ActivationBinding | None] = {}
176
+
177
+
178
+ def _on_registry_global(_data: int | None, registry: int | None, name: int, interface_name: bytes | None, _version: int) -> None:
179
+ """Note the number of the activation global when the compositor lists it."""
180
+ if registry is not None and interface_name == ACTIVATION_GLOBAL_NAME.encode():
181
+ _activation_global_name_by_registry[registry] = name
182
+
183
+
184
+ def _on_registry_global_remove(_data: int | None, registry: int | None, name: int) -> None:
185
+ """Forget the activation global if the compositor withdraws it."""
186
+ if registry is not None and _activation_global_name_by_registry.get(registry) == name:
187
+ del _activation_global_name_by_registry[registry]
188
+
189
+
190
+ def _on_callback_done(_data: int | None, callback: int | None, _callback_data: int) -> None:
191
+ """Note that the compositor answered everything sent before a sync request."""
192
+ if callback is not None:
193
+ _finished_callback_set.add(callback)
194
+
195
+
196
+ def _on_token_done(_data: int | None, token: int | None, value: bytes | None) -> None:
197
+ """Keep the token the compositor handed out."""
198
+ if token is not None:
199
+ _token_by_proxy[token] = value or b""
200
+
201
+
202
+ def _on_keyboard_keymap(_data: int | None, _keyboard: int | None, _format: int, descriptor: int, _size: int) -> None:
203
+ """Close the keymap file the compositor sends to every new keyboard object: only the focus is read here."""
204
+ try:
205
+ os.close(descriptor)
206
+ except OSError:
207
+ pass
208
+
209
+
210
+ def _on_keyboard_enter(_data: int | None, keyboard: int | None, serial: int, surface: int | None, _keys: int | None) -> None:
211
+ """Note which GitPane surface has the keyboard, with the serial that gave it."""
212
+ if keyboard is not None and surface:
213
+ _keyboard_focus_by_proxy[keyboard] = KeyboardFocus(surface, serial)
214
+
215
+
216
+ def _on_keyboard_leave(_data: int | None, keyboard: int | None, _serial: int, _surface: int | None) -> None:
217
+ """Forget the focus when the keyboard leaves GitPane."""
218
+ if keyboard is not None:
219
+ _keyboard_focus_by_proxy[keyboard] = None
220
+
221
+
222
+ def _on_keyboard_key(_data: int | None, _keyboard: int | None, _serial: int, _time: int, _key: int, _state: int) -> None:
223
+ """Ignore a key: Qt's own keyboard object handles the typing."""
224
+
225
+
226
+ def _on_keyboard_modifiers(_data: int | None, _keyboard: int | None, _serial: int, _depressed: int, _latched: int, _locked: int, _group: int) -> None:
227
+ """Ignore the modifiers: Qt's own keyboard object handles them."""
228
+
229
+
230
+ def _on_keyboard_repeat_info(_data: int | None, _keyboard: int | None, _rate: int, _delay: int) -> None:
231
+ """Ignore the key repeat settings: Qt's own keyboard object handles them."""
232
+
233
+
234
+ # C signatures of the event handlers: void handler(void *data, struct wl_proxy *proxy, <event arguments>).
235
+ REGISTRY_GLOBAL_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32)
236
+ REGISTRY_GLOBAL_REMOVE_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32)
237
+ CALLBACK_DONE_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32)
238
+ TOKEN_DONE_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p)
239
+ KEYBOARD_KEYMAP_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_int32, ctypes.c_uint32)
240
+ KEYBOARD_ENTER_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p)
241
+ KEYBOARD_LEAVE_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p)
242
+ KEYBOARD_KEY_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32)
243
+ KEYBOARD_MODIFIERS_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32)
244
+ KEYBOARD_REPEAT_INFO_HANDLER_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int32, ctypes.c_int32)
245
+
246
+ # Kept at module level: the client library calls these pointers for as long as the objects they listen to exist,
247
+ # and the registry lives as long as the process.
248
+ REGISTRY_HANDLERS = (REGISTRY_GLOBAL_HANDLER_TYPE(_on_registry_global), REGISTRY_GLOBAL_REMOVE_HANDLER_TYPE(_on_registry_global_remove))
249
+ CALLBACK_HANDLERS = (CALLBACK_DONE_HANDLER_TYPE(_on_callback_done),)
250
+ TOKEN_HANDLERS = (TOKEN_DONE_HANDLER_TYPE(_on_token_done),)
251
+ # In the order of the wl_keyboard events: keymap, enter, leave, key, modifiers, repeat_info.
252
+ KEYBOARD_HANDLERS = (KEYBOARD_KEYMAP_HANDLER_TYPE(_on_keyboard_keymap), KEYBOARD_ENTER_HANDLER_TYPE(_on_keyboard_enter), KEYBOARD_LEAVE_HANDLER_TYPE(_on_keyboard_leave),
253
+ KEYBOARD_KEY_HANDLER_TYPE(_on_keyboard_key), KEYBOARD_MODIFIERS_HANDLER_TYPE(_on_keyboard_modifiers),
254
+ KEYBOARD_REPEAT_INFO_HANDLER_TYPE(_on_keyboard_repeat_info))
255
+
256
+
257
+ def _listener(handlers: tuple[object, ...]) -> ctypes.Array[ctypes.c_void_p]:
258
+ """Build the C table of event handlers the client library calls, in event order."""
259
+ pointers: list[int | None] = []
260
+ for handler in handlers:
261
+ pointers.append(ctypes.cast(handler, ctypes.c_void_p).value)
262
+ return (ctypes.c_void_p * len(pointers))(*pointers)
263
+
264
+
265
+ REGISTRY_LISTENER = _listener(REGISTRY_HANDLERS)
266
+ CALLBACK_LISTENER = _listener(CALLBACK_HANDLERS)
267
+ TOKEN_LISTENER = _listener(TOKEN_HANDLERS)
268
+ KEYBOARD_LISTENER = _listener(KEYBOARD_HANDLERS)
269
+
270
+
271
+ @functools.cache
272
+ def _wayland_library() -> ctypes.CDLL:
273
+ """Use the Wayland client library Qt already loaded, and declare the calls this module makes.
274
+
275
+ RTLD_NOLOAD never loads a second copy beside Qt's: outside a Wayland session the library is not loaded and this
276
+ raises OSError instead.
277
+ """
278
+ library = ctypes.CDLL(WAYLAND_CLIENT_LIBRARY_NAME, mode=os.RTLD_NOLOAD | os.RTLD_LAZY)
279
+ pointer = ctypes.c_void_p
280
+ library.wl_display_create_queue.argtypes = [pointer]
281
+ library.wl_display_create_queue.restype = pointer
282
+ library.wl_event_queue_destroy.argtypes = [pointer]
283
+ library.wl_event_queue_destroy.restype = None
284
+ library.wl_proxy_create_wrapper.argtypes = [pointer]
285
+ library.wl_proxy_create_wrapper.restype = pointer
286
+ library.wl_proxy_wrapper_destroy.argtypes = [pointer]
287
+ library.wl_proxy_wrapper_destroy.restype = None
288
+ library.wl_proxy_set_queue.argtypes = [pointer, pointer]
289
+ library.wl_proxy_set_queue.restype = None
290
+ library.wl_proxy_marshal_array_flags.argtypes = [pointer, ctypes.c_uint32, pointer, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(WaylandArgument)]
291
+ library.wl_proxy_marshal_array_flags.restype = pointer
292
+ library.wl_proxy_add_listener.argtypes = [pointer, pointer, pointer]
293
+ library.wl_proxy_add_listener.restype = ctypes.c_int
294
+ library.wl_proxy_get_version.argtypes = [pointer]
295
+ library.wl_proxy_get_version.restype = ctypes.c_uint32
296
+ library.wl_proxy_destroy.argtypes = [pointer]
297
+ library.wl_proxy_destroy.restype = None
298
+ library.wl_display_prepare_read_queue.argtypes = [pointer, pointer]
299
+ library.wl_display_prepare_read_queue.restype = ctypes.c_int
300
+ library.wl_display_read_events.argtypes = [pointer]
301
+ library.wl_display_read_events.restype = ctypes.c_int
302
+ library.wl_display_cancel_read.argtypes = [pointer]
303
+ library.wl_display_cancel_read.restype = None
304
+ library.wl_display_dispatch_queue_pending.argtypes = [pointer, pointer]
305
+ library.wl_display_dispatch_queue_pending.restype = ctypes.c_int
306
+ library.wl_display_flush.argtypes = [pointer]
307
+ library.wl_display_flush.restype = ctypes.c_int
308
+ library.wl_display_get_fd.argtypes = [pointer]
309
+ library.wl_display_get_fd.restype = ctypes.c_int
310
+ return library
311
+
312
+
313
+ @dataclass(frozen=True)
314
+ class _ProtocolStructures:
315
+ """The C descriptions of the interfaces this module uses, kept alive as long as the library may read them."""
316
+
317
+ address_by_name: dict[str, int]
318
+ kept_objects: tuple[object, ...]
319
+
320
+
321
+ @functools.cache
322
+ def _protocol_structures() -> _ProtocolStructures:
323
+ """Build the C descriptions of xdg-activation-v1 from ProtocolInterface, pointing at the library's own core interfaces."""
324
+ library = _wayland_library()
325
+ address_by_name: dict[str, int] = {}
326
+ kept_objects: list[object] = []
327
+ for name, symbol in LIBRARY_INTERFACE_SYMBOL_BY_NAME.items():
328
+ address_by_name[name] = ctypes.addressof(WaylandInterface.in_dll(library, symbol))
329
+ for description in OWN_PROTOCOL_INTERFACES:
330
+ interface = WaylandInterface()
331
+ kept_objects.append(interface)
332
+ address_by_name[description.name] = ctypes.addressof(interface)
333
+ interface.name = description.name.encode()
334
+ interface.version = description.version
335
+ interface.method_count = len(description.requests)
336
+ interface.methods = _message_array(description.requests, address_by_name, kept_objects)
337
+ interface.event_count = len(description.events)
338
+ interface.events = _message_array(description.events, address_by_name, kept_objects)
339
+ return _ProtocolStructures(address_by_name, tuple(kept_objects))
340
+
341
+
342
+ def _message_array(descriptions: tuple[ProtocolMessage, ...], address_by_name: dict[str, int], kept_objects: list[object]) -> ctypes._Pointer[WaylandMessage] | None:
343
+ """Build the C array of messages of one interface, each with the interfaces its arguments name."""
344
+ if not descriptions:
345
+ return None
346
+ messages = (WaylandMessage * len(descriptions))()
347
+ kept_objects.append(messages)
348
+ for index, description in enumerate(descriptions):
349
+ types = (ctypes.c_void_p * max(len(description.type_names), MINIMUM_ARRAY_LENGTH))()
350
+ kept_objects.append(types)
351
+ for type_index, type_name in enumerate(description.type_names):
352
+ types[type_index] = address_by_name[type_name] if type_name is not None else None
353
+ name = description.name.encode()
354
+ signature = description.signature.encode()
355
+ kept_objects.extend((name, signature))
356
+ messages[index].name = name
357
+ messages[index].signature = signature
358
+ messages[index].types = ctypes.cast(types, ctypes.POINTER(ctypes.c_void_p))
359
+ return ctypes.cast(messages, ctypes.POINTER(WaylandMessage))
360
+
361
+
362
+ def _marshal(library: ctypes.CDLL, proxy: int, opcode: int, arguments: list[WaylandArgument], *, interface: int | None = None, version: int | None = None,
363
+ flags: int = NO_MARSHAL_FLAG) -> int | None:
364
+ """Send one request on a proxy, the way the generated protocol functions do, and return the object it creates, if any."""
365
+ argument_array = (WaylandArgument * max(len(arguments), MINIMUM_ARRAY_LENGTH))(*arguments)
366
+ request_version = version if version is not None else library.wl_proxy_get_version(proxy)
367
+ return library.wl_proxy_marshal_array_flags(proxy, opcode, interface, request_version, flags, argument_array)
368
+
369
+
370
+ def _new_id_argument() -> WaylandArgument:
371
+ """The placeholder the client library replaces with the number of the object a request creates."""
372
+ argument = WaylandArgument()
373
+ argument.o = None
374
+ return argument
375
+
376
+
377
+ def _dispatch_until(library: ctypes.CDLL, display: int, queue: int, is_done: Callable[[], bool], deadline: float) -> bool:
378
+ """Read the compositor's answers for GitPane's queue until is_done, never waiting past deadline (time.monotonic()).
379
+
380
+ Reads cooperate with Qt's own reading thread through the client library's prepare and read protocol: whichever
381
+ reader comes last reads the socket and hands each event to its queue.
382
+ """
383
+ descriptor = library.wl_display_get_fd(display)
384
+ while not is_done():
385
+ if library.wl_display_prepare_read_queue(display, queue) != 0:
386
+ if library.wl_display_dispatch_queue_pending(display, queue) < 0:
387
+ raise OSError("The Wayland connection failed.")
388
+ continue
389
+ library.wl_display_flush(display)
390
+ remaining = deadline - time.monotonic()
391
+ readable_descriptors: list[int] = []
392
+ if remaining > 0:
393
+ readable_descriptors, _writable_descriptors, _failed_descriptors = select.select([descriptor], [], [], remaining)
394
+ if not readable_descriptors:
395
+ library.wl_display_cancel_read(display)
396
+ return False
397
+ if library.wl_display_read_events(display) < 0:
398
+ raise OSError("The Wayland connection failed.")
399
+ if library.wl_display_dispatch_queue_pending(display, queue) < 0:
400
+ raise OSError("The Wayland connection failed.")
401
+ return True
402
+
403
+
404
+ def _roundtrip(library: ctypes.CDLL, display: int, binding_queue: int, display_wrapper: int, deadline: float) -> None:
405
+ """Wait until the compositor has answered every request sent so far on the connection, or raise TimeoutError."""
406
+ structures = _protocol_structures()
407
+ callback = _marshal(library, display_wrapper, DISPLAY_SYNC_OPCODE, [_new_id_argument()], interface=structures.address_by_name["wl_callback"])
408
+ if not callback:
409
+ raise OSError("The compositor could not be asked for an answer.")
410
+ library.wl_proxy_add_listener(callback, CALLBACK_LISTENER, None)
411
+ try:
412
+ if not _dispatch_until(library, display, binding_queue, lambda: callback in _finished_callback_set, deadline):
413
+ raise TimeoutError("The compositor did not answer in time.")
414
+ finally:
415
+ _finished_callback_set.discard(callback)
416
+ library.wl_proxy_destroy(callback)
417
+
418
+
419
+ def _activation_binding(display: int, deadline: float) -> _ActivationBinding | None:
420
+ """Bind the activation global once per connection, on an event queue of GitPane's own; None when GitPane cannot.
421
+
422
+ Kept for the whole process, a failure included: a registry has no request that destroys it, so binding again for
423
+ every click would leave one more object on the compositor each time. A compositor that has no activation global, or
424
+ that does not answer before deadline the first time, is therefore not asked again until GitPane restarts; the
425
+ started programs then open as they do without a token.
426
+ """
427
+ if display in _activation_binding_by_display:
428
+ return _activation_binding_by_display[display]
429
+ try:
430
+ binding = _bind_activation(display, deadline)
431
+ except OSError:
432
+ _activation_binding_by_display[display] = None
433
+ raise
434
+ _activation_binding_by_display[display] = binding
435
+ return binding
436
+
437
+
438
+ def _bind_activation(display: int, deadline: float) -> _ActivationBinding | None:
439
+ """Create GitPane's event queue on the connection and bind the activation global on it; None when the compositor has none.
440
+
441
+ What it created is freed again on GitPane's side when it returns None or fails, so a binding that is not kept
442
+ leaves neither an event queue nor a registry listening on the connection.
443
+ """
444
+ library = _wayland_library()
445
+ structures = _protocol_structures()
446
+ binding_queue = library.wl_display_create_queue(display)
447
+ if not binding_queue:
448
+ raise OSError("No event queue could be created on the Wayland connection.")
449
+ display_wrapper = None
450
+ registry = None
451
+ try:
452
+ display_wrapper = library.wl_proxy_create_wrapper(display)
453
+ if not display_wrapper:
454
+ raise OSError("The Wayland connection could not be wrapped.")
455
+ library.wl_proxy_set_queue(display_wrapper, binding_queue)
456
+ registry = _marshal(library, display_wrapper, DISPLAY_GET_REGISTRY_OPCODE, [_new_id_argument()], interface=structures.address_by_name["wl_registry"])
457
+ if not registry:
458
+ raise OSError("The compositor's registry could not be read.")
459
+ library.wl_proxy_add_listener(registry, REGISTRY_LISTENER, None)
460
+ _roundtrip(library, display, binding_queue, display_wrapper, deadline)
461
+ global_name = _activation_global_name_by_registry.get(registry)
462
+ if global_name is not None:
463
+ name_argument = WaylandArgument()
464
+ name_argument.u = global_name
465
+ interface_argument = WaylandArgument()
466
+ interface_argument.s = ACTIVATION_GLOBAL_NAME.encode()
467
+ version_argument = WaylandArgument()
468
+ version_argument.u = ACTIVATION_VERSION
469
+ activation = _marshal(library, registry, REGISTRY_BIND_OPCODE, [name_argument, interface_argument, version_argument, _new_id_argument()],
470
+ interface=structures.address_by_name[ACTIVATION_GLOBAL_NAME], version=ACTIVATION_VERSION)
471
+ if not activation:
472
+ raise OSError("The activation protocol could not be bound.")
473
+ return _ActivationBinding(binding_queue, display_wrapper, activation)
474
+ except BaseException:
475
+ _release_binding_setup(library, binding_queue, display_wrapper, registry)
476
+ raise
477
+ _release_binding_setup(library, binding_queue, display_wrapper, registry)
478
+ return None
479
+
480
+
481
+ def _release_binding_setup(library: ctypes.CDLL, binding_queue: int, display_wrapper: int | None, registry: int | None) -> None:
482
+ """Free, on GitPane's side, the registry, display wrapper and event queue of a binding that is not kept."""
483
+ if registry:
484
+ _activation_global_name_by_registry.pop(registry, None)
485
+ # A registry has no request that destroys it: this frees GitPane's proxy only, the compositor keeps its object.
486
+ library.wl_proxy_destroy(registry)
487
+ if display_wrapper:
488
+ library.wl_proxy_wrapper_destroy(display_wrapper)
489
+ library.wl_event_queue_destroy(binding_queue)
490
+
491
+
492
+ def activation_deadline() -> float:
493
+ """Return the monotonic time by which one click must have its token, binding, keyboard focus and token included."""
494
+ return time.monotonic() + WAYLAND_ACTIVATION_TOKEN_TIMEOUT_MS / MILLISECONDS_PER_SECOND
495
+
496
+
497
+ def read_keyboard_focus(display: int, seat: int, deadline: float) -> KeyboardFocus | None:
498
+ """Return the GitPane surface that has the keyboard of seat, with the serial that gave it, or None when GitPane has not.
499
+
500
+ A new keyboard object receives at once the current focus and its serial when the focus is on one of GitPane's
501
+ surfaces; it is released as soon as that answer is read. Call only with a seat that has a keyboard: the protocol
502
+ forbids asking a keyboard of a seat that never had one.
503
+ """
504
+ library = _wayland_library()
505
+ binding = _activation_binding(display, deadline)
506
+ if binding is None:
507
+ return None
508
+ seat_version = library.wl_proxy_get_version(seat)
509
+ keyboard_interface = WaylandInterface.from_address(_protocol_structures().address_by_name["wl_keyboard"])
510
+ if seat_version < KEYBOARD_RELEASE_SINCE_VERSION or keyboard_interface.event_count > len(KEYBOARD_HANDLERS):
511
+ return None
512
+ seat_wrapper = library.wl_proxy_create_wrapper(seat)
513
+ if not seat_wrapper:
514
+ raise OSError("The seat could not be wrapped.")
515
+ try:
516
+ library.wl_proxy_set_queue(seat_wrapper, binding.queue)
517
+ keyboard = _marshal(library, seat_wrapper, SEAT_GET_KEYBOARD_OPCODE, [_new_id_argument()], interface=ctypes.addressof(keyboard_interface), version=seat_version)
518
+ finally:
519
+ library.wl_proxy_wrapper_destroy(seat_wrapper)
520
+ if not keyboard:
521
+ raise OSError("No keyboard object could be created.")
522
+ _keyboard_focus_by_proxy[keyboard] = None
523
+ library.wl_proxy_add_listener(keyboard, KEYBOARD_LISTENER, None)
524
+ try:
525
+ _roundtrip(library, display, binding.queue, binding.display_wrapper, deadline)
526
+ return _keyboard_focus_by_proxy.get(keyboard)
527
+ finally:
528
+ _keyboard_focus_by_proxy.pop(keyboard, None)
529
+ _marshal(library, keyboard, KEYBOARD_RELEASE_OPCODE, [], flags=MARSHAL_DESTROY_FLAG)
530
+
531
+
532
+ def request_activation_token(display: int, seat: int, serial: int, surface: int, deadline: float) -> str:
533
+ """Ask the compositor for a token naming surface and serial on seat, and return it once checked, or an empty text.
534
+
535
+ Never names the program that will start: GNOME Shell would take GitPane for it. Each request starts the
536
+ compositor's startup notice (a busy pointer outside the windows) until a program uses the token, for 15 s at most
537
+ under Mutter, so it is only asked for when a program is about to start.
538
+ """
539
+ library = _wayland_library()
540
+ binding = _activation_binding(display, deadline)
541
+ if binding is None:
542
+ return ""
543
+ structures = _protocol_structures()
544
+ token = _marshal(library, binding.activation, ACTIVATION_GET_TOKEN_OPCODE, [_new_id_argument()], interface=structures.address_by_name[TOKEN_INTERFACE_NAME])
545
+ if not token:
546
+ raise OSError("No activation token object could be created.")
547
+ library.wl_proxy_add_listener(token, TOKEN_LISTENER, None)
548
+ try:
549
+ if serial and seat:
550
+ serial_argument = WaylandArgument()
551
+ serial_argument.u = serial
552
+ seat_argument = WaylandArgument()
553
+ seat_argument.o = seat
554
+ _marshal(library, token, TOKEN_SET_SERIAL_OPCODE, [serial_argument, seat_argument])
555
+ if surface:
556
+ surface_argument = WaylandArgument()
557
+ surface_argument.o = surface
558
+ _marshal(library, token, TOKEN_SET_SURFACE_OPCODE, [surface_argument])
559
+ _marshal(library, token, TOKEN_COMMIT_OPCODE, [])
560
+ if not _dispatch_until(library, display, binding.queue, lambda: token in _token_by_proxy, deadline):
561
+ raise TimeoutError("The compositor did not hand out a token in time.")
562
+ return _checked_token(_token_by_proxy[token])
563
+ finally:
564
+ _token_by_proxy.pop(token, None)
565
+ _marshal(library, token, TOKEN_DESTROY_OPCODE, [], flags=MARSHAL_DESTROY_FLAG)
566
+
567
+
568
+ def _checked_token(raw: bytes) -> str:
569
+ """Return a token received from the compositor when it is visible ASCII of a bounded length, an empty text otherwise.
570
+
571
+ The token goes into a started program's environment and into a D-Bus argument: a control character, a
572
+ non-ASCII character or an oversized value is refused rather than passed on.
573
+ """
574
+ if not raw or len(raw) > ACTIVATION_TOKEN_MAX_LENGTH:
575
+ return ""
576
+ try:
577
+ text = raw.decode("ascii")
578
+ except UnicodeDecodeError:
579
+ return ""
580
+ for character in text:
581
+ if not FIRST_TOKEN_CHARACTER <= character <= LAST_TOKEN_CHARACTER:
582
+ return ""
583
+ return text
584
+
585
+
586
+ def wayland_native_interface_type() -> type | None:
587
+ """Return Qt's Wayland native interface class, or None where PySide6 has none.
588
+
589
+ PySide6 only exposes QNativeInterface.QWaylandApplication when its Qt was built with Wayland support: the Windows
590
+ wheel has only QNativeInterface.QWindowsScreen, and reading the Wayland name there raises AttributeError.
591
+ """
592
+ return getattr(QNativeInterface, "QWaylandApplication", None)
593
+
594
+
595
+ def wayland_activation_token() -> str:
596
+ """Return a token for the user's last action in GitPane, or an empty text when there is nothing to name.
597
+
598
+ Read on the window's thread when the user clicks, before any worker starts. The token names the GitPane surface
599
+ that has the keyboard with the serial that gave it the keyboard, which the compositor accepts for as long as GitPane
600
+ keeps the keyboard, wherever the pointer went, a context menu that reached past the window included. Without the
601
+ keyboard, it names the focused window with the last press Qt saw. The value is never written to the log.
602
+ """
603
+ native = wayland_native_interface()
604
+ if native is None:
605
+ return ""
606
+ deadline = activation_deadline()
607
+ try:
608
+ display = native.display()
609
+ seat = native.seat()
610
+ if seat and native.keyboard():
611
+ focus = read_keyboard_focus(display, seat, deadline)
612
+ if focus is not None:
613
+ return _logged_token(request_activation_token(display, seat, focus.serial, focus.surface, deadline))
614
+ window = QGuiApplication.focusWindow()
615
+ serial = native.lastInputSerial()
616
+ last_input_seat = native.lastInputSeat()
617
+ if window is None or not serial or not last_input_seat:
618
+ logging.getLogger(__name__).debug("no activation token: no GitPane window has the keyboard or a press")
619
+ return ""
620
+ # Under Qt's Wayland plugin, a window's identifier is its wl_surface.
621
+ return _logged_token(request_activation_token(display, last_input_seat, serial, int(window.winId()), deadline))
622
+ except (OSError, AttributeError, ctypes.ArgumentError, ValueError) as error:
623
+ logging.getLogger(__name__).debug("no activation token: %s", type(error).__name__)
624
+ return ""
625
+
626
+
627
+ def _logged_token(token: str) -> str:
628
+ """Log whether a token came, never its value, and return it."""
629
+ logging.getLogger(__name__).debug("activation token received" if token else "no activation token: the compositor's answer was refused")
630
+ return token
631
+
632
+
633
+ def wayland_native_interface() -> object | None:
634
+ """Return the native interface of Qt's own Wayland connection, or None without an application or outside Qt's Wayland platform.
635
+
636
+ The one place that decides whether GitPane runs on Qt's Wayland connection, for the token request and for the
637
+ activation alike.
638
+ """
639
+ application = QGuiApplication.instance()
640
+ if application is None:
641
+ return None
642
+ native = application.nativeInterface()
643
+ wayland_interface_type = wayland_native_interface_type()
644
+ if wayland_interface_type is None or not isinstance(native, wayland_interface_type):
645
+ return None
646
+ return native
647
+
648
+
649
+ def _wayland_display() -> int | None:
650
+ """Return the address of Qt's own Wayland connection, or None outside Qt's Wayland platform."""
651
+ native = wayland_native_interface()
652
+ if native is None:
653
+ return None
654
+ return native.display()
655
+
656
+
657
+ def activate_surface(token: str, surface: int) -> bool:
658
+ """Ask the compositor to activate a GitPane surface with an activation token, and tell whether the request was sent.
659
+
660
+ Sent on GitPane's own binding of the activation protocol, then flushed at once as the token request is, so the
661
+ compositor reads it without waiting for Qt's next write. Nothing is sent without a token, a surface or a binding;
662
+ the compositor decides alone whether the token lets the window come to the front.
663
+ """
664
+ if not token or not surface:
665
+ return False
666
+ try:
667
+ display = _wayland_display()
668
+ if not display:
669
+ return False
670
+ binding = _activation_binding(display, activation_deadline())
671
+ if binding is None:
672
+ return False
673
+ library = _wayland_library()
674
+ token_argument = WaylandArgument()
675
+ token_argument.s = token.encode("ascii")
676
+ surface_argument = WaylandArgument()
677
+ surface_argument.o = surface
678
+ _marshal(library, binding.activation, ACTIVATION_ACTIVATE_OPCODE, [token_argument, surface_argument])
679
+ library.wl_display_flush(display)
680
+ except (OSError, AttributeError, ctypes.ArgumentError, ValueError) as error:
681
+ logging.getLogger(__name__).debug("window not activated: %s", type(error).__name__)
682
+ return False
683
+ return True
684
+
685
+
686
+ def activate_window_with_keyboard_token(window: QWindow) -> bool:
687
+ """Bring a GitPane window to the front with a token named on the GitPane surface that has the keyboard, and tell whether it was asked.
688
+
689
+ The token is used at once on GitPane's own surface and never written to the log nor handed to another program.
690
+ """
691
+ token = wayland_activation_token()
692
+ if not token:
693
+ return False
694
+ # Under Qt's Wayland plugin, a window's identifier is its wl_surface.
695
+ return activate_surface(token, int(window.winId()))