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.
- gitpane/__init__.py +0 -0
- gitpane/account/__init__.py +0 -0
- gitpane/account/account_choice.py +297 -0
- gitpane/account/address_owner.py +67 -0
- gitpane/account/authentication.py +1811 -0
- gitpane/account/authentication_worker.py +836 -0
- gitpane/account/credential_manager.py +480 -0
- gitpane/account/credential_manager_install.py +171 -0
- gitpane/account/keyring_reader.py +135 -0
- gitpane/account/keyring_state.py +158 -0
- gitpane/account/ssh.py +312 -0
- gitpane/account/ssh_askpass.py +216 -0
- gitpane/account/ssh_prompt.py +222 -0
- gitpane/application.py +684 -0
- gitpane/commit/__init__.py +0 -0
- gitpane/commit/commit_metadata.py +88 -0
- gitpane/commit/commit_push.py +276 -0
- gitpane/commit/commit_write.py +830 -0
- gitpane/config.py +1066 -0
- gitpane/controller/__init__.py +0 -0
- gitpane/controller/account_controller.py +677 -0
- gitpane/controller/clone_controller.py +776 -0
- gitpane/controller/close_readiness.py +212 -0
- gitpane/controller/commit_timing_controller.py +566 -0
- gitpane/controller/conflict_controller.py +885 -0
- gitpane/controller/console_registry.py +38 -0
- gitpane/controller/emoji_window_controller.py +95 -0
- gitpane/controller/file_action_controller.py +479 -0
- gitpane/controller/history_read_controller.py +671 -0
- gitpane/controller/history_rewrite_controller.py +739 -0
- gitpane/controller/navigation_controller.py +658 -0
- gitpane/controller/opening_controller.py +375 -0
- gitpane/controller/operation_policy.py +76 -0
- gitpane/controller/rebase_controller.py +797 -0
- gitpane/controller/rebase_editor_controller.py +309 -0
- gitpane/controller/reference_form_controller.py +1677 -0
- gitpane/controller/reference_read_controller.py +242 -0
- gitpane/controller/refresh_controller.py +477 -0
- gitpane/controller/replay_controller.py +805 -0
- gitpane/controller/session_registry.py +337 -0
- gitpane/controller/setting_controller.py +592 -0
- gitpane/controller/shutdown_controller.py +345 -0
- gitpane/controller/staging_controller.py +199 -0
- gitpane/controller/staging_session_registry.py +47 -0
- gitpane/controller/stash_controller.py +367 -0
- gitpane/controller/transport_controller.py +813 -0
- gitpane/controller/welcome_controller.py +102 -0
- gitpane/controller/window_activation.py +116 -0
- gitpane/controller/worktree_state_controller.py +169 -0
- gitpane/emoji/__init__.py +0 -0
- gitpane/emoji/emoji_catalogue.py +156 -0
- gitpane/git_config/__init__.py +0 -0
- gitpane/git_config/git_setting.py +441 -0
- gitpane/history/__init__.py +0 -0
- gitpane/history/avatar.py +241 -0
- gitpane/history/file_history.py +359 -0
- gitpane/history/graph.py +145 -0
- gitpane/history/history_filter.py +31 -0
- gitpane/history/history_parse.py +605 -0
- gitpane/history/repository.py +1600 -0
- gitpane/history/repository_value.py +341 -0
- gitpane/history/revision.py +180 -0
- gitpane/history/revision_file.py +182 -0
- gitpane/history_rewrite/__init__.py +0 -0
- gitpane/history_rewrite/graph.py +210 -0
- gitpane/history_rewrite/guard.py +288 -0
- gitpane/history_rewrite/index.py +125 -0
- gitpane/history_rewrite/prepare.py +378 -0
- gitpane/history_rewrite/protocol.py +320 -0
- gitpane/history_rewrite/publish.py +444 -0
- gitpane/history_rewrite/recovery.py +481 -0
- gitpane/history_rewrite/value.py +229 -0
- gitpane/operation/__init__.py +0 -0
- gitpane/operation/console_line.py +313 -0
- gitpane/operation/console_tone.py +76 -0
- gitpane/operation/diagnostic.py +954 -0
- gitpane/operation/launch_signature.py +254 -0
- gitpane/operation/operation_status.py +87 -0
- gitpane/operation/operation_trace.py +766 -0
- gitpane/operation/repository_operation.py +781 -0
- gitpane/preference/__init__.py +0 -0
- gitpane/preference/preference_location.py +40 -0
- gitpane/preference/preference_store.py +590 -0
- gitpane/process/__init__.py +0 -0
- gitpane/process/exact_arguments.py +77 -0
- gitpane/process/git_output_text.py +20 -0
- gitpane/process/process_group.py +851 -0
- gitpane/process/process_log.py +114 -0
- gitpane/process/process_runner.py +1033 -0
- gitpane/process/released_child_watch.py +138 -0
- gitpane/recent/__init__.py +0 -0
- gitpane/recent/mount_table.py +422 -0
- gitpane/recent/recent_availability.py +414 -0
- gitpane/recent/recent_name.py +11 -0
- gitpane/recent/recent_search.py +114 -0
- gitpane/reference/__init__.py +0 -0
- gitpane/reference/reference_edit.py +2030 -0
- gitpane/reference/reference_snapshot.py +196 -0
- gitpane/reference/stash.py +730 -0
- gitpane/reference/stash_list.py +164 -0
- gitpane/sequencer/__init__.py +0 -0
- gitpane/sequencer/commit_message_rule.py +154 -0
- gitpane/sequencer/conflict.py +506 -0
- gitpane/sequencer/merge_recovery.py +163 -0
- gitpane/sequencer/merge_state.py +166 -0
- gitpane/sequencer/mergetool.py +856 -0
- gitpane/sequencer/rebase.py +750 -0
- gitpane/sequencer/rebase_edit_file.py +224 -0
- gitpane/sequencer/rebase_state.py +523 -0
- gitpane/sequencer/replay.py +1435 -0
- gitpane/sequencer/replay_legacy.py +253 -0
- gitpane/sequencer/replay_state.py +747 -0
- gitpane/submodule/__init__.py +0 -0
- gitpane/submodule/checkout_refusal.py +102 -0
- gitpane/submodule/checkout_refusal_notice.py +111 -0
- gitpane/submodule/cloned_without_checkout.py +65 -0
- gitpane/submodule/submodule_discovery.py +1180 -0
- gitpane/submodule/submodule_loss.py +218 -0
- gitpane/system/__init__.py +0 -0
- gitpane/system/appearance_portal.py +125 -0
- gitpane/system/application_identity.py +48 -0
- gitpane/system/command_line.py +36 -0
- gitpane/system/desktop_icon_fact.py +128 -0
- gitpane/system/desktop_icon_support.py +270 -0
- gitpane/system/desktop_shortcut.py +536 -0
- gitpane/system/icns_image.py +35 -0
- gitpane/system/installed_launcher.py +136 -0
- gitpane/system/linux_distribution.py +151 -0
- gitpane/system/network_folder.py +46 -0
- gitpane/system/path_identity.py +95 -0
- gitpane/system/private_tree_removal.py +47 -0
- gitpane/system/process_program.py +63 -0
- gitpane/system/qt_folder.py +38 -0
- gitpane/system/ready_channel_watch.py +67 -0
- gitpane/system/startup_activation.py +65 -0
- gitpane/system/terminal_detach.py +384 -0
- gitpane/system/xdg_session.py +85 -0
- gitpane/text/__init__.py +0 -0
- gitpane/text/text_query.py +97 -0
- gitpane/text/typed_path.py +75 -0
- gitpane/text/typed_text.py +17 -0
- gitpane/thread/__init__.py +0 -0
- gitpane/thread/weak_signal.py +93 -0
- gitpane/tool/__init__.py +0 -0
- gitpane/tool/app_bundle.py +164 -0
- gitpane/tool/desktop_text_editor.py +156 -0
- gitpane/tool/difftool.py +429 -0
- gitpane/tool/editor.py +445 -0
- gitpane/tool/editor_bridge.py +193 -0
- gitpane/tool/editor_recipe.py +481 -0
- gitpane/tool/file_reveal.py +94 -0
- gitpane/tool/git_executable.py +264 -0
- gitpane/tool/git_version.py +62 -0
- gitpane/tool/kdiff3.py +41 -0
- gitpane/tool/kdiff3_install.py +162 -0
- gitpane/tool/launch_services_text_editor.py +104 -0
- gitpane/tool/mergetool_recipe.py +84 -0
- gitpane/tool/system_editor_default.py +80 -0
- gitpane/tool/system_text_editor.py +44 -0
- gitpane/tool/system_tool.py +816 -0
- gitpane/tool/terminal_recipe.py +390 -0
- gitpane/transport/__init__.py +0 -0
- gitpane/transport/branch_exchange.py +1690 -0
- gitpane/transport/clone.py +1273 -0
- gitpane/transport/credential_read_retry.py +35 -0
- gitpane/transport/git_refusal.py +207 -0
- gitpane/transport/push_tracking.py +121 -0
- gitpane/transport/refusal_notice.py +225 -0
- gitpane/transport/remote.py +747 -0
- gitpane/transport/remote_config.py +544 -0
- gitpane/ui/__init__.py +0 -0
- gitpane/ui/console/__init__.py +0 -0
- gitpane/ui/console/error_dialog.py +332 -0
- gitpane/ui/console/operation_dialog.py +408 -0
- gitpane/ui/console/operation_output.py +91 -0
- gitpane/ui/console/operation_output_view.py +373 -0
- gitpane/ui/emoji/__init__.py +0 -0
- gitpane/ui/emoji/emoji_font.py +257 -0
- gitpane/ui/emoji/emoji_grid.py +125 -0
- gitpane/ui/emoji/emoji_window.py +154 -0
- gitpane/ui/emoji/file_action_icon.py +131 -0
- gitpane/ui/emoji/opentype_family_name.py +182 -0
- gitpane/ui/history_rewrite/__init__.py +0 -0
- gitpane/ui/history_rewrite/details.py +78 -0
- gitpane/ui/history_rewrite/preview.py +565 -0
- gitpane/ui/history_rewrite/preview_model.py +291 -0
- gitpane/ui/history_rewrite/question.py +87 -0
- gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
- gitpane/ui/look/__init__.py +0 -0
- gitpane/ui/look/action_icon.py +504 -0
- gitpane/ui/look/application_icon.py +78 -0
- gitpane/ui/look/application_style.py +317 -0
- gitpane/ui/look/avatar_label.py +53 -0
- gitpane/ui/look/color_contrast.py +43 -0
- gitpane/ui/look/desktop_appearance_follower.py +118 -0
- gitpane/ui/look/fixed_font.py +21 -0
- gitpane/ui/look/gtk_settings.py +116 -0
- gitpane/ui/look/icon_density.py +16 -0
- gitpane/ui/look/reference_font.py +19 -0
- gitpane/ui/look/theme_color.py +63 -0
- gitpane/ui/look/theme_look.py +95 -0
- gitpane/ui/look/theme_palette.py +173 -0
- gitpane/ui/look/warning_icon.py +86 -0
- gitpane/ui/look/windows_control_look.py +250 -0
- gitpane/ui/reader/__init__.py +0 -0
- gitpane/ui/reader/commit_message.py +129 -0
- gitpane/ui/reader/commit_panel.py +378 -0
- gitpane/ui/reader/diff_panel.py +626 -0
- gitpane/ui/reader/file_action.py +138 -0
- gitpane/ui/reader/file_tree_panel.py +235 -0
- gitpane/ui/reader/reader_toolbar.py +56 -0
- gitpane/ui/reader/revision_tree.py +80 -0
- gitpane/ui/reader/search_result.py +165 -0
- gitpane/ui/reader/source_text.py +134 -0
- gitpane/ui/reader/source_view.py +151 -0
- gitpane/ui/reader/syntax_span.py +67 -0
- gitpane/ui/reference/__init__.py +0 -0
- gitpane/ui/reference/branch_selector.py +106 -0
- gitpane/ui/reference/checkout_dialog.py +419 -0
- gitpane/ui/reference/checkout_revision_dialog.py +182 -0
- gitpane/ui/reference/commit_summary.py +339 -0
- gitpane/ui/reference/create_branch_dialog.py +346 -0
- gitpane/ui/reference/create_tag_dialog.py +283 -0
- gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
- gitpane/ui/reference/delete_tag_dialog.py +394 -0
- gitpane/ui/reference/form_opening.py +53 -0
- gitpane/ui/reference/reference_dialog.py +417 -0
- gitpane/ui/reference/reference_feedback.py +62 -0
- gitpane/ui/reference/reference_form.py +139 -0
- gitpane/ui/reference/rename_branch_dialog.py +179 -0
- gitpane/ui/reference/reset_branch_dialog.py +430 -0
- gitpane/ui/reference/reset_dialog.py +263 -0
- gitpane/ui/reference/revision_dialog.py +121 -0
- gitpane/ui/reference/revision_picker.py +227 -0
- gitpane/ui/reference/select_branches_dialog.py +199 -0
- gitpane/ui/reference/stash_dialog.py +171 -0
- gitpane/ui/repository/__init__.py +0 -0
- gitpane/ui/repository/graph_delegate.py +298 -0
- gitpane/ui/repository/history_avatar.py +130 -0
- gitpane/ui/repository/history_model.py +179 -0
- gitpane/ui/repository/repository_list_popup.py +319 -0
- gitpane/ui/repository/repository_window.py +2580 -0
- gitpane/ui/resource/ArrowDown.png +0 -0
- gitpane/ui/resource/ArrowUp.png +0 -0
- gitpane/ui/resource/BranchCheckout.png +0 -0
- gitpane/ui/resource/BranchCreate.png +0 -0
- gitpane/ui/resource/BranchDelete.png +0 -0
- gitpane/ui/resource/CopyToClipboard.png +0 -0
- gitpane/ui/resource/GitPane-16.png +0 -0
- gitpane/ui/resource/GitPane-24.png +0 -0
- gitpane/ui/resource/GitPane-256.png +0 -0
- gitpane/ui/resource/GitPane-32.png +0 -0
- gitpane/ui/resource/GitPane-48.png +0 -0
- gitpane/ui/resource/GitPane-64.png +0 -0
- gitpane/ui/resource/GitPane.ico +0 -0
- gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
- gitpane/ui/resource/Renamed.png +0 -0
- gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
- gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
- gitpane/ui/resource/Stash.png +0 -0
- gitpane/ui/resource/Tag.png +0 -0
- gitpane/ui/resource/TagCreate.png +0 -0
- gitpane/ui/resource/TagDelete.png +0 -0
- gitpane/ui/resource/Unmerged.png +0 -0
- gitpane/ui/resource/checkout.png +0 -0
- gitpane/ui/sequencer/__init__.py +0 -0
- gitpane/ui/sequencer/conflict_dialog.py +532 -0
- gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
- gitpane/ui/sequencer/rebase_dialog.py +461 -0
- gitpane/ui/sequencer/rebase_editor_window.py +220 -0
- gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
- gitpane/ui/sequencer/replay_dialog.py +501 -0
- gitpane/ui/setting/__init__.py +0 -0
- gitpane/ui/setting/account_page.py +724 -0
- gitpane/ui/setting/add_account_dialog.py +1005 -0
- gitpane/ui/setting/credential_manager_dialog.py +151 -0
- gitpane/ui/setting/git_setting_widget.py +388 -0
- gitpane/ui/setting/setting_block.py +19 -0
- gitpane/ui/setting/setting_dialog.py +750 -0
- gitpane/ui/setting/ssh_widget.py +232 -0
- gitpane/ui/setting/utility_page.py +240 -0
- gitpane/ui/staging/__init__.py +0 -0
- gitpane/ui/staging/staging_action.py +356 -0
- gitpane/ui/staging/staging_window.py +745 -0
- gitpane/ui/transport/__init__.py +0 -0
- gitpane/ui/transport/clone_dialog.py +879 -0
- gitpane/ui/transport/push_dialog.py +580 -0
- gitpane/ui/transport/push_rejection_prompt.py +100 -0
- gitpane/ui/transport/push_tracking_question.py +88 -0
- gitpane/ui/transport/remote_dialog.py +943 -0
- gitpane/ui/welcome/__init__.py +0 -0
- gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
- gitpane/ui/welcome/welcome_window.py +1137 -0
- gitpane/ui/widget/__init__.py +0 -0
- gitpane/ui/widget/busy_indicator.py +106 -0
- gitpane/ui/widget/commit_date_text.py +55 -0
- gitpane/ui/widget/content_growth.py +262 -0
- gitpane/ui/widget/content_measure.py +99 -0
- gitpane/ui/widget/dialog_layout.py +668 -0
- gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
- gitpane/ui/widget/flow_layout.py +110 -0
- gitpane/ui/widget/long_text.py +542 -0
- gitpane/ui/widget/mnemonic.py +13 -0
- gitpane/ui/widget/numbered_text_edit.py +127 -0
- gitpane/ui/widget/plain_label.py +15 -0
- gitpane/ui/widget/plain_tooltip.py +19 -0
- gitpane/ui/widget/refresh_advice.py +25 -0
- gitpane/ui/widget/result_label.py +227 -0
- gitpane/ui/widget/result_status_bar.py +53 -0
- gitpane/ui/widget/scrolled_text.py +69 -0
- gitpane/ui/widget/shortcut_text.py +31 -0
- gitpane/ui/widget/standard_icon_label.py +55 -0
- gitpane/ui/widget/system_shortcut.py +75 -0
- gitpane/ui/widget/unavailable_reason.py +29 -0
- gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
- gitpane/ui/window_control/__init__.py +0 -0
- gitpane/ui/window_control/button_focus.py +42 -0
- gitpane/ui/window_control/compositor_answer.py +152 -0
- gitpane/ui/window_control/compositor_growth_cap.py +73 -0
- gitpane/ui/window_control/desktop_activation.py +203 -0
- gitpane/ui/window_control/dialog_modality.py +122 -0
- gitpane/ui/window_control/item_activation.py +42 -0
- gitpane/ui/window_control/modal_focus_relay.py +124 -0
- gitpane/ui/window_control/wayland_activation.py +695 -0
- gitpane/ui/window_control/window_placement.py +305 -0
- gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane/vendor/w32.exe +0 -0
- gitpane/vendor/w64-arm.exe +0 -0
- gitpane/vendor/w64.exe +0 -0
- gitpane/worktree/__init__.py +0 -0
- gitpane/worktree/command_sequence.py +110 -0
- gitpane/worktree/file_action_target.py +61 -0
- gitpane/worktree/file_menu_action.py +52 -0
- gitpane/worktree/historical_restore.py +361 -0
- gitpane/worktree/ignore_rule.py +266 -0
- gitpane/worktree/mutation.py +1539 -0
- gitpane/worktree/partial_patch.py +435 -0
- gitpane/worktree/staged_reset.py +447 -0
- gitpane/worktree/staging.py +1418 -0
- gitpane/worktree/trash.py +302 -0
- gitpane/worktree/worktree_file.py +406 -0
- gitpane/worktree/worktree_service.py +584 -0
- gitpane/worktree/worktree_snapshot.py +630 -0
- gitpane-1.0.0.dist-info/METADATA +141 -0
- gitpane-1.0.0.dist-info/RECORD +353 -0
- gitpane-1.0.0.dist-info/WHEEL +5 -0
- gitpane-1.0.0.dist-info/entry_points.txt +2 -0
- gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- 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])
|