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,232 @@
1
+ """Present an editable SSH candidate separately from accepted tool preferences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import Qt, Signal
9
+ from PySide6.QtWidgets import QDialog, QDialogButtonBox, QFileDialog, QGridLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QScrollArea, QVBoxLayout, QWidget
10
+
11
+ from gitpane.account.ssh import SSH_UNAVAILABLE_TEXT, SshConfiguration, SshVerification
12
+ from gitpane.account.ssh_prompt import SshQuestion
13
+ from gitpane.config import ERROR_HEIGHT, ERROR_WIDTH, SETTING_MIN_HEIGHT, SETTING_MIN_WIDTH, fit_window
14
+ from gitpane.text.typed_path import session_typed_path, typed_path_value
15
+ from gitpane.ui.widget.result_label import ResultLabel
16
+ from gitpane.ui.window_control.button_focus import set_button_enabled
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable
20
+
21
+
22
+ class SshWidget(QGroupBox):
23
+ """Show exact paths and asynchronous verification without saving from the widget."""
24
+
25
+ draft_changed = Signal()
26
+ verification_requested = Signal()
27
+
28
+ def __init__(self, configuration: SshConfiguration, effective_filename: str = "", effective_source: str = "", parent: QWidget | None = None, *,
29
+ typed_path: Callable[[str], str] = session_typed_path) -> None:
30
+ """Keep browse controls aligned and long values scrollable in Settings.
31
+
32
+ configuration is the saved setting, the only value the form can save back; the client operations really use
33
+ and where it comes from are shown apart, so a detected client never lands in the editable field. ``typed_path``
34
+ reads ``~`` in both paths where that is the usage; tests hand their own system and home folder.
35
+ """
36
+ super().__init__("SSH client", parent)
37
+ self.typed_path = typed_path
38
+ self.request_id = ""
39
+ self.verified_configuration: SshConfiguration | None = None
40
+ self.effective_filename = ""
41
+ layout = QGridLayout(self)
42
+ label_column, value_column, browse_column = 0, 1, 2
43
+ executable_row, utility_row, source_row, effective_row, action_row, status_row = range(6)
44
+ self.path_edit = QLineEdit(configuration.ssh_filename)
45
+ self.utility_edit = QLineEdit(configuration.utility_dir if os.name == "nt" else "")
46
+ self.browse_button = QPushButton("Browse…")
47
+ self.utility_browse_button = QPushButton("Browse…")
48
+ for row, label_text, edit, button in ((executable_row, "Executable", self.path_edit, self.browse_button),
49
+ (utility_row, "Git utilities directory (Windows)", self.utility_edit, self.utility_browse_button)):
50
+ label = QLabel(label_text)
51
+ label.setBuddy(edit)
52
+ edit.setAccessibleName(label_text)
53
+ button.setAccessibleName("Browse for " + label_text)
54
+ layout.addWidget(label, row, label_column)
55
+ layout.addWidget(edit, row, value_column)
56
+ layout.addWidget(button, row, browse_column)
57
+ edit.textChanged.connect(self._changed)
58
+ self.utility_edit.setEnabled(os.name == "nt")
59
+ self.utility_browse_button.setEnabled(os.name == "nt")
60
+ self.utility_edit.setPlaceholderText("Optional" if os.name == "nt" else "Not applicable on this operating system")
61
+ self.source_label = QLabel()
62
+ self.effective_edit = QLineEdit()
63
+ self.effective_edit.setReadOnly(True)
64
+ self.effective_edit.setAccessibleName("Effective SSH executable")
65
+ effective_label = QLabel("Effective value")
66
+ effective_label.setBuddy(self.effective_edit)
67
+ layout.addWidget(self.source_label, source_row, label_column, 1, 3)
68
+ layout.addWidget(effective_label, effective_row, label_column)
69
+ layout.addWidget(self.effective_edit, effective_row, value_column, 1, 2)
70
+ self.verify_button = QPushButton("Verify SSH client")
71
+ self.verify_button.clicked.connect(self.verification_requested)
72
+ layout.addWidget(self.verify_button, action_row, value_column)
73
+ self.status_label = ResultLabel("")
74
+ layout.addWidget(self.status_label, status_row, label_column, 1, 3)
75
+ layout.setColumnStretch(value_column, 1)
76
+ self.browse_button.clicked.connect(self._browse)
77
+ self.utility_browse_button.clicked.connect(self._browse_utility)
78
+ self.show_effective(effective_filename, effective_source)
79
+
80
+ def show_effective(self, filename: str, source: str) -> None:
81
+ """Show the client operations use and where it comes from; ask for one only when there is none.
82
+
83
+ A verification result already shown stays: it still describes the saved executable. The client is kept so that
84
+ Verify can check it while Executable is empty.
85
+ """
86
+ self.effective_filename = filename
87
+ self._update_verify_button()
88
+ self.source_label.setText("Source: " + source)
89
+ self.effective_edit.setText(filename or "No SSH client configured")
90
+ if not filename:
91
+ self.status_label.setText(SSH_UNAVAILABLE_TEXT)
92
+ elif self.status_label.text() == SSH_UNAVAILABLE_TEXT:
93
+ self.status_label.clear()
94
+
95
+ def configuration(self) -> SshConfiguration:
96
+ """Capture the draft without the blanks around each typed path, its inside untouched, and with ``~`` read as the home folder.
97
+
98
+ Verify and Save both read this, so a client pasted with a space around it is checked and saved as the path
99
+ itself, and a check made on it still counts for Save. ``~`` is read where that is the usage, so the saved value
100
+ is the full path. The Git utilities directory is only used on Windows, where ``~`` stays as typed; it follows the
101
+ same reading so that every path field has the same rule.
102
+ """
103
+ return SshConfiguration(typed_path_value(self.path_edit.text(), self.typed_path), typed_path_value(self.utility_edit.text(), self.typed_path))
104
+
105
+ def verification_configuration(self) -> SshConfiguration:
106
+ """Name what Verify checks: the typed Executable when there is one, otherwise the client operations use now.
107
+
108
+ A typed or saved choice wins over the detected client, as it does for operations; checking the detected client
109
+ never puts it in the editable field, so Save still writes nothing for it.
110
+ """
111
+ draft = self.configuration()
112
+ if draft.ssh_filename:
113
+ return draft
114
+ return SshConfiguration(self.effective_filename, draft.utility_dir)
115
+
116
+ def set_verifying(self) -> None:
117
+ """Keep edits possible while showing that the captured candidate is being checked."""
118
+ set_button_enabled(self.verify_button, False)
119
+ self.status_label.setText("Verifying SSH client…")
120
+
121
+ def show_result(self, result: SshVerification) -> None:
122
+ """Publish a version only when it still describes what Verify would check now.
123
+
124
+ Only a check of the draft itself counts as verifying it for Save: a check of the detected client leaves the
125
+ draft unverified, so it can never make Save write a path.
126
+ """
127
+ self.request_id = ""
128
+ self._update_verify_button()
129
+ if result.configuration != self.verification_configuration():
130
+ self.status_label.show_failure("SSH path changed during verification. Verify the current executable.")
131
+ return
132
+ self.verified_configuration = result.configuration if result.succeeded and result.configuration == self.configuration() else None
133
+ if result.succeeded:
134
+ self.status_label.setText(result.version)
135
+ else:
136
+ self.status_label.show_failure(result.cause)
137
+
138
+ def _changed(self) -> None:
139
+ self.verified_configuration = None
140
+ self._update_verify_button()
141
+ self.status_label.clear()
142
+ self.draft_changed.emit()
143
+
144
+ def _update_verify_button(self) -> None:
145
+ """Offer Verify when there is a client to check and no check is already running."""
146
+ set_button_enabled(self.verify_button, bool(self.verification_configuration().ssh_filename) and not self.request_id)
147
+
148
+ def _browse(self) -> None:
149
+ """Choose the SSH client without changing the draft when cancelled.
150
+
151
+ The chooser starts from the typed path without the blanks around it and with ``~`` read, the value every other
152
+ check of the field uses.
153
+ """
154
+ start_filename = typed_path_value(self.path_edit.text(), self.typed_path)
155
+ filename, _ = QFileDialog.getOpenFileName(self, "SSH executable", start_filename)
156
+ if filename:
157
+ self.path_edit.setText(filename)
158
+ self.path_edit.setFocus()
159
+
160
+ def _browse_utility(self) -> None:
161
+ """Choose the Git utilities directory without changing the draft when cancelled.
162
+
163
+ The chooser starts from the typed directory without the blanks around it and with ``~`` read, the value every
164
+ other check of the field uses.
165
+ """
166
+ start_dir = typed_path_value(self.utility_edit.text(), self.typed_path)
167
+ utility_dir = QFileDialog.getExistingDirectory(self, "Git utilities directory (Windows)", start_dir)
168
+ if utility_dir:
169
+ self.utility_edit.setText(utility_dir)
170
+ self.utility_edit.setFocus()
171
+
172
+
173
+ class SshPromptDialog(QDialog):
174
+ """Ask explicitly about a host key or accept a masked private passphrase asynchronously."""
175
+
176
+ def __init__(self, question: SshQuestion, parent: QWidget | None = None) -> None:
177
+ """Show captured operation text as plain text and never retain the accepted secret."""
178
+ super().__init__(parent)
179
+ self.question = question
180
+ self.answered = False
181
+ self.setWindowTitle("SSH host key" if question.confirmation else "SSH authentication")
182
+ self.resize(ERROR_WIDTH, ERROR_HEIGHT)
183
+ layout = QVBoxLayout(self)
184
+ description_label = QLabel(question.description)
185
+ description_label.setTextFormat(Qt.TextFormat.PlainText)
186
+ layout.addWidget(description_label)
187
+ scroll = QScrollArea()
188
+ scroll.setWidgetResizable(True)
189
+ label = QLabel(question.text)
190
+ label.setTextFormat(Qt.TextFormat.PlainText)
191
+ label.setWordWrap(True)
192
+ label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
193
+ scroll.setWidget(label)
194
+ layout.addWidget(scroll)
195
+ self.answer_edit = QLineEdit()
196
+ self.answer_edit.setEchoMode(QLineEdit.EchoMode.Password)
197
+ self.answer_edit.setAccessibleName("SSH passphrase or password")
198
+ label.setBuddy(self.answer_edit)
199
+ self.answer_edit.setVisible(not question.confirmation)
200
+ layout.addWidget(self.answer_edit)
201
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No if question.confirmation
202
+ else QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
203
+ buttons.accepted.connect(self.accept)
204
+ buttons.rejected.connect(self.reject)
205
+ if question.confirmation:
206
+ buttons.button(QDialogButtonBox.StandardButton.Yes).clicked.connect(self.accept)
207
+ buttons.button(QDialogButtonBox.StandardButton.No).clicked.connect(self.reject)
208
+ buttons.button(QDialogButtonBox.StandardButton.No).setDefault(True)
209
+ buttons.button(QDialogButtonBox.StandardButton.No).setFocus()
210
+ else:
211
+ self.answer_edit.setFocus()
212
+ layout.addWidget(buttons)
213
+ fit_window(self, SETTING_MIN_WIDTH, SETTING_MIN_HEIGHT)
214
+
215
+ def accept(self) -> None:
216
+ """Send the answer directly to its private socket and immediately clear the field."""
217
+ if self.answered:
218
+ return
219
+ self.answered = True
220
+ # Sent exactly as typed, blanks included, unlike the other typed values: a space may be part of a passphrase.
221
+ answer = "yes" if self.question.confirmation else self.answer_edit.text()
222
+ self.answer_edit.clear()
223
+ self.question.answer(answer)
224
+ super().accept()
225
+
226
+ def reject(self) -> None:
227
+ """Refuse an unapproved question without retaining its entered credential."""
228
+ if not self.answered:
229
+ self.answered = True
230
+ self.answer_edit.clear()
231
+ self.question.answer("no" if self.question.confirmation else None)
232
+ super().reject()
@@ -0,0 +1,240 @@
1
+ """Show the Settings page that puts a GitPane shortcut on the user's desktop.
2
+
3
+ The page saves nothing: its button acts at once, so it never counts as unsaved work and the bottom Save and Cancel
4
+ keep applying to the other pages. The shortcut is written on a worker thread, since gio or PowerShell may take a
5
+ while, and its report comes back to the page through a queued signal. The report gives one line per file written or
6
+ tried, each in the colour of what happened to that file, then the command the shortcut starts in ordinary text.
7
+
8
+ Each time the page is shown, it reads on another worker thread whether the desktop can show a desktop icon at all; when
9
+ it cannot, a note in ordinary text under the result lines says why and gives the steps that would let it. The result
10
+ lines follow the facts last read: the report shown is drawn again when new facts arrive. Both worker threads always
11
+ answer the page; an error nobody expects gives no note, or a failure line for the shortcut, and is written to the
12
+ ``--log`` journal, since it shows a defect of GitPane rather than something about the desktop.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import threading
18
+ from enum import Enum
19
+ from typing import TYPE_CHECKING
20
+
21
+ from PySide6.QtCore import Qt, Signal
22
+ from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
23
+
24
+ from gitpane.account.credential_manager import ignore_line
25
+ from gitpane.config import SPACING_MD, control_height
26
+ from gitpane.system.desktop_icon_support import desktop_icon_note, report_for_desktop
27
+ from gitpane.system.desktop_shortcut import ShortcutOutcome, ShortcutOutcomeKind, ShortcutReport, ShortcutStatus
28
+ from gitpane.thread.weak_signal import hand_over_to_the_page, weak_signal_emitter
29
+ from gitpane.ui.setting.setting_block import block_group
30
+ from gitpane.ui.widget.result_label import ResultLabel, ResultTone
31
+ from gitpane.ui.window_control.button_focus import set_button_enabled
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Callable
35
+
36
+ from PySide6.QtGui import QShowEvent
37
+
38
+ from gitpane.system.desktop_icon_support import DesktopIconFacts
39
+
40
+ SHORTCUT_THREAD_NAME = "gitpane-desktop-shortcut"
41
+ DESKTOP_ICON_FACT_THREAD_NAME = "gitpane-desktop-icon-facts"
42
+ DESKTOP_ICON_FACT_READ_FAILURE_LOG_TEMPLATE = "Reading whether the desktop can show a desktop icon failed unexpectedly: {error_class}: {error}"
43
+ DESKTOP_SHORTCUT_CREATION_FAILURE_LOG_TEMPLATE = "Creating the desktop shortcut failed unexpectedly: {error_class}: {error}"
44
+ # The detail of the red line of a creation that failed on an error nobody expects: its class says more than its
45
+ # message alone, which can be empty or a bare key.
46
+ UNEXPECTED_CREATION_ERROR_DETAIL_TEMPLATE = "{error_class}: {error}"
47
+ # The colour of each kind of result line: a file written is green, a file that could not be written for want of its
48
+ # folder is orange, a refused write is red.
49
+ RESULT_TONE_BY_OUTCOME_KIND = {
50
+ ShortcutOutcomeKind.WRITTEN: ResultTone.SUCCESS,
51
+ ShortcutOutcomeKind.NOT_POSSIBLE: ResultTone.WARNING,
52
+ ShortcutOutcomeKind.FAILED: ResultTone.FAILURE,
53
+ }
54
+
55
+
56
+ class UtilityPageText(Enum):
57
+ """Texts the Utilities page shows, one member per text so a misspelt name is an error."""
58
+
59
+ PAGE_NAME = "Utilities"
60
+ DESKTOP_SHORTCUT = "Desktop shortcut"
61
+ """The title of the block holding the shortcut button."""
62
+ CREATE_DESKTOP_SHORTCUT = "Create desktop shortcut"
63
+ CREATING = "Creating desktop shortcut…"
64
+ STARTS = "Starts: {command}"
65
+ """The ordinary line under the result lines that names the program the shortcut starts and its arguments."""
66
+
67
+
68
+ def run_shortcut_creation(create_shortcut: Callable[[], ShortcutReport], hand_over: Callable[[ShortcutReport], None], log_line: Callable[[str], None] = ignore_line) -> None:
69
+ """Create the shortcut on the worker thread and hand its report over to the page's thread.
70
+
71
+ A creator that fails on the file system or on a helper still gives one line. Any other error also gives one red
72
+ line, and is written through ``log_line``, since it shows a defect of GitPane, not of the system. The report is
73
+ handed over in every case, so the page always enables its button again.
74
+ """
75
+ # The answer left when even an exit of the worker itself ends the creator: the button is enabled again all the same.
76
+ report = ShortcutReport((ShortcutOutcome(ShortcutStatus.WRITE_FAILED),))
77
+ try:
78
+ report = create_shortcut()
79
+ except (OSError, ValueError, RuntimeError) as error:
80
+ report = ShortcutReport((ShortcutOutcome(ShortcutStatus.WRITE_FAILED, str(error)),))
81
+ except Exception as error: # noqa: BLE001 -- the worker must always answer the page; the error is written to the --log journal
82
+ report = ShortcutReport((ShortcutOutcome(ShortcutStatus.WRITE_FAILED, UNEXPECTED_CREATION_ERROR_DETAIL_TEMPLATE.format(error_class=type(error).__name__, error=error)),))
83
+ log_line(DESKTOP_SHORTCUT_CREATION_FAILURE_LOG_TEMPLATE.format(error_class=type(error).__name__, error=error))
84
+ finally:
85
+ hand_over_to_the_page(hand_over, report)
86
+
87
+
88
+ def run_fact_read(read_facts: Callable[[], DesktopIconFacts], hand_over: Callable[[DesktopIconFacts | None], None], log_line: Callable[[str], None] = ignore_line) -> None:
89
+ """Read the desktop icon facts on the worker thread and hand them over to the page's thread, or None when the read failed.
90
+
91
+ A failed read gives no note, so the page never says something about the desktop it could not read. The page
92
+ always gets an answer, so its next read starts again. An error nobody expects shows a defect of GitPane, not of
93
+ the desktop: it is written through ``log_line``.
94
+ """
95
+ facts: DesktopIconFacts | None = None
96
+ try:
97
+ facts = read_facts()
98
+ except (OSError, ValueError, RuntimeError):
99
+ facts = None
100
+ except Exception as error: # noqa: BLE001 -- the worker must always answer the page; the error is written to the --log journal
101
+ facts = None
102
+ log_line(DESKTOP_ICON_FACT_READ_FAILURE_LOG_TEMPLATE.format(error_class=type(error).__name__, error=error))
103
+ finally:
104
+ hand_over_to_the_page(hand_over, facts)
105
+
106
+
107
+ class UtilityPage(QWidget):
108
+ """The Utilities page: one button that puts a GitPane shortcut on the desktop, then one line per file saying how it went, the line naming what the shortcut starts, and a note when the desktop cannot show the icon."""
109
+
110
+ shortcut_finished = Signal(object)
111
+ facts_read = Signal(object)
112
+
113
+ def __init__(self, create_shortcut: Callable[[], ShortcutReport], read_desktop_icon_facts: Callable[[], DesktopIconFacts], parent: QWidget | None = None, *,
114
+ log_line: Callable[[str], None] = ignore_line) -> None:
115
+ """Build the page around ``create_shortcut``, which writes the shortcut, and ``read_desktop_icon_facts``, which reads the session; both run off the window's thread.
116
+
117
+ ``log_line`` writes one line to the ``--log`` journal, from any thread, when either worker meets an error
118
+ nobody expects; without a journal it writes nothing. ``last_report`` is the report the lines show, kept so
119
+ that facts arriving later draw it again; it is None while no report is shown.
120
+ """
121
+ super().__init__(parent)
122
+ self.create_shortcut = create_shortcut
123
+ self.read_desktop_icon_facts = read_desktop_icon_facts
124
+ self.log_line = log_line
125
+ self.creating = False
126
+ self.reading_facts = False
127
+ self.desktop_icon_facts: DesktopIconFacts | None = None
128
+ self.last_report: ShortcutReport | None = None
129
+ self.result_labels: list[ResultLabel] = []
130
+ layout = QVBoxLayout(self)
131
+ layout.setContentsMargins(0, 0, 0, 0)
132
+ layout.setSpacing(SPACING_MD)
133
+ group, group_layout = block_group(UtilityPageText.DESKTOP_SHORTCUT.value)
134
+ self.create_button = QPushButton(UtilityPageText.CREATE_DESKTOP_SHORTCUT.value)
135
+ self.create_button.clicked.connect(self.start_creation)
136
+ button_layout = QHBoxLayout()
137
+ button_layout.addWidget(self.create_button)
138
+ button_layout.addStretch()
139
+ group_layout.addLayout(button_layout)
140
+ # The wait line, shown only while a creation runs, so an empty label never adds a gap above the result lines.
141
+ self.status_label = ResultLabel()
142
+ self.status_label.hide()
143
+ group_layout.addWidget(self.status_label)
144
+ self.result_layout = QVBoxLayout()
145
+ self.result_layout.setContentsMargins(0, 0, 0, 0)
146
+ self.result_layout.setSpacing(group_layout.spacing())
147
+ group_layout.addLayout(self.result_layout)
148
+ # The note under the result lines, shown only when the desktop cannot show the icon.
149
+ self.note_label = ResultLabel()
150
+ self.note_label.hide()
151
+ group_layout.addWidget(self.note_label)
152
+ layout.addWidget(group)
153
+ layout.addStretch()
154
+ self.shortcut_finished.connect(self.show_report, Qt.ConnectionType.QueuedConnection)
155
+ self.facts_read.connect(self.show_facts, Qt.ConnectionType.QueuedConnection)
156
+ self.refresh_metrics()
157
+
158
+ def start_creation(self) -> None:
159
+ """Start writing the shortcut on a worker thread, unless a creation is still running."""
160
+ if self.creating:
161
+ return
162
+ self.creating = True
163
+ set_button_enabled(self.create_button, False)
164
+ # A report no longer shown is never drawn again by facts that arrive during this creation.
165
+ self.last_report = None
166
+ self.remove_result_labels()
167
+ self.status_label.setText(UtilityPageText.CREATING.value)
168
+ self.status_label.show()
169
+ hand_over = weak_signal_emitter(self, lambda page: page.shortcut_finished)
170
+ thread = threading.Thread(target=run_shortcut_creation, args=(self.create_shortcut, hand_over, self.log_line), name=SHORTCUT_THREAD_NAME, daemon=True)
171
+ thread.start()
172
+
173
+ def showEvent(self, event: QShowEvent) -> None:
174
+ """Read again whether the desktop can show the icon each time the page is shown, since the user may have followed the note meanwhile."""
175
+ super().showEvent(event)
176
+ self.start_fact_read()
177
+
178
+ def start_fact_read(self) -> None:
179
+ """Start reading the desktop icon facts on a worker thread, unless a read is still running."""
180
+ if self.reading_facts:
181
+ return
182
+ self.reading_facts = True
183
+ hand_over = weak_signal_emitter(self, lambda page: page.facts_read)
184
+ thread = threading.Thread(target=run_fact_read, args=(self.read_desktop_icon_facts, hand_over, self.log_line), name=DESKTOP_ICON_FACT_THREAD_NAME, daemon=True)
185
+ thread.start()
186
+
187
+ def show_facts(self, facts: DesktopIconFacts | None) -> None:
188
+ """Keep the facts read, show the note they give under the result lines, or hide it when they give none or could not be read, and draw the report shown again with them.
189
+
190
+ The facts can arrive after the click, or change while a report is shown when the page is shown again: drawing
191
+ the report again keeps the colour of the shortcut's line in agreement with the note.
192
+ """
193
+ self.reading_facts = False
194
+ self.desktop_icon_facts = facts
195
+ note = desktop_icon_note(facts) if facts is not None else ""
196
+ self.note_label.setText(note)
197
+ self.note_label.setVisible(bool(note))
198
+ if self.last_report is not None:
199
+ self.show_report_lines(self.last_report)
200
+
201
+ def remove_result_labels(self) -> None:
202
+ """Take away the result lines of the previous click."""
203
+ for label in self.result_labels:
204
+ self.result_layout.removeWidget(label)
205
+ label.hide()
206
+ label.deleteLater()
207
+ self.result_labels = []
208
+
209
+ def add_result_label(self, text: str, tone: ResultTone) -> None:
210
+ """Add one selectable result line under the previous ones, in ``tone``."""
211
+ label = ResultLabel()
212
+ label.show_result(text, tone)
213
+ self.result_layout.addWidget(label)
214
+ self.result_labels.append(label)
215
+
216
+ def show_report(self, report: ShortcutReport) -> None:
217
+ """Let the button be clicked again, end the wait line, then keep ``report`` and show its lines."""
218
+ self.creating = False
219
+ set_button_enabled(self.create_button, True)
220
+ self.status_label.clear()
221
+ self.status_label.hide()
222
+ self.last_report = report
223
+ self.show_report_lines(report)
224
+
225
+ def show_report_lines(self, report: ShortcutReport) -> None:
226
+ """Show one line per file of ``report`` in the colour of what happened to it, by the facts last read, then the command the shortcut starts.
227
+
228
+ A desktop shortcut written that the desktop will not show is shown in orange; the note stays under these
229
+ lines. The facts can arrive after the click or change while the report is shown: drawing it from the writer's
230
+ report with the latest facts keeps the shortcut's line in agreement with the note.
231
+ """
232
+ self.remove_result_labels()
233
+ for outcome in report_for_desktop(report, self.desktop_icon_facts).file_outcomes:
234
+ self.add_result_label(outcome.line(), RESULT_TONE_BY_OUTCOME_KIND[outcome.status.outcome_kind])
235
+ if report.start_command_line:
236
+ self.add_result_label(UtilityPageText.STARTS.value.format(command=report.start_command_line), ResultTone.NEUTRAL)
237
+
238
+ def refresh_metrics(self) -> None:
239
+ """Give the button the height of the other Settings controls for the current font."""
240
+ self.create_button.setMinimumHeight(control_height(self.create_button.fontMetrics().height()))
File without changes