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,390 @@
|
|
|
1
|
+
"""Detect the terminals this machine offers, from known recipes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import QObject, Signal
|
|
15
|
+
|
|
16
|
+
from gitpane.config import LINUX_PLATFORM, MACOS_PLATFORM, SYSTEM_ROOT_ENVIRONMENT_NAME, WINDOWS_PLATFORM
|
|
17
|
+
from gitpane.tool.app_bundle import bundled_program, find_application, read_bundle
|
|
18
|
+
from gitpane.tool.git_executable import GIT_BINARY_DIR_BASENAME, GIT_SHELL_DIR_BASENAME, git_root_dirs
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Callable, Mapping
|
|
22
|
+
|
|
23
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
24
|
+
from gitpane.tool.app_bundle import AppBundle
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TerminalLaunch(Enum):
|
|
28
|
+
"""How a terminal recipe is started once its program or application has been found."""
|
|
29
|
+
|
|
30
|
+
# The recipe's own executable, found on the PATH or in known folders, started detached.
|
|
31
|
+
PROGRAM = "program"
|
|
32
|
+
# A program inside the macOS application found (a known tool, or the declared program), started detached.
|
|
33
|
+
BUNDLE_PROGRAM = "bundle-program"
|
|
34
|
+
# macOS's open command with the recipe arguments: it hands the request to LaunchServices and answers at once.
|
|
35
|
+
OPEN = "open"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class TerminalRecipe:
|
|
40
|
+
"""Describe only documented arguments, preserving registry order for initial choice.
|
|
41
|
+
|
|
42
|
+
On macOS ``executable`` is the application folder name searched in the application folders, and ``bundle_program``
|
|
43
|
+
the path of the program to start inside it (empty: the program the application declares). Arguments may hold the
|
|
44
|
+
markers ``{directory}``, ``{directory_url}`` (the folder encoded for a URL) and ``{bundle}`` (the application folder).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
id: str
|
|
48
|
+
label: str
|
|
49
|
+
platform: str
|
|
50
|
+
executable: str
|
|
51
|
+
arguments: tuple[str, ...]
|
|
52
|
+
new_console: bool = False
|
|
53
|
+
launch: TerminalLaunch = TerminalLaunch.PROGRAM
|
|
54
|
+
bundle_program: tuple[str, ...] = ()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
TERMINAL_RECIPES = (
|
|
58
|
+
# Terminal.app ships with macOS, so it always comes first and is the default choice whatever else is installed.
|
|
59
|
+
# open -a gives it the folder, in which it opens a new window; no AppleScript, so no permission prompt.
|
|
60
|
+
TerminalRecipe("terminal-macos", "Terminal", MACOS_PLATFORM, "Terminal.app", ("-a", "{bundle}", "{directory}"), launch=TerminalLaunch.OPEN),
|
|
61
|
+
# Third-party macOS terminals, offered only when installed, in this fixed order. Their ids are saved in the
|
|
62
|
+
# preferences and must never be renamed.
|
|
63
|
+
# iTerm2 opens a window in the folder it is given through open -a, like Terminal.
|
|
64
|
+
TerminalRecipe("iterm2", "iTerm2", MACOS_PLATFORM, "iTerm.app", ("-a", "{bundle}", "{directory}"), launch=TerminalLaunch.OPEN),
|
|
65
|
+
# Ghostty documents --working-directory. Started by open it defaults to the home folder, and an already running
|
|
66
|
+
# Ghostty would apply its own inherited folder, so -n starts a new instance that reads the option.
|
|
67
|
+
TerminalRecipe("ghostty", "Ghostty", MACOS_PLATFORM, "Ghostty.app", ("-na", "{bundle}", "--args", "--working-directory={directory}"), launch=TerminalLaunch.OPEN),
|
|
68
|
+
# kitty's documentation runs the program inside its application for command line options, which open would not
|
|
69
|
+
# pass; --directory changes to the folder at launch.
|
|
70
|
+
TerminalRecipe("kitty", "kitty", MACOS_PLATFORM, "kitty.app", ("--directory", "{directory}"), launch=TerminalLaunch.BUNDLE_PROGRAM,
|
|
71
|
+
bundle_program=("Contents", "MacOS", "kitty")),
|
|
72
|
+
# Alacritty's declared program accepts --working-directory, documented as the folder the shell starts in.
|
|
73
|
+
TerminalRecipe("alacritty", "Alacritty", MACOS_PLATFORM, "Alacritty.app", ("--working-directory", "{directory}"), launch=TerminalLaunch.BUNDLE_PROGRAM),
|
|
74
|
+
# The declared program of WezTerm is its interface; its command line tool, wezterm, documents start --cwd.
|
|
75
|
+
TerminalRecipe("wezterm", "WezTerm", MACOS_PLATFORM, "WezTerm.app", ("start", "--cwd", "{directory}"), launch=TerminalLaunch.BUNDLE_PROGRAM,
|
|
76
|
+
bundle_program=("Contents", "MacOS", "wezterm")),
|
|
77
|
+
# Warp documents a link that opens a window in a folder; the folder is encoded so that &, # or ? in its name
|
|
78
|
+
# never become parameters of the link.
|
|
79
|
+
TerminalRecipe("warp", "Warp", MACOS_PLATFORM, "Warp.app", ("warp://action/new_window?path={directory_url}",), launch=TerminalLaunch.OPEN),
|
|
80
|
+
TerminalRecipe("wt", "Windows Terminal", WINDOWS_PLATFORM, "wt.exe", ("new-tab", "-d", "{directory}")),
|
|
81
|
+
TerminalRecipe("pwsh", "PowerShell", WINDOWS_PLATFORM, "pwsh.exe", ("-NoExit",), True),
|
|
82
|
+
TerminalRecipe("powershell", "Windows PowerShell", WINDOWS_PLATFORM, "powershell.exe", ("-NoExit",), True),
|
|
83
|
+
TerminalRecipe("cmd", "Command Prompt", WINDOWS_PLATFORM, "cmd.exe", ("/D", "/K"), True),
|
|
84
|
+
TerminalRecipe("ptyxis", "Ptyxis", LINUX_PLATFORM, "ptyxis", ("--new-window", "--working-directory", "{directory}")),
|
|
85
|
+
TerminalRecipe("gnome-terminal", "GNOME Terminal", LINUX_PLATFORM, "gnome-terminal", ("--working-directory", "{directory}")),
|
|
86
|
+
TerminalRecipe("konsole", "Konsole", LINUX_PLATFORM, "konsole", ("--workdir", "{directory}")),
|
|
87
|
+
TerminalRecipe("xfce4-terminal", "Xfce Terminal", LINUX_PLATFORM, "xfce4-terminal", ("--working-directory={directory}",)),
|
|
88
|
+
TerminalRecipe("xterm", "XTerm", LINUX_PLATFORM, "xterm", ()),
|
|
89
|
+
)
|
|
90
|
+
# Stable id saved in the preferences for the Git Bash entry, whichever of its three executables was found.
|
|
91
|
+
GIT_BASH_TERMINAL_ID = "git-bash"
|
|
92
|
+
GIT_BASH_LABEL = "Git Bash"
|
|
93
|
+
# Git for Windows ships a windowed launcher; bash and sh are the console programs Git Extensions falls back to.
|
|
94
|
+
GIT_BASH_EXECUTABLE_BASENAME = "git-bash.exe"
|
|
95
|
+
BASH_EXECUTABLE_BASENAME = "bash.exe"
|
|
96
|
+
SH_EXECUTABLE_BASENAME = "sh.exe"
|
|
97
|
+
# Folder name of a Git for Windows installation under Program Files.
|
|
98
|
+
GIT_INSTALLATION_DIR_BASENAME = "Git"
|
|
99
|
+
NATIVE_PROGRAM_FILES_ENVIRONMENT_NAME = "ProgramW6432"
|
|
100
|
+
X86_PROGRAM_FILES_ENVIRONMENT_NAME = "ProgramFiles(x86)"
|
|
101
|
+
# The launcher is a windowed program and opens its own window; bash.exe and sh.exe need a new console. Started bare,
|
|
102
|
+
# usr\bin\bash.exe skips the Git for Windows profile and has neither ls nor git on its PATH; a login interactive
|
|
103
|
+
# shell reads that profile, as git-bash.exe does, and keeps the folder it was started in.
|
|
104
|
+
GIT_BASH_SHELL_ARGUMENTS = ("--login", "-i")
|
|
105
|
+
GIT_BASH_RECIPES = (
|
|
106
|
+
TerminalRecipe(GIT_BASH_TERMINAL_ID, GIT_BASH_LABEL, WINDOWS_PLATFORM, GIT_BASH_EXECUTABLE_BASENAME, ()),
|
|
107
|
+
TerminalRecipe("git-bash-bash", GIT_BASH_LABEL, WINDOWS_PLATFORM, BASH_EXECUTABLE_BASENAME, GIT_BASH_SHELL_ARGUMENTS, True),
|
|
108
|
+
TerminalRecipe("git-bash-sh", GIT_BASH_LABEL, WINDOWS_PLATFORM, SH_EXECUTABLE_BASENAME, GIT_BASH_SHELL_ARGUMENTS, True),
|
|
109
|
+
)
|
|
110
|
+
RECIPE_BY_ID: dict[str, TerminalRecipe] = {}
|
|
111
|
+
# Git Bash recipes are launchable by id but stay out of TERMINAL_RECIPES: they have their own search,
|
|
112
|
+
# and the generic loop only knows the installation folders of the other Windows terminals.
|
|
113
|
+
for recipe in (*TERMINAL_RECIPES, *GIT_BASH_RECIPES):
|
|
114
|
+
RECIPE_BY_ID[recipe.id] = recipe
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class TerminalDescriptor:
|
|
119
|
+
"""Keep detected and formerly selected terminals identifiable without substituting tools."""
|
|
120
|
+
|
|
121
|
+
id: str
|
|
122
|
+
label: str
|
|
123
|
+
program_filename: str
|
|
124
|
+
recipe_id: str
|
|
125
|
+
available: bool = True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TerminalRegistry(QObject):
|
|
129
|
+
"""Publish one persisted terminal choice to every repository window."""
|
|
130
|
+
|
|
131
|
+
changed = Signal()
|
|
132
|
+
|
|
133
|
+
def __init__(self, preference: PreferenceStore, detector: Callable[[], list[TerminalDescriptor]] | None = None, parent: QObject | None = None) -> None:
|
|
134
|
+
"""Inject detection and retain useful names when a selected executable disappears."""
|
|
135
|
+
super().__init__(parent)
|
|
136
|
+
self.preference = preference
|
|
137
|
+
self.detector = detector if detector is not None else detect_terminal
|
|
138
|
+
self.detected_terminals: list[TerminalDescriptor] = []
|
|
139
|
+
self.terminals: list[TerminalDescriptor] = []
|
|
140
|
+
self.selected_id = ""
|
|
141
|
+
self.warning = ""
|
|
142
|
+
preference.changed.connect(self._preference_changed)
|
|
143
|
+
self.redetect()
|
|
144
|
+
|
|
145
|
+
def redetect(self) -> None:
|
|
146
|
+
"""Reinventory known recipes without silently replacing a saved missing terminal."""
|
|
147
|
+
self.detected_terminals = self.detector()
|
|
148
|
+
self.preference.refresh()
|
|
149
|
+
self.warning = ""
|
|
150
|
+
if not self.preference.value_by_key["terminal_id"] and self.detected_terminals:
|
|
151
|
+
first_terminal_index = 0
|
|
152
|
+
try:
|
|
153
|
+
self.select(self.detected_terminals[first_terminal_index].id)
|
|
154
|
+
except OSError as error:
|
|
155
|
+
self.warning = str(error)
|
|
156
|
+
self._rebuild()
|
|
157
|
+
|
|
158
|
+
def select(self, terminal_id: str) -> None:
|
|
159
|
+
"""Persist the complete selection atomically before announcing it to other windows."""
|
|
160
|
+
for terminal in self.detected_terminals:
|
|
161
|
+
if terminal.id == terminal_id and terminal.available:
|
|
162
|
+
self.preference.update({"terminal_id": terminal.id, "terminal_label": terminal.label, "terminal_filename": terminal.program_filename})
|
|
163
|
+
self.warning = ""
|
|
164
|
+
self._rebuild()
|
|
165
|
+
return
|
|
166
|
+
raise OSError("This terminal is unavailable. Detect terminals again.")
|
|
167
|
+
|
|
168
|
+
def selected(self) -> TerminalDescriptor | None:
|
|
169
|
+
"""Return exactly the shared choice, including its unavailable representation."""
|
|
170
|
+
for terminal in self.terminals:
|
|
171
|
+
if terminal.id == self.selected_id:
|
|
172
|
+
return terminal
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _preference_changed(self, changed_value_by_key: dict[str, object]) -> None:
|
|
176
|
+
if any(key.startswith("terminal_") for key in changed_value_by_key):
|
|
177
|
+
self._rebuild()
|
|
178
|
+
|
|
179
|
+
def _rebuild(self) -> None:
|
|
180
|
+
"""Combine the inventory with saved identity without constructing executable commands."""
|
|
181
|
+
self.terminals = list(self.detected_terminals)
|
|
182
|
+
self.selected_id = str(self.preference.value_by_key["terminal_id"])
|
|
183
|
+
if self.selected_id and not any(terminal.id == self.selected_id for terminal in self.terminals):
|
|
184
|
+
recipe = RECIPE_BY_ID.get(self.selected_id)
|
|
185
|
+
label = str(self.preference.value_by_key["terminal_label"]) or (recipe.label if recipe else self.selected_id)
|
|
186
|
+
program_filename = str(self.preference.value_by_key["terminal_filename"])
|
|
187
|
+
self.terminals.append(TerminalDescriptor(self.selected_id, label, program_filename, self.selected_id, False))
|
|
188
|
+
self.changed.emit()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def executable_available(program_filename: str) -> bool:
|
|
192
|
+
"""Check a concrete executable without evaluating a shell command.
|
|
193
|
+
|
|
194
|
+
A relative name is accepted and read from the current folder; the SSH and credential searches use
|
|
195
|
+
``absolute_executable_available`` of ``gitpane.account.ssh`` instead, which refuses one.
|
|
196
|
+
"""
|
|
197
|
+
return bool(program_filename) and os.path.isfile(program_filename) and os.access(program_filename, os.X_OK)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _windows_candidate_filenames(recipe: TerminalRecipe, environment_by_name: Mapping[str, str]) -> list[str]:
|
|
201
|
+
"""Use known installation directories only when the corresponding environment exists."""
|
|
202
|
+
location_by_id = {
|
|
203
|
+
"wt": ("LOCALAPPDATA", "Microsoft", "WindowsApps"),
|
|
204
|
+
"pwsh": ("ProgramFiles", "PowerShell", "7"),
|
|
205
|
+
"powershell": (SYSTEM_ROOT_ENVIRONMENT_NAME, "System32", "WindowsPowerShell", "v1.0"),
|
|
206
|
+
"cmd": (SYSTEM_ROOT_ENVIRONMENT_NAME, "System32"),
|
|
207
|
+
}
|
|
208
|
+
environment_name, *directory_parts = location_by_id[recipe.id]
|
|
209
|
+
root_dir = environment_by_name.get(environment_name, "")
|
|
210
|
+
if not root_dir:
|
|
211
|
+
return []
|
|
212
|
+
return [os.path.join(root_dir, *directory_parts, recipe.executable)]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def git_bash_candidate_filenames(executable: str, git_filename: str, git_utility_dir: str, which: Callable[[str], str | None],
|
|
216
|
+
environment_by_name: Mapping[str, str]) -> list[str]:
|
|
217
|
+
"""List where one Git Bash executable may be, in search order, starting beside the Git GitPane runs.
|
|
218
|
+
|
|
219
|
+
After the Git in use comes the order of Git Extensions: both Program Files, the Git utilities folder, the Linux
|
|
220
|
+
tool folders of the installation, then the PATH. The bin folder of the installation, whose bash.exe sets up the
|
|
221
|
+
Git for Windows environment itself, is looked at before the Linux tool folders. An environment variable that is
|
|
222
|
+
absent or empty adds nothing.
|
|
223
|
+
"""
|
|
224
|
+
root_dirs = git_root_dirs(git_filename)
|
|
225
|
+
installation_dirs = []
|
|
226
|
+
for environment_name in (NATIVE_PROGRAM_FILES_ENVIRONMENT_NAME, X86_PROGRAM_FILES_ENVIRONMENT_NAME):
|
|
227
|
+
program_files_dir = environment_by_name.get(environment_name, "")
|
|
228
|
+
if program_files_dir:
|
|
229
|
+
installation_dirs.append(os.path.join(program_files_dir, GIT_INSTALLATION_DIR_BASENAME))
|
|
230
|
+
candidate_filenames = []
|
|
231
|
+
for root_dir in root_dirs:
|
|
232
|
+
candidate_filenames.append(os.path.join(root_dir, executable))
|
|
233
|
+
for installation_dir in installation_dirs:
|
|
234
|
+
candidate_filenames.append(os.path.join(installation_dir, executable))
|
|
235
|
+
if git_utility_dir:
|
|
236
|
+
candidate_filenames.append(os.path.join(git_utility_dir, executable))
|
|
237
|
+
native_program_files_dir = environment_by_name.get(NATIVE_PROGRAM_FILES_ENVIRONMENT_NAME, "")
|
|
238
|
+
for tool_dir_parts in ((GIT_BINARY_DIR_BASENAME,), (GIT_SHELL_DIR_BASENAME, GIT_BINARY_DIR_BASENAME)):
|
|
239
|
+
for root_dir in root_dirs:
|
|
240
|
+
candidate_filenames.append(os.path.join(root_dir, *tool_dir_parts, executable))
|
|
241
|
+
if native_program_files_dir:
|
|
242
|
+
candidate_filenames.append(os.path.join(native_program_files_dir, GIT_INSTALLATION_DIR_BASENAME, *tool_dir_parts, executable))
|
|
243
|
+
path_filename = which(executable)
|
|
244
|
+
if path_filename:
|
|
245
|
+
candidate_filenames.append(path_filename)
|
|
246
|
+
return candidate_filenames
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def inside_system_root(filename: str, environment_by_name: Mapping[str, str]) -> bool:
|
|
250
|
+
"""Tell whether a file lies under the Windows folder, where only system programs live."""
|
|
251
|
+
system_root_dir = environment_by_name.get(SYSTEM_ROOT_ENVIRONMENT_NAME, "")
|
|
252
|
+
if not system_root_dir:
|
|
253
|
+
return False
|
|
254
|
+
normalized_filename = os.path.normcase(os.path.abspath(filename))
|
|
255
|
+
normalized_system_root_dir = os.path.normcase(os.path.abspath(system_root_dir))
|
|
256
|
+
try:
|
|
257
|
+
return os.path.commonpath((normalized_filename, normalized_system_root_dir)) == normalized_system_root_dir
|
|
258
|
+
except ValueError:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _detect_git_bash(git_filename: str, git_utility_dir: str, which: Callable[[str], str | None], available: Callable[[str], bool],
|
|
263
|
+
environment_by_name: Mapping[str, str]) -> TerminalDescriptor | None:
|
|
264
|
+
"""Find Git Bash the way Git Extensions does: git-bash.exe first, then bash.exe, then sh.exe.
|
|
265
|
+
|
|
266
|
+
All three forms share one saved id so the user's choice survives a fallback; only the launch recipe differs.
|
|
267
|
+
"""
|
|
268
|
+
for recipe in GIT_BASH_RECIPES:
|
|
269
|
+
for candidate_filename in git_bash_candidate_filenames(recipe.executable, git_filename, git_utility_dir, which, environment_by_name):
|
|
270
|
+
# The WSL launcher in System32 is also named bash.exe; taking it would open another machine, not Git Bash.
|
|
271
|
+
if inside_system_root(candidate_filename, environment_by_name):
|
|
272
|
+
continue
|
|
273
|
+
if available(candidate_filename):
|
|
274
|
+
return TerminalDescriptor(GIT_BASH_TERMINAL_ID, GIT_BASH_LABEL, os.path.abspath(candidate_filename), recipe.id)
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def bundle_recipe_program(recipe: TerminalRecipe, bundle_dir: str) -> str:
|
|
279
|
+
"""Return the program a bundle recipe starts inside ``bundle_dir``, or ``""`` when the application does not hold it.
|
|
280
|
+
|
|
281
|
+
It is the known tool the recipe names, or else the program the application declares in its Info.plist; either way
|
|
282
|
+
only an executable file inside the application folder comes out.
|
|
283
|
+
"""
|
|
284
|
+
if recipe.bundle_program:
|
|
285
|
+
return bundled_program(bundle_dir, recipe.bundle_program)
|
|
286
|
+
bundle = read_bundle(bundle_dir)
|
|
287
|
+
if bundle is None:
|
|
288
|
+
return ""
|
|
289
|
+
return bundle.executable_filename
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _detect_macos_terminals(find_bundle: Callable[[tuple[str, ...]], AppBundle | None]) -> list[TerminalDescriptor]:
|
|
293
|
+
"""List the installed macOS terminals in the order of the recipe table, each named by its application folder.
|
|
294
|
+
|
|
295
|
+
An application is offered once, from the first application folder that holds it; a recipe that starts a program
|
|
296
|
+
inside the application is only offered when that program is there.
|
|
297
|
+
"""
|
|
298
|
+
terminals = []
|
|
299
|
+
bundle_identity_set: set[str] = set()
|
|
300
|
+
for recipe in TERMINAL_RECIPES:
|
|
301
|
+
if recipe.platform != MACOS_PLATFORM:
|
|
302
|
+
continue
|
|
303
|
+
bundle = find_bundle((recipe.executable,))
|
|
304
|
+
if bundle is None:
|
|
305
|
+
continue
|
|
306
|
+
if recipe.launch == TerminalLaunch.BUNDLE_PROGRAM and not bundle_recipe_program(recipe, bundle.bundle_dir):
|
|
307
|
+
continue
|
|
308
|
+
bundle_identity = os.path.normcase(bundle.bundle_dir)
|
|
309
|
+
if bundle_identity in bundle_identity_set:
|
|
310
|
+
continue
|
|
311
|
+
bundle_identity_set.add(bundle_identity)
|
|
312
|
+
terminals.append(TerminalDescriptor(recipe.id, recipe.label, bundle.bundle_dir, recipe.id))
|
|
313
|
+
return terminals
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def detect_terminal(platform: str = sys.platform, which: Callable[[str], str | None] = shutil.which,
|
|
317
|
+
realpath: Callable[[str], str] = os.path.realpath, available: Callable[[str], bool] = executable_available,
|
|
318
|
+
environment_by_name: Mapping[str, str] | None = None, git_filename: str = "", git_utility_dir: str = "",
|
|
319
|
+
find_bundle: Callable[[tuple[str, ...]], AppBundle | None] = find_application) -> list[TerminalDescriptor]:
|
|
320
|
+
"""Inventory recognized executables with a stable order and a recognized Linux alternative first.
|
|
321
|
+
|
|
322
|
+
On Windows, Git Bash comes first, searched from the Git GitPane runs (git_filename) and the Git utilities
|
|
323
|
+
directory before the usual installation folders and the PATH. On macOS terminals are applications, looked up by
|
|
324
|
+
their folder name in the application folders (find_bundle), with Terminal first.
|
|
325
|
+
"""
|
|
326
|
+
if platform == MACOS_PLATFORM:
|
|
327
|
+
return _detect_macos_terminals(find_bundle)
|
|
328
|
+
environment_by_name = os.environ if environment_by_name is None else environment_by_name
|
|
329
|
+
terminals = []
|
|
330
|
+
program_identity_set: set[str] = set()
|
|
331
|
+
recipe_id_set: set[str] = set()
|
|
332
|
+
if platform == WINDOWS_PLATFORM:
|
|
333
|
+
git_bash = _detect_git_bash(git_filename, git_utility_dir, which, available, environment_by_name)
|
|
334
|
+
if git_bash is not None:
|
|
335
|
+
terminals.append(git_bash)
|
|
336
|
+
program_identity_set.add(os.path.normcase(git_bash.program_filename))
|
|
337
|
+
if platform == LINUX_PLATFORM:
|
|
338
|
+
alternative_filename = which("x-terminal-emulator")
|
|
339
|
+
if alternative_filename:
|
|
340
|
+
target_filename = realpath(alternative_filename)
|
|
341
|
+
for recipe in TERMINAL_RECIPES:
|
|
342
|
+
if recipe.platform == platform and os.path.basename(target_filename) == recipe.executable and available(target_filename):
|
|
343
|
+
terminals.append(TerminalDescriptor(recipe.id, recipe.label, target_filename, recipe.id))
|
|
344
|
+
program_identity_set.add(os.path.normcase(target_filename))
|
|
345
|
+
recipe_id_set.add(recipe.id)
|
|
346
|
+
break
|
|
347
|
+
for recipe in TERMINAL_RECIPES:
|
|
348
|
+
if recipe.platform != platform or recipe.id in recipe_id_set:
|
|
349
|
+
continue
|
|
350
|
+
program_filename = which(recipe.executable)
|
|
351
|
+
candidate_filenames = [program_filename] if program_filename else []
|
|
352
|
+
if platform == WINDOWS_PLATFORM:
|
|
353
|
+
candidate_filenames.extend(_windows_candidate_filenames(recipe, environment_by_name))
|
|
354
|
+
for candidate_filename in candidate_filenames:
|
|
355
|
+
if not available(candidate_filename):
|
|
356
|
+
continue
|
|
357
|
+
canonical_filename = os.path.abspath(candidate_filename) if platform == WINDOWS_PLATFORM else realpath(candidate_filename)
|
|
358
|
+
program_identity = os.path.normcase(canonical_filename)
|
|
359
|
+
if program_identity not in program_identity_set:
|
|
360
|
+
terminals.append(TerminalDescriptor(recipe.id, recipe.label, canonical_filename, recipe.id))
|
|
361
|
+
program_identity_set.add(program_identity)
|
|
362
|
+
break
|
|
363
|
+
return terminals
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Markers a recipe argument may hold, each replaced by one value in a single pass, so a folder name that happens to
|
|
367
|
+
# contain a marker's text is never expanded a second time.
|
|
368
|
+
TERMINAL_ARGUMENT_MARKER_PATTERN = re.compile(r"\{(directory|directory_url|bundle)\}")
|
|
369
|
+
# Characters a folder path keeps unencoded inside a URL: only the separator, so &, #, ? and spaces never become syntax.
|
|
370
|
+
URL_PATH_SAFE_CHARACTERS = "/"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def terminal_arguments(descriptor: TerminalDescriptor, repository_dir: str) -> list[str]:
|
|
374
|
+
"""Expand the directory as a single argument, never as executable shell text.
|
|
375
|
+
|
|
376
|
+
The application folder (a macOS terminal's descriptor names it) and the folder encoded for a URL are expanded the
|
|
377
|
+
same way, one element per argument.
|
|
378
|
+
"""
|
|
379
|
+
recipe = RECIPE_BY_ID.get(descriptor.recipe_id)
|
|
380
|
+
if recipe is None:
|
|
381
|
+
raise ValueError(f"Unknown terminal recipe: {descriptor.recipe_id}")
|
|
382
|
+
value_by_marker = {
|
|
383
|
+
"directory": repository_dir,
|
|
384
|
+
"directory_url": urllib.parse.quote(repository_dir, safe=URL_PATH_SAFE_CHARACTERS),
|
|
385
|
+
"bundle": descriptor.program_filename,
|
|
386
|
+
}
|
|
387
|
+
arguments = []
|
|
388
|
+
for argument in recipe.arguments:
|
|
389
|
+
arguments.append(TERMINAL_ARGUMENT_MARKER_PATTERN.sub(lambda match: value_by_marker[match.group(1)], argument))
|
|
390
|
+
return arguments
|
|
File without changes
|