pcd-cli 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/PKG-INFO +10 -3
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/README.txt +5 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/pyproject.toml +6 -4
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/catalog.py +1 -1
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/__init__.py +2 -4
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/app.py +9 -7
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/config.py +6 -1
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/shell.py +38 -35
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/filesystem.py +2 -15
- pcd_cli-0.2.4/src/pcd_cli/integrations/__init__.py +1 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shell.py +146 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/__init__.py +1 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/cmd.py +176 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/cmd_autorun.py +78 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/common.py +27 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/powershell.py +125 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/registry.py +37 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/startup_file.py +114 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/unix.py +115 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/windows_registry.py +74 -0
- pcd_cli-0.2.4/src/pcd_cli/integrations/shells/windows_shell.py +30 -0
- pcd_cli-0.2.4/src/pcd_cli/locking/__init__.py +33 -0
- pcd_cli-0.2.4/src/pcd_cli/locking/unix.py +27 -0
- pcd_cli-0.2.4/src/pcd_cli/locking/windows.py +39 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/navigation.py +6 -3
- pcd_cli-0.2.2/src/pcd_cli/shell_integration.py +0 -298
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/LICENSE +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/__init__.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cache.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/projects.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/roots.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/config.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/history.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/models.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/picker.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/scanner.py +0 -0
- {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/search.py +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pcd-cli
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Fast project navigator for Bash, Zsh and
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Fast project navigator for Bash, Zsh, Fish, PowerShell and CMD
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Keywords: cli,cd,projects,navigation,bash,zsh,fish
|
|
7
|
+
Keywords: cli,cd,projects,navigation,bash,zsh,fish,powershell,cmd
|
|
8
8
|
Author: pcd-cli contributors
|
|
9
9
|
Requires-Python: >=3.12,<4
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Operating System :: MacOS
|
|
13
13
|
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -19,6 +20,7 @@ Classifier: Topic :: Utilities
|
|
|
19
20
|
Requires-Dist: click (>=8.5.0,<9)
|
|
20
21
|
Requires-Dist: platformdirs (>=4.11.7,<5)
|
|
21
22
|
Requires-Dist: prompt-toolkit (>=3.0.53,<4)
|
|
23
|
+
Requires-Dist: shellingham (>=1.5.4,<2) ; sys_platform == "win32"
|
|
22
24
|
Requires-Dist: tomlkit (>=0.15.1,<1)
|
|
23
25
|
Description-Content-Type: text/plain
|
|
24
26
|
|
|
@@ -36,6 +38,10 @@ Supported shells:
|
|
|
36
38
|
* Bash
|
|
37
39
|
* Zsh
|
|
38
40
|
* Fish
|
|
41
|
+
* PowerShell 7+
|
|
42
|
+
* Windows PowerShell 5.1
|
|
43
|
+
* Command Prompt (cmd)
|
|
44
|
+
* Git Bash on Windows
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
INSTALL
|
|
@@ -76,6 +82,7 @@ COMMANDS
|
|
|
76
82
|
pcd config validate Validate the user configuration
|
|
77
83
|
|
|
78
84
|
pcd shell install Install shell integration
|
|
85
|
+
pcd shell init <shell> Print shell integration code
|
|
79
86
|
pcd shell status Show shell integration status
|
|
80
87
|
pcd shell uninstall Remove shell integration
|
|
81
88
|
|
|
@@ -12,6 +12,10 @@ Supported shells:
|
|
|
12
12
|
* Bash
|
|
13
13
|
* Zsh
|
|
14
14
|
* Fish
|
|
15
|
+
* PowerShell 7+
|
|
16
|
+
* Windows PowerShell 5.1
|
|
17
|
+
* Command Prompt (cmd)
|
|
18
|
+
* Git Bash on Windows
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
INSTALL
|
|
@@ -52,6 +56,7 @@ COMMANDS
|
|
|
52
56
|
pcd config validate Validate the user configuration
|
|
53
57
|
|
|
54
58
|
pcd shell install Install shell integration
|
|
59
|
+
pcd shell init <shell> Print shell integration code
|
|
55
60
|
pcd shell status Show shell integration status
|
|
56
61
|
pcd shell uninstall Remove shell integration
|
|
57
62
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pcd-cli"
|
|
3
|
-
version = "0.2.
|
|
4
|
-
description = "Fast project navigator for Bash, Zsh and
|
|
3
|
+
version = "0.2.4"
|
|
4
|
+
description = "Fast project navigator for Bash, Zsh, Fish, PowerShell and CMD"
|
|
5
5
|
readme = { file = "README.txt", content-type = "text/plain" }
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
authors = [{ name = "pcd-cli contributors" }]
|
|
9
9
|
requires-python = ">=3.12,<4"
|
|
10
|
-
keywords = ["cli", "cd", "projects", "navigation", "bash", "zsh", "fish"]
|
|
10
|
+
keywords = ["cli", "cd", "projects", "navigation", "bash", "zsh", "fish", "powershell", "cmd"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 3 - Alpha",
|
|
13
13
|
"Environment :: Console",
|
|
14
14
|
"Operating System :: MacOS",
|
|
15
15
|
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Operating System :: Microsoft :: Windows",
|
|
16
17
|
"Programming Language :: Python :: 3 :: Only",
|
|
17
18
|
"Programming Language :: Python :: 3.12",
|
|
18
19
|
"Programming Language :: Python :: 3.13",
|
|
@@ -23,6 +24,7 @@ dependencies = [
|
|
|
23
24
|
"click>=8.5.0,<9",
|
|
24
25
|
"platformdirs>=4.11.7,<5",
|
|
25
26
|
"prompt-toolkit>=3.0.53,<4",
|
|
27
|
+
"shellingham>=1.5.4,<2; sys_platform == 'win32'",
|
|
26
28
|
"tomlkit>=0.15.1,<1",
|
|
27
29
|
]
|
|
28
30
|
|
|
@@ -107,5 +109,5 @@ fail_under = 95
|
|
|
107
109
|
show_missing = true
|
|
108
110
|
skip_covered = true
|
|
109
111
|
exclude_also = [
|
|
110
|
-
"if TYPE_CHECKING
|
|
112
|
+
"if TYPE_CHECKING",
|
|
111
113
|
]
|
|
@@ -9,10 +9,9 @@ from pcd_cli.cli.config import (
|
|
|
9
9
|
from pcd_cli.cli.projects import add, list_projects, project, remove
|
|
10
10
|
from pcd_cli.cli.roots import init, refresh, roots, uninit
|
|
11
11
|
from pcd_cli.cli.shell import (
|
|
12
|
+
init_shell,
|
|
12
13
|
install_shell,
|
|
13
|
-
print_shell_integration,
|
|
14
14
|
shell_commands,
|
|
15
|
-
shell_init,
|
|
16
15
|
shell_status,
|
|
17
16
|
uninstall_shell,
|
|
18
17
|
)
|
|
@@ -24,18 +23,17 @@ __all__ = [
|
|
|
24
23
|
"config_commands",
|
|
25
24
|
"edit_config",
|
|
26
25
|
"init",
|
|
26
|
+
"init_shell",
|
|
27
27
|
"install_shell",
|
|
28
28
|
"list_projects",
|
|
29
29
|
"main",
|
|
30
30
|
"package_version",
|
|
31
31
|
"print_config_path",
|
|
32
|
-
"print_shell_integration",
|
|
33
32
|
"project",
|
|
34
33
|
"refresh",
|
|
35
34
|
"remove",
|
|
36
35
|
"roots",
|
|
37
36
|
"shell_commands",
|
|
38
|
-
"shell_init",
|
|
39
37
|
"shell_status",
|
|
40
38
|
"show_config",
|
|
41
39
|
"uninit",
|
|
@@ -9,11 +9,11 @@ from pcd_cli.catalog import ProjectCatalog
|
|
|
9
9
|
from pcd_cli.cli.config import config_commands
|
|
10
10
|
from pcd_cli.cli.projects import project_commands
|
|
11
11
|
from pcd_cli.cli.roots import root_commands
|
|
12
|
-
from pcd_cli.cli.shell import shell_commands
|
|
12
|
+
from pcd_cli.cli.shell import shell_commands
|
|
13
13
|
from pcd_cli.config import InvalidConfigError
|
|
14
|
+
from pcd_cli.integrations.shell import ShellIntegrationError
|
|
14
15
|
from pcd_cli.models import ExitCode
|
|
15
16
|
from pcd_cli.navigation import navigate_to_project, project_completions
|
|
16
|
-
from pcd_cli.shell_integration import ShellIntegrationError
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from click.shell_completion import CompletionItem
|
|
@@ -68,12 +68,15 @@ def package_version() -> str:
|
|
|
68
68
|
@click.pass_context
|
|
69
69
|
def cli(ctx: click.Context, project_name: str | None) -> None:
|
|
70
70
|
"""Jump to local projects by name."""
|
|
71
|
-
catalog = ProjectCatalog.create()
|
|
72
|
-
ctx.obj = catalog
|
|
73
|
-
|
|
74
71
|
if project_name is not None and ctx.invoked_subcommand is not None:
|
|
75
72
|
raise click.UsageError("--project cannot be combined with a command")
|
|
76
73
|
|
|
74
|
+
if ctx.invoked_subcommand == "shell":
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
catalog = ProjectCatalog.create()
|
|
78
|
+
ctx.obj = catalog
|
|
79
|
+
|
|
77
80
|
if ctx.invoked_subcommand is not None:
|
|
78
81
|
return
|
|
79
82
|
|
|
@@ -89,7 +92,6 @@ for command in (
|
|
|
89
92
|
*root_commands,
|
|
90
93
|
config_commands,
|
|
91
94
|
shell_commands,
|
|
92
|
-
shell_init,
|
|
93
95
|
):
|
|
94
96
|
cli.add_command(command)
|
|
95
97
|
|
|
@@ -104,5 +106,5 @@ def main() -> None:
|
|
|
104
106
|
click.echo(f"Shell integration error: {exc}", err=True)
|
|
105
107
|
raise SystemExit(ExitCode.ERROR) from None
|
|
106
108
|
except OSError as exc:
|
|
107
|
-
click.echo(f"
|
|
109
|
+
click.echo(f"I/O error: {exc}", err=True)
|
|
108
110
|
raise SystemExit(ExitCode.ERROR) from None
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import shlex
|
|
5
5
|
import subprocess
|
|
6
|
+
import sys
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
import click
|
|
@@ -55,7 +56,11 @@ def edit_config(catalog: ProjectCatalog) -> None:
|
|
|
55
56
|
|
|
56
57
|
catalog.config.ensure_exists()
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
arguments = [*command, str(catalog.config.path)]
|
|
60
|
+
if os.environ.get("PCD_SHELL") == "1":
|
|
61
|
+
result = subprocess.run(arguments, check=False, stdout=sys.stderr)
|
|
62
|
+
else:
|
|
63
|
+
result = subprocess.run(arguments, check=False)
|
|
59
64
|
if result.returncode:
|
|
60
65
|
raise click.ClickException(f"Editor exited with status {result.returncode}")
|
|
61
66
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
|
|
3
|
-
from pcd_cli.
|
|
5
|
+
from pcd_cli.integrations.shell import (
|
|
4
6
|
detect_shell,
|
|
7
|
+
invoking_shell,
|
|
5
8
|
render_shell_integration,
|
|
6
9
|
Shell,
|
|
7
|
-
shell_integration_active,
|
|
8
10
|
ShellIntegration,
|
|
9
11
|
ShellIntegrationError,
|
|
10
12
|
ShellIntegrationState,
|
|
@@ -24,10 +26,11 @@ def shell_commands() -> None:
|
|
|
24
26
|
)
|
|
25
27
|
def install_shell(shell: str | None) -> None:
|
|
26
28
|
"""Install persistent shell integration into the shell startup file."""
|
|
27
|
-
|
|
29
|
+
selected = _select_shell(shell, prompt=True)
|
|
30
|
+
integration = ShellIntegration.for_shell(selected)
|
|
28
31
|
if integration.install():
|
|
29
32
|
click.echo(f"Installed {integration.shell.value} integration in {integration.config_path}")
|
|
30
|
-
click.echo(f"
|
|
33
|
+
click.echo(f"Reload the current shell with: {integration.reload_command()}")
|
|
31
34
|
return
|
|
32
35
|
|
|
33
36
|
state = integration.state()
|
|
@@ -47,14 +50,15 @@ def install_shell(shell: str | None) -> None:
|
|
|
47
50
|
type=click.Choice([item.value for item in Shell], case_sensitive=False),
|
|
48
51
|
)
|
|
49
52
|
def shell_status(shell: str | None) -> None:
|
|
50
|
-
"""Show whether
|
|
51
|
-
integration =
|
|
53
|
+
"""Show shell configuration and whether this invocation used a wrapper."""
|
|
54
|
+
integration = ShellIntegration.for_shell(_select_shell(shell))
|
|
52
55
|
click.echo(f"Shell: {integration.shell.value}")
|
|
53
56
|
click.echo(f"Config: {integration.config_path}")
|
|
54
57
|
click.echo(f"Configured: {integration.state().value}")
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
wrapper: Shell | None = invoking_shell()
|
|
60
|
+
invoked = "no" if wrapper is None else f"yes ({wrapper.value})"
|
|
61
|
+
click.echo(f"Invoked through wrapper: {invoked}")
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
@shell_commands.command("uninstall")
|
|
@@ -65,7 +69,7 @@ def shell_status(shell: str | None) -> None:
|
|
|
65
69
|
)
|
|
66
70
|
def uninstall_shell(shell: str | None) -> None:
|
|
67
71
|
"""Remove integration installed by `pcd shell install`."""
|
|
68
|
-
integration =
|
|
72
|
+
integration = ShellIntegration.for_shell(_select_shell(shell))
|
|
69
73
|
if integration.uninstall():
|
|
70
74
|
click.echo(f"Removed shell integration from {integration.config_path}")
|
|
71
75
|
return
|
|
@@ -78,47 +82,46 @@ def uninstall_shell(shell: str | None) -> None:
|
|
|
78
82
|
click.echo(f"Shell integration is not installed in {integration.config_path}")
|
|
79
83
|
|
|
80
84
|
|
|
81
|
-
@shell_commands.command("
|
|
85
|
+
@shell_commands.command("init")
|
|
82
86
|
@click.argument(
|
|
83
87
|
"shell",
|
|
84
88
|
required=False,
|
|
85
89
|
type=click.Choice([item.value for item in Shell], case_sensitive=False),
|
|
86
90
|
)
|
|
87
|
-
|
|
88
|
-
def print_shell_integration(ctx: click.Context, shell: str | None) -> None:
|
|
91
|
+
def init_shell(shell: str | None) -> None:
|
|
89
92
|
"""Print shell integration for manual dotfile management."""
|
|
90
|
-
selected =
|
|
91
|
-
click.echo(render_shell_integration(selected
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@click.command("shell-init", hidden=True)
|
|
95
|
-
@click.argument("shell", type=click.Choice([item.value for item in Shell], case_sensitive=False))
|
|
96
|
-
@click.pass_context
|
|
97
|
-
def shell_init(ctx: click.Context, shell: str) -> None:
|
|
98
|
-
"""Backward-compatible alias for `pcd shell print`."""
|
|
99
|
-
click.echo(
|
|
100
|
-
render_shell_integration(Shell(shell.casefold()), _registered_command_names(ctx)),
|
|
101
|
-
nl=False,
|
|
102
|
-
)
|
|
103
|
-
|
|
93
|
+
selected = _select_shell(shell)
|
|
94
|
+
click.echo(render_shell_integration(selected), nl=False)
|
|
104
95
|
|
|
105
|
-
def _shell_integration(shell: str | None) -> ShellIntegration:
|
|
106
|
-
return ShellIntegration.for_shell(_selected_shell(shell))
|
|
107
96
|
|
|
108
|
-
|
|
109
|
-
def _selected_shell(shell: str | None) -> Shell:
|
|
97
|
+
def _select_shell(shell: str | None, *, prompt: bool = False) -> Shell:
|
|
110
98
|
if shell is not None:
|
|
111
99
|
return Shell(shell.casefold())
|
|
112
100
|
|
|
113
101
|
try:
|
|
114
102
|
return detect_shell()
|
|
115
103
|
except ShellIntegrationError as exc:
|
|
104
|
+
if prompt and sys.platform == "win32":
|
|
105
|
+
return _prompt_windows_shell()
|
|
116
106
|
raise click.UsageError(str(exc)) from exc
|
|
117
107
|
|
|
118
108
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
def _prompt_windows_shell() -> Shell:
|
|
110
|
+
choices = (
|
|
111
|
+
("PowerShell 7+", Shell.PWSH),
|
|
112
|
+
("Windows PowerShell 5.1", Shell.POWERSHELL),
|
|
113
|
+
("Command Prompt (cmd)", Shell.CMD),
|
|
114
|
+
("Git Bash", Shell.BASH),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
click.echo("Select shell:")
|
|
118
|
+
for index, (label, _shell) in enumerate(choices, start=1):
|
|
119
|
+
click.echo(f" {index}. {label}")
|
|
120
|
+
|
|
121
|
+
choice = click.prompt(
|
|
122
|
+
"Enter number",
|
|
123
|
+
type=click.IntRange(1, len(choices)),
|
|
124
|
+
show_choices=False,
|
|
125
|
+
)
|
|
123
126
|
|
|
124
|
-
return
|
|
127
|
+
return choices[choice - 1][1]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import fcntl
|
|
4
3
|
import os
|
|
5
4
|
import tempfile
|
|
6
5
|
from contextlib import contextmanager
|
|
@@ -10,6 +9,8 @@ from typing import Self, TextIO, TYPE_CHECKING
|
|
|
10
9
|
|
|
11
10
|
from platformdirs import PlatformDirs
|
|
12
11
|
|
|
12
|
+
from pcd_cli.locking import file_lock as file_lock
|
|
13
|
+
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from collections.abc import Generator
|
|
15
16
|
|
|
@@ -48,20 +49,6 @@ def format_path(path: Path) -> str:
|
|
|
48
49
|
return "~" if not relative.parts else f"~/{relative}"
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
@contextmanager
|
|
52
|
-
def file_lock(path: Path) -> Generator[None]:
|
|
53
|
-
"""Prevent concurrent writers from overwriting each other's changes."""
|
|
54
|
-
lock_path = path.with_name(f".{path.name}.lock")
|
|
55
|
-
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
with lock_path.open("a+", encoding="utf-8") as stream:
|
|
58
|
-
fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
|
|
59
|
-
try:
|
|
60
|
-
yield
|
|
61
|
-
finally:
|
|
62
|
-
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
|
|
63
|
-
|
|
64
|
-
|
|
65
52
|
@contextmanager
|
|
66
53
|
def atomic_write(path: Path) -> Generator[TextIO]:
|
|
67
54
|
"""Write through a temporary file so readers never observe partial data."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""External environment integrations."""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Generate, install, and inspect shell integration for pcd."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
from pcd_cli.integrations.shells import startup_file
|
|
10
|
+
from pcd_cli.integrations.shells.common import (
|
|
11
|
+
Shell as Shell,
|
|
12
|
+
SHELL_CD_EXIT_CODE as SHELL_CD_EXIT_CODE,
|
|
13
|
+
SHELL_MODE_ENV,
|
|
14
|
+
SHELL_WRAPPER_ENV,
|
|
15
|
+
ShellIntegrationError as ShellIntegrationError,
|
|
16
|
+
ShellIntegrationState as ShellIntegrationState,
|
|
17
|
+
)
|
|
18
|
+
from pcd_cli.integrations.shells.registry import backend_for
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ShellIntegration:
|
|
23
|
+
shell: Shell
|
|
24
|
+
config_path: Path
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def detect(cls) -> Self:
|
|
28
|
+
return cls.for_shell(detect_shell())
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def for_shell(cls, shell: Shell) -> Self:
|
|
32
|
+
return cls(shell=shell, config_path=shell_config_path(shell))
|
|
33
|
+
|
|
34
|
+
def state(self) -> ShellIntegrationState:
|
|
35
|
+
if self.shell is Shell.CMD:
|
|
36
|
+
from pcd_cli.integrations.shells import cmd
|
|
37
|
+
|
|
38
|
+
return cmd.state(self.config_path)
|
|
39
|
+
return startup_file.state(self.config_path, self.shell)
|
|
40
|
+
|
|
41
|
+
def reload_command(self) -> str:
|
|
42
|
+
return backend_for(self.shell).reload_command(self.shell, self.config_path)
|
|
43
|
+
|
|
44
|
+
def install(self) -> bool:
|
|
45
|
+
"""Install managed integration. Return whether persistent state changed."""
|
|
46
|
+
if self.shell is Shell.CMD:
|
|
47
|
+
from pcd_cli.integrations.shells import cmd
|
|
48
|
+
|
|
49
|
+
return cmd.install(self.config_path)
|
|
50
|
+
command = backend_for(self.shell).startup_command(self.shell)
|
|
51
|
+
if command is None:
|
|
52
|
+
raise ShellIntegrationError(f"Shell does not use a startup file: {self.shell}")
|
|
53
|
+
return startup_file.install(self.config_path, self.shell, command)
|
|
54
|
+
|
|
55
|
+
def uninstall(self) -> bool:
|
|
56
|
+
"""Remove only integration installed by pcd, leaving manual setup untouched."""
|
|
57
|
+
if self.shell is Shell.CMD:
|
|
58
|
+
from pcd_cli.integrations.shells import cmd
|
|
59
|
+
|
|
60
|
+
return cmd.uninstall(self.config_path)
|
|
61
|
+
return startup_file.uninstall(self.config_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def detect_shell() -> Shell:
|
|
65
|
+
"""Detect the active wrapper or surrounding shell."""
|
|
66
|
+
wrapper = invoking_shell()
|
|
67
|
+
if wrapper is not None:
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
if sys.platform == "win32":
|
|
71
|
+
from pcd_cli.integrations.shells import windows_shell
|
|
72
|
+
|
|
73
|
+
name = windows_shell.detect_shell()
|
|
74
|
+
source = "the parent process"
|
|
75
|
+
else:
|
|
76
|
+
executable = os.environ.get("SHELL", "")
|
|
77
|
+
name = Path(executable).name.casefold().removesuffix(".exe")
|
|
78
|
+
source = f"SHELL={executable!r}"
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
return Shell(name)
|
|
82
|
+
except ValueError as exc:
|
|
83
|
+
supported = ", ".join(shell.value for shell in Shell)
|
|
84
|
+
raise ShellIntegrationError(
|
|
85
|
+
f"Cannot detect a supported shell from {source}; choose one of: {supported}"
|
|
86
|
+
) from exc
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def shell_config_path(shell: Shell) -> Path:
|
|
90
|
+
"""Return the persistent integration file for a shell."""
|
|
91
|
+
return backend_for(shell).config_path(shell, Path.home())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def shell_integration_active() -> bool:
|
|
95
|
+
return os.environ.get(SHELL_MODE_ENV) == "1"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def invoking_shell() -> Shell | None:
|
|
99
|
+
"""Identify the wrapper for this invocation, not the parent shell's state."""
|
|
100
|
+
try:
|
|
101
|
+
return Shell(os.environ.get(SHELL_WRAPPER_ENV, ""))
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def inactive_shell_message() -> str:
|
|
107
|
+
"""Explain how to activate directory changes when the wrapper is not running."""
|
|
108
|
+
install_command = "pcd shell install"
|
|
109
|
+
if sys.platform == "win32" and invoking_shell() is None:
|
|
110
|
+
return (
|
|
111
|
+
"Shell integration is not active, so pcd cannot change this shell's directory. "
|
|
112
|
+
f"If you have not configured it manually, run: {install_command}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
integration = ShellIntegration.detect()
|
|
117
|
+
state = integration.state()
|
|
118
|
+
except (OSError, ShellIntegrationError):
|
|
119
|
+
return (
|
|
120
|
+
"Shell integration is not active, so pcd cannot change this shell's directory. "
|
|
121
|
+
f"If you have not configured it manually, run: {install_command}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if state is ShellIntegrationState.ABSENT:
|
|
125
|
+
return (
|
|
126
|
+
"Shell integration is not installed, so pcd cannot change this shell's directory. "
|
|
127
|
+
f"Install it with: {install_command}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
f"Shell integration is configured in {integration.config_path} but is not active in "
|
|
132
|
+
f"this shell. Reload it with: {integration.reload_command()}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render_managed_block(shell: Shell) -> str:
|
|
137
|
+
"""Render the small persistent block written into a shell startup file."""
|
|
138
|
+
command = backend_for(shell).startup_command(shell)
|
|
139
|
+
if command is None:
|
|
140
|
+
raise ValueError(f"Shell does not use a startup file: {shell}")
|
|
141
|
+
return startup_file.managed_block(command)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def render_shell_integration(shell: Shell) -> str:
|
|
145
|
+
"""Generate shell integration for directory changes and completion."""
|
|
146
|
+
return backend_for(shell).render(shell)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shell-specific integration backends."""
|