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,408 @@
1
+ """Present live filtered command output with supervised cancellation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QSignalBlocker, QSize, Qt, Signal
9
+ from PySide6.QtWidgets import QCheckBox, QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QScrollArea, QStyle, QVBoxLayout
10
+ from shiboken6 import isValid
11
+
12
+ from gitpane.config import (
13
+ CONSOLE_ABORTABLE_OPERATION_SET,
14
+ OPERATION_HEIGHT,
15
+ OPERATION_KEEP_OPEN_PREFERENCE_KEY,
16
+ OPERATION_WIDTH,
17
+ ConsoleOperation,
18
+ SharedText,
19
+ fit_window,
20
+ )
21
+ from gitpane.operation.console_line import COMPLETED_STATUS_SET
22
+ from gitpane.operation.console_tone import ConsoleTone, result_tone
23
+ from gitpane.operation.operation_status import OperationStatus
24
+ from gitpane.transport.refusal_notice import headline_first
25
+ from gitpane.ui.console.operation_output_view import OperationOutputView
26
+ from gitpane.ui.widget.result_label import ResultLabel
27
+ from gitpane.ui.widget.standard_icon_label import StandardIconLabel
28
+
29
+ if TYPE_CHECKING:
30
+ from PySide6.QtCore import QRect
31
+ from PySide6.QtGui import QCloseEvent, QShowEvent
32
+ from PySide6.QtWidgets import QWidget
33
+
34
+ from gitpane.operation.operation_trace import OperationTrace
35
+ from gitpane.preference.preference_store import PreferenceStore
36
+ from gitpane.process.process_runner import ProcessRequest
37
+
38
+ # A pause names where the user picks the operation up again: the Rebase window for a rebase, the repository window
39
+ # banner for a cherry-pick or revert, the conflicts for a Stash pop. The console kind decides it, since a Continue
40
+ # started from the banner has no form.
41
+ PAUSE_LABEL_BY_CONSOLE_OPERATION = {ConsoleOperation.REBASE: "Paused: Git is waiting for you in the Rebase window",
42
+ ConsoleOperation.REFERENCE: "Paused: resolve the commit operation before continuing",
43
+ ConsoleOperation.STASH_POP: "Paused: resolve the merge conflicts"}
44
+ DEFAULT_PAUSE_LABEL = "Paused"
45
+ # The red failure text repeats the server's lines and Git's last lines. Past this many lines it scrolls in its own
46
+ # area, so the console keeps its size and the full output below keeps most of its height.
47
+ FAILURE_TEXT_VISIBLE_LINE_COUNT = 10
48
+
49
+
50
+ class OperationDialogText(Enum):
51
+ """Texts only the operation console shows, one member per text so a misspelt name is an error."""
52
+
53
+ COMPLETED = "Completed"
54
+ ABORTED = "Aborted"
55
+ PARTIALLY_COMPLETED = "Partially completed"
56
+
57
+
58
+ # The status line for each end status but a pause, which shows where the user picks the operation up again. Every
59
+ # status is written here one by one, so a status added later has no line until one is chosen for it: a cancelled run
60
+ # reads like an Abort, a stop by a limit or a refusal like a failure, and an end GitPane did not observe as such
61
+ # (a full queue, a volume refusal, a step that never began, a wait still under way…) as an unknown outcome.
62
+ RESULT_TEXT_BY_STATUS: dict[OperationStatus, OperationDialogText | SharedText] = {
63
+ OperationStatus.SUCCESS: OperationDialogText.COMPLETED,
64
+ OperationStatus.ABORTED: OperationDialogText.ABORTED,
65
+ OperationStatus.CANCELLED: OperationDialogText.ABORTED,
66
+ OperationStatus.FAILED: SharedText.FAILED,
67
+ OperationStatus.REFUSED: SharedText.FAILED,
68
+ OperationStatus.START_FAILED: SharedText.FAILED,
69
+ OperationStatus.TIMEOUT: SharedText.FAILED,
70
+ OperationStatus.IDLE_TIMEOUT: SharedText.FAILED,
71
+ OperationStatus.PARTIAL: OperationDialogText.PARTIALLY_COMPLETED,
72
+ OperationStatus.UNKNOWN: SharedText.OUTCOME_UNKNOWN,
73
+ OperationStatus.VOLUME: SharedText.OUTCOME_UNKNOWN,
74
+ OperationStatus.QUEUE_FULL: SharedText.OUTCOME_UNKNOWN,
75
+ OperationStatus.NOT_STARTED: SharedText.OUTCOME_UNKNOWN,
76
+ OperationStatus.SIGN_IN_TIMEOUT: SharedText.OUTCOME_UNKNOWN,
77
+ OperationStatus.STEP_NOT_STARTED: SharedText.OUTCOME_UNKNOWN,
78
+ OperationStatus.NO_CHANGE: SharedText.OUTCOME_UNKNOWN,
79
+ OperationStatus.PENDING: SharedText.OUTCOME_UNKNOWN,
80
+ }
81
+
82
+
83
+ def status_line_text(status: OperationStatus, pause_label: str) -> str:
84
+ """Give the text of the console's status line for an end status; a pause shows where the user picks the operation up again."""
85
+ if status == OperationStatus.PAUSED:
86
+ return pause_label
87
+ # The fallback only keeps the console readable if a status is ever added without running the tests that require its line.
88
+ return RESULT_TEXT_BY_STATUS.get(status, SharedText.OUTCOME_UNKNOWN).value
89
+
90
+
91
+ class OperationDialog(QDialog):
92
+ """Display safe operation records without taking ownership of the process."""
93
+
94
+ abort_requested = Signal()
95
+ followup_requested = Signal()
96
+
97
+ def __init__(self, trace: OperationTrace, title: str, parent: QWidget | None = None, keep_open: bool = True, *, preference: PreferenceStore | None = None,
98
+ console_operation: ConsoleOperation) -> None:
99
+ """Build permanent controls beneath the scrolling plain-text output.
100
+
101
+ The console kind decides once whether Abort is offered, so callers never choose it one by one.
102
+ """
103
+ super().__init__(parent)
104
+ self.trace = trace
105
+ self.abortable = console_operation in CONSOLE_ABORTABLE_OPERATION_SET
106
+ self.pause_label = PAUSE_LABEL_BY_CONSOLE_OPERATION.get(console_operation, DEFAULT_PAUSE_LABEL)
107
+ self.running = True
108
+ self.success = False
109
+ self.completed = False
110
+ self.followup_pending = False
111
+ # Set by a result whose console carries a line the user must read: Keep dialog open changed in another console
112
+ # then leaves it open, and only the user's own click in this console closes it.
113
+ self.stays_open = False
114
+ self.preference = preference
115
+ self.dismissed = False
116
+ self.return_focus = parent.focusWidget() if parent else None
117
+ self.setWindowTitle(title)
118
+ self.resize(OPERATION_WIDTH, OPERATION_HEIGHT)
119
+ layout = QVBoxLayout(self)
120
+ status_row = QHBoxLayout()
121
+ icon_side = self.style().pixelMetric(QStyle.PixelMetric.PM_SmallIconSize)
122
+ self.status_icon = StandardIconLabel(QSize(icon_side, icon_side))
123
+ status_row.addWidget(self.status_icon, 0, Qt.AlignmentFlag.AlignVCenter)
124
+ self.status_label = QLabel("Starting…")
125
+ self.status_label.setTextFormat(Qt.TextFormat.PlainText)
126
+ self.status_label.setWordWrap(True)
127
+ status_row.addWidget(self.status_label, 1)
128
+ layout.addLayout(status_row)
129
+ # Fetch and Push output is buried under the debug traces GitPane turns on, so a failure repeats here, in bold
130
+ # red, the plain reason and what Git itself said; the full output stays below. A long refusal (a hook's lines,
131
+ # repeated in Git's own lines) would make the console taller than the screen, so the reason scrolls in its own
132
+ # area and the console keeps its size.
133
+ self.failure_scroll_area = QScrollArea()
134
+ self.failure_scroll_area.setWidgetResizable(True)
135
+ self.failure_scroll_area.setFrameShape(QScrollArea.Shape.NoFrame)
136
+ # The text wraps, long words included, so a horizontal bar would never have anything to show.
137
+ self.failure_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
138
+ # No extra Tab stop: the label keeps its own keyboard selection and the wheel still scrolls the area.
139
+ self.failure_scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
140
+ self.failure_scroll_area.hide()
141
+ layout.addWidget(self.failure_scroll_area)
142
+ # The area is in the console before it takes the label, so the label watches the console for theme changes
143
+ # from the start: it follows its window every time its parent changes.
144
+ self.failure_label = ResultLabel()
145
+ self.failure_label.setAccessibleName("Failure reason")
146
+ self.failure_scroll_area.setWidget(self.failure_label)
147
+ self.failure_label.hide()
148
+ self.progress = QProgressBar()
149
+ self.progress.setRange(0, 0)
150
+ self.progress.setAccessibleName("Operation progress")
151
+ layout.addWidget(self.progress)
152
+ self.output_view = OperationOutputView(trace, self)
153
+ layout.addWidget(self.output_view, 1)
154
+ self.preference_label = QLabel()
155
+ self.preference_label.setTextFormat(Qt.TextFormat.PlainText)
156
+ self.preference_label.setWordWrap(True)
157
+ self.preference_label.hide()
158
+ layout.addWidget(self.preference_label)
159
+ self.followup_label = QLabel()
160
+ self.followup_label.setTextFormat(Qt.TextFormat.PlainText)
161
+ self.followup_label.setWordWrap(True)
162
+ self.followup_label.hide()
163
+ layout.addWidget(self.followup_label)
164
+ actions = QHBoxLayout()
165
+ self.keep_open_check = QCheckBox("Keep dialog open")
166
+ self.keep_open_check.setChecked(bool(preference.read(OPERATION_KEEP_OPEN_PREFERENCE_KEY)) if preference is not None else keep_open)
167
+ actions.addWidget(self.keep_open_check)
168
+ self.copy_button = QPushButton("Copy details")
169
+ self.copy_button.clicked.connect(self.output_view.copy_detail)
170
+ actions.addWidget(self.copy_button)
171
+ self.followup_button = QPushButton("Push tag")
172
+ self.followup_button.setAutoDefault(False)
173
+ self.followup_button.clicked.connect(self.followup_requested)
174
+ self.followup_button.hide()
175
+ actions.addWidget(self.followup_button)
176
+ self.abort_button = QPushButton("Abort")
177
+ self.abort_button.clicked.connect(self.request_abort)
178
+ # The button also gates Escape and the close button through request_abort, so a local console keeps it disabled as well as hidden.
179
+ self.abort_button.setEnabled(self.abortable)
180
+ self.abort_button.setVisible(self.abortable)
181
+ actions.addWidget(self.abort_button)
182
+ self.ok_button = QPushButton("OK")
183
+ self.ok_button.setEnabled(False)
184
+ self.ok_button.clicked.connect(self.accept)
185
+ actions.addWidget(self.ok_button)
186
+ layout.addLayout(actions)
187
+ self.keep_open_check.toggled.connect(self._keep_open)
188
+ trace.runner.stop_requested.connect(self._process_stopping)
189
+ if preference is not None:
190
+ preference.changed.connect(self._preference_changed)
191
+ self.finished.connect(self._restore_focus)
192
+ fit_window(self, 0, 0)
193
+
194
+ def refresh_metrics(self, available_geometry: QRect | None = None) -> None:
195
+ """Keep output, the red failure text and permanent actions reachable after screen or font changes."""
196
+ available = available_geometry if available_geometry is not None else self.screen().availableGeometry()
197
+ content_width = self._content_width(available)
198
+ self.output_view.refresh_metrics(content_width)
199
+ self._fit_failure_height(content_width)
200
+ self.layout().activate()
201
+ fit_window(self, 0, 0, available_geometry)
202
+
203
+ def _content_width(self, available: QRect) -> int:
204
+ """Return the width the console's content gets on a screen area: the console's width within that area, less the layout margins.
205
+
206
+ It is the only definition of that width, shared by the output view and the red failure text so both wrap alike.
207
+ """
208
+ margins = self.layout().contentsMargins()
209
+ return min(self.width(), available.width()) - margins.left() - margins.right()
210
+
211
+ def _fit_failure_height(self, width: int) -> None:
212
+ """Give the red failure area the height of its text wrapped at ``width``, at most ``FAILURE_TEXT_VISIBLE_LINE_COUNT`` lines; past that the text scrolls.
213
+
214
+ It runs when the text changes, when the console is shown and after a screen or font change, never on resize: a
215
+ console the user narrows shows the scroll bar a little earlier instead of measuring everything again at each step.
216
+ """
217
+ if self.failure_label.isHidden():
218
+ return
219
+ # The bold of the label's style sheet takes part in the measure.
220
+ self.failure_label.ensurePolished()
221
+ label_margins = self.failure_label.contentsMargins()
222
+ vertical_margin = label_margins.top() + label_margins.bottom() + self.failure_label.margin() * 2
223
+ height_limit = self.failure_label.fontMetrics().lineSpacing() * FAILURE_TEXT_VISIBLE_LINE_COUNT + vertical_margin
224
+ self.failure_scroll_area.setFixedHeight(min(self.failure_label.heightForWidth(width), height_limit))
225
+
226
+ def showEvent(self, event: QShowEvent) -> None:
227
+ """Refresh a reopened output view without retaining a second text document."""
228
+ self.dismissed = False
229
+ self.output_view.stopped = False
230
+ super().showEvent(event)
231
+ self.refresh_metrics()
232
+
233
+ def set_running(self) -> None:
234
+ """Show an active process while disabling premature dismissal; only a network console can be stopped."""
235
+ self.running = True
236
+ self.success = False
237
+ self.completed = False
238
+ self.output_view.output_highlighter.show_paused_result(False)
239
+ self.status_label.setText("Running…")
240
+ self.show_failure_text("")
241
+ self.abort_button.setEnabled(self.abortable)
242
+ self.ok_button.setEnabled(False)
243
+ self.progress.show()
244
+
245
+ def show_result(self, status: OperationStatus, complete: bool = True, *, failure_reported: bool = False, failure_text: str = "", failure_headline: str = "",
246
+ expected_stop: bool = False, stays_open: bool = False) -> None:
247
+ """Keep every failure or incomplete trace available for inspection, and let an expected stop go like a success.
248
+
249
+ ``success`` means the operation reached its goal; ``completed`` also covers a verified Abort and an expected
250
+ stop, and decides whether Keep dialog open may dismiss the console. An expected stop is a pause that waits for
251
+ the user (a conflict or an edit stop of a rebase, a cherry-pick or revert conflict, or a Stash pop stopped on
252
+ conflicts when the caller says so with ``expected_stop``): like Git Extensions, the console then closes when
253
+ Keep dialog open is unchecked, since what the user must answer is elsewhere. ``failure_reported`` tells that the
254
+ result carries a failure cause: a pause with a cause (a refused hook, a fatal Git error) is a real failure and
255
+ keeps the console open, as does an incomplete trace. ``failure_text`` is a short explanation of a failure, shown
256
+ in bold red under the status line, after the ``failure_headline`` sentence when there is one; a success or an
257
+ expected stop never shows them. ``stays_open`` keeps a completed console open even with Keep dialog open
258
+ unchecked, for a success whose console carries a line the user must read (a pushed branch that could not be
259
+ linked to its remote branch); Keep dialog open takes over again as soon as the user changes it in this console,
260
+ while a change made from another console only updates its box. The byte counter is rewritten at once so the
261
+ result is shown with the exact total.
262
+ """
263
+ self.output_view.flush_output(exact_count=True)
264
+ complete = complete and self.trace.complete
265
+ # A plain pause waits for the user: Git's error lines explain the stop and are shown neutral. An incomplete trace
266
+ # or a reported failure keeps the error rendering and the console. The pause is read from the same classification
267
+ # as the colour of the console's end line, so the window and that line cannot disagree.
268
+ paused = result_tone(status, failure_reported=failure_reported or not complete, expected_stop=expected_stop) == ConsoleTone.PAUSED
269
+ neutral = paused or (status == OperationStatus.ABORTED and complete and not failure_reported)
270
+ failed = status != OperationStatus.SUCCESS and not paused
271
+ self.show_failure_text(failure_text if failed else "", failure_headline if failed else "")
272
+ self.running = False
273
+ self.stays_open = stays_open
274
+ self.success = status == OperationStatus.SUCCESS and complete
275
+ self.completed = (status in COMPLETED_STATUS_SET and complete) or neutral
276
+ self.output_view.output_highlighter.show_paused_result(paused)
277
+ label = self.pause_label if paused else status_line_text(status, self.pause_label)
278
+ self.status_label.setText(label if complete else "Trace incomplete: controlled exchanges are missing. Available command output is shown below.")
279
+ self.status_label.setWordWrap(True)
280
+ self.abort_button.setEnabled(False)
281
+ self.ok_button.setEnabled(True)
282
+ self.ok_button.setDefault(True)
283
+ self.ok_button.setFocus()
284
+ self.progress.hide()
285
+ if self.success:
286
+ icon = QStyle.StandardPixmap.SP_DialogApplyButton
287
+ elif neutral:
288
+ icon = QStyle.StandardPixmap.SP_MessageBoxInformation
289
+ else:
290
+ icon = QStyle.StandardPixmap.SP_MessageBoxWarning
291
+ self.status_icon.set_standard_pixmap(icon)
292
+ if self.completed and not stays_open and not self.followup_pending and not self.keep_open_check.isChecked() and not self.dismissed:
293
+ self.accept()
294
+
295
+ def show_failure_text(self, text: str, headline: str = "") -> None:
296
+ """Show a failure explanation in bold red under the status line, or hide the line when there is none.
297
+
298
+ It is also used to rewrite the explanation of a finished run, when something learned afterwards completes it,
299
+ such as an account found to need a reconnect. The text goes through the console's own filter once more, so nothing shown here can hold more than the output
300
+ does. The headline is GitPane's own sentence, built from an address and an account label that are not secrets:
301
+ it is shown first and unfiltered, because the filter has learned the Git username from the command's URL and
302
+ would hide the very account the sentence names. The explanation takes at most ``FAILURE_TEXT_VISIBLE_LINE_COUNT``
303
+ lines and scrolls past them, so the console keeps its size.
304
+ """
305
+ if not text and not headline:
306
+ self.failure_label.clear()
307
+ self.failure_label.hide()
308
+ self.failure_scroll_area.hide()
309
+ return
310
+ self.failure_label.show_failure(headline_first(headline, self.trace.filter.redact(text)))
311
+ self.failure_label.show()
312
+ self.failure_scroll_area.show()
313
+ self._fit_failure_height(self._content_width(self.screen().availableGeometry()))
314
+
315
+ def settle_pause(self) -> None:
316
+ """Treat a console that stopped on a failure as done once the user answered it or its operation ended.
317
+
318
+ An expected pause no longer waits here: its console is already completed when the result is shown. This covers
319
+ what stays open on a failure while the operation still waits for the user (a rebase step Git refused, a refused
320
+ push). From then on Keep dialog open decides, as for a completed result: the console closes now when the
321
+ option is unchecked, or later when the user unchecks it. A console that still runs is left alone, because
322
+ closing it would request a stop; nothing here aborts the operation.
323
+ """
324
+ if self.running or self.dismissed:
325
+ return
326
+ self.completed = True
327
+ if self.trace.complete and not self.followup_pending and not self.keep_open_check.isChecked():
328
+ self.accept()
329
+
330
+ def show_followup(self, message: str = "") -> None:
331
+ """Keep an explicitly requested follow-up available without changing the Git result."""
332
+ self.followup_pending = bool(message)
333
+ self.followup_label.setText(self.trace.filter.redact(message))
334
+ self.followup_label.setVisible(self.followup_pending)
335
+ self.followup_button.setVisible(self.followup_pending)
336
+
337
+ def _process_stopping(self, request: ProcessRequest, _cause: str) -> None:
338
+ """Display supervised timeout and shutdown while the process remains alive."""
339
+ if request.operation_id == self.trace.operation_id and self.running and not self.dismissed:
340
+ self.status_label.setText("Stopping…")
341
+ self.abort_button.setEnabled(False)
342
+
343
+ def request_abort(self) -> None:
344
+ """Request a supervised stop without claiming rollback or process exit."""
345
+ if self.running and self.abort_button.isEnabled():
346
+ self.status_label.setText("Stopping…")
347
+ self.abort_button.setEnabled(False)
348
+ self.abort_requested.emit()
349
+
350
+ def reject(self) -> None:
351
+ """Treat Escape and the window close action as stop while running."""
352
+ if self.running:
353
+ self.request_abort()
354
+ return
355
+ super().reject()
356
+
357
+ def accept(self) -> None:
358
+ """Prevent Enter or direct acceptance from dismissing a running operation."""
359
+ if not self.running:
360
+ super().accept()
361
+
362
+ def closeEvent(self, event: QCloseEvent) -> None:
363
+ """Keep the native dialog visible until a requested stop has actually completed."""
364
+ if self.running:
365
+ self.request_abort()
366
+ event.ignore()
367
+ return
368
+ super().closeEvent(event)
369
+
370
+ def _keep_open(self, checked: bool) -> None:
371
+ """Save the changed close preference, then close a finished console when the box was just unchecked.
372
+
373
+ A preference that cannot be saved puts the box back and says why, so the box never shows a choice that was lost.
374
+ Other open consoles follow the saved value through the preference store.
375
+ """
376
+ if self.preference is not None:
377
+ try:
378
+ self.preference.update({OPERATION_KEEP_OPEN_PREFERENCE_KEY: checked})
379
+ except (OSError, ValueError, TypeError) as error:
380
+ with QSignalBlocker(self.keep_open_check):
381
+ self.keep_open_check.setChecked(not checked)
382
+ self.preference_label.setText("Could not save Keep dialog open: " + self.trace.filter.redact(str(error)))
383
+ self.preference_label.show()
384
+ return
385
+ self.preference_label.hide()
386
+ if self.completed and self.trace.complete and not self.followup_pending and not checked and not self.dismissed:
387
+ self.accept()
388
+
389
+ def _preference_changed(self, value_by_key: dict[str, object]) -> None:
390
+ """Share only successfully saved settings with other open operation dialogs.
391
+
392
+ A console held open for a line the user must read only follows the box: it closes on the user's own click in it,
393
+ never on a change made from another console.
394
+ """
395
+ if OPERATION_KEEP_OPEN_PREFERENCE_KEY not in value_by_key or self.dismissed:
396
+ return
397
+ checked = bool(value_by_key[OPERATION_KEEP_OPEN_PREFERENCE_KEY])
398
+ with QSignalBlocker(self.keep_open_check):
399
+ self.keep_open_check.setChecked(checked)
400
+ if self.completed and self.trace.complete and not self.followup_pending and not checked and not self.stays_open:
401
+ self.accept()
402
+
403
+ def _restore_focus(self) -> None:
404
+ """Restore focus only if the original trigger still exists, and stop the output rendering of the closed console."""
405
+ self.dismissed = True
406
+ self.output_view.stop()
407
+ if self.return_focus is not None and isValid(self.return_focus):
408
+ self.return_focus.setFocus()
@@ -0,0 +1,91 @@
1
+ """Colour each console line from the tone the view stored on its block, without changing the filtered console text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QEvent
8
+ from PySide6.QtGui import QColor, QFont, QSyntaxHighlighter, QTextCharFormat
9
+
10
+ from gitpane.history.revision_file import utf16_length
11
+ from gitpane.operation.console_tone import PAUSE_NEUTRAL_TONE_SET, ConsoleTone
12
+ from gitpane.ui.look.theme_color import is_dark_palette
13
+ from gitpane.ui.widget.result_label import ResultTone, result_color
14
+
15
+ if TYPE_CHECKING:
16
+ from PySide6.QtCore import QObject
17
+ from PySide6.QtWidgets import QPlainTextEdit
18
+
19
+ # The one place a console tone becomes a colour: green for a success, orange for what waits on the user or warns, red
20
+ # for a failure. The colours themselves are the result colours of the rest of the application.
21
+ RESULT_TONE_BY_CONSOLE_TONE = {
22
+ ConsoleTone.SUCCESS: ResultTone.SUCCESS,
23
+ ConsoleTone.PAUSED: ResultTone.WARNING,
24
+ ConsoleTone.CANCELLED: ResultTone.WARNING,
25
+ ConsoleTone.GIT_WARNING: ResultTone.WARNING,
26
+ ConsoleTone.FAILURE: ResultTone.FAILURE,
27
+ ConsoleTone.COMMAND_FAILURE: ResultTone.FAILURE,
28
+ ConsoleTone.GIT_ERROR: ResultTone.FAILURE,
29
+ ConsoleTone.FAILURE_CAUSE: ResultTone.FAILURE,
30
+ }
31
+ CONSOLE_TONE_VALUE_SET = frozenset(ConsoleTone)
32
+ # Bold is kept for the cause GitPane writes to explain a refusal, so it stands out among Git's own red lines.
33
+ BOLD_CONSOLE_TONE_SET = frozenset({ConsoleTone.FAILURE_CAUSE})
34
+
35
+
36
+ class OperationOutputHighlighter(QSyntaxHighlighter):
37
+ """Colour a whole console line, in the current theme, from the tone stored as the state of its block.
38
+
39
+ The view sets that state from what the trace recorded for each line; this highlighter only reads it, so a colour
40
+ never depends on recognising a word in the text. Once the operation has stopped on an expected pause (a conflict
41
+ waiting for the user), Git's own error lines and the failure line of its command are left neutral: they describe
42
+ why Git stopped, not a failure. Errors GitPane reports itself keep their colour in every case. Only a cause GitPane
43
+ writes to explain a refusal is bold; every other line keeps the normal weight.
44
+ """
45
+
46
+ def __init__(self, output: QPlainTextEdit) -> None:
47
+ """Attach visual formatting to the existing bounded plain-text document."""
48
+ super().__init__(output.document())
49
+ self.output = output
50
+ self.paused_result = False
51
+ self.format_by_tone: dict[ConsoleTone, QTextCharFormat] = {}
52
+ self.refresh_palette()
53
+ output.installEventFilter(self)
54
+
55
+ def refresh_palette(self) -> None:
56
+ """Rebuild the colour of each tone for the console's actual background, then recolour the document."""
57
+ dark = is_dark_palette(self.output.palette())
58
+ format_by_tone: dict[ConsoleTone, QTextCharFormat] = {}
59
+ for tone, result_tone in RESULT_TONE_BY_CONSOLE_TONE.items():
60
+ text_format = QTextCharFormat()
61
+ text_format.setForeground(QColor(result_color(result_tone, dark)))
62
+ if tone in BOLD_CONSOLE_TONE_SET:
63
+ text_format.setFontWeight(QFont.Weight.Bold)
64
+ format_by_tone[tone] = text_format
65
+ self.format_by_tone = format_by_tone
66
+ self.rehighlight()
67
+
68
+ def show_paused_result(self, paused_result: bool) -> None:
69
+ """Switch between the pause rendering and the normal rendering, recoloring the whole document only on a change."""
70
+ if paused_result == self.paused_result:
71
+ return
72
+ self.paused_result = paused_result
73
+ self.rehighlight()
74
+
75
+ def eventFilter(self, watched: QObject, event: QEvent) -> bool:
76
+ """Recolor the document after a palette change while preserving selection and scroll."""
77
+ if event.type() == QEvent.Type.PaletteChange:
78
+ self.refresh_palette()
79
+ return super().eventFilter(watched, event)
80
+
81
+ def highlightBlock(self, text: str) -> None:
82
+ """Colour the whole line from its block's tone, in bold only for a cause GitPane wrote; a neutral or unknown state leaves it as it is."""
83
+ state = self.currentBlockState()
84
+ if state not in CONSOLE_TONE_VALUE_SET:
85
+ return
86
+ tone = ConsoleTone(state)
87
+ if tone == ConsoleTone.NEUTRAL:
88
+ return
89
+ if self.paused_result and tone in PAUSE_NEUTRAL_TONE_SET:
90
+ return
91
+ self.setFormat(0, utf16_length(text), self.format_by_tone[tone])