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.
Files changed (37) hide show
  1. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/PKG-INFO +10 -3
  2. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/README.txt +5 -0
  3. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/pyproject.toml +6 -4
  4. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/catalog.py +1 -1
  5. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/__init__.py +2 -4
  6. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/app.py +9 -7
  7. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/config.py +6 -1
  8. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/shell.py +38 -35
  9. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/filesystem.py +2 -15
  10. pcd_cli-0.2.4/src/pcd_cli/integrations/__init__.py +1 -0
  11. pcd_cli-0.2.4/src/pcd_cli/integrations/shell.py +146 -0
  12. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/__init__.py +1 -0
  13. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/cmd.py +176 -0
  14. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/cmd_autorun.py +78 -0
  15. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/common.py +27 -0
  16. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/powershell.py +125 -0
  17. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/registry.py +37 -0
  18. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/startup_file.py +114 -0
  19. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/unix.py +115 -0
  20. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/windows_registry.py +74 -0
  21. pcd_cli-0.2.4/src/pcd_cli/integrations/shells/windows_shell.py +30 -0
  22. pcd_cli-0.2.4/src/pcd_cli/locking/__init__.py +33 -0
  23. pcd_cli-0.2.4/src/pcd_cli/locking/unix.py +27 -0
  24. pcd_cli-0.2.4/src/pcd_cli/locking/windows.py +39 -0
  25. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/navigation.py +6 -3
  26. pcd_cli-0.2.2/src/pcd_cli/shell_integration.py +0 -298
  27. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/LICENSE +0 -0
  28. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/__init__.py +0 -0
  29. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cache.py +0 -0
  30. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/projects.py +0 -0
  31. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/cli/roots.py +0 -0
  32. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/config.py +0 -0
  33. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/history.py +0 -0
  34. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/models.py +0 -0
  35. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/picker.py +0 -0
  36. {pcd_cli-0.2.2 → pcd_cli-0.2.4}/src/pcd_cli/scanner.py +0 -0
  37. {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.2
4
- Summary: Fast project navigator for Bash, Zsh and Fish
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.2"
4
- description = "Fast project navigator for Bash, Zsh and Fish"
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
  ]
@@ -35,7 +35,7 @@ class ProjectCatalog:
35
35
  )
36
36
 
37
37
  def projects(self) -> list[Project]:
38
- if cached := self.cache.load():
38
+ if (cached := self.cache.load()) is not None:
39
39
  return cached
40
40
 
41
41
  return self.refresh()
@@ -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, shell_init
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"Filesystem error: {exc}", err=True)
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
- result = subprocess.run([*command, str(catalog.config.path)], check=False)
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.shell_integration import (
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
- integration = _shell_integration(shell)
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"Restart the shell or run: exec {integration.shell.value}")
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 shell integration is configured and active."""
51
- integration = _shell_integration(shell)
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
- active = shell_integration_active()
57
- click.echo(f"Active in current shell: {'yes' if active else 'no'}")
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 = _shell_integration(shell)
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("print")
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
- @click.pass_context
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 = _selected_shell(shell)
91
- click.echo(render_shell_integration(selected, _registered_command_names(ctx)), nl=False)
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 _registered_command_names(ctx: click.Context) -> tuple[str, ...]:
120
- root = ctx.find_root()
121
- if not isinstance(root.command, click.Group):
122
- return ()
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 tuple(root.command.list_commands(root))
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."""