pcd-cli 0.2.2__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pcd-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Fast project navigator for Bash, Zsh and Fish
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -76,6 +76,7 @@ COMMANDS
76
76
  pcd config validate Validate the user configuration
77
77
 
78
78
  pcd shell install Install shell integration
79
+ pcd shell init <shell> Print shell integration code
79
80
  pcd shell status Show shell integration status
80
81
  pcd shell uninstall Remove shell integration
81
82
 
@@ -52,6 +52,7 @@ COMMANDS
52
52
  pcd config validate Validate the user configuration
53
53
 
54
54
  pcd shell install Install shell integration
55
+ pcd shell init <shell> Print shell integration code
55
56
  pcd shell status Show shell integration status
56
57
  pcd shell uninstall Remove shell integration
57
58
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pcd-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Fast project navigator for Bash, Zsh and Fish"
5
5
  readme = { file = "README.txt", content-type = "text/plain" }
6
6
  license = "MIT"
@@ -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
@@ -89,7 +89,6 @@ for command in (
89
89
  *root_commands,
90
90
  config_commands,
91
91
  shell_commands,
92
- shell_init,
93
92
  ):
94
93
  cli.add_command(command)
95
94
 
@@ -1,10 +1,10 @@
1
1
  import click
2
2
 
3
- from pcd_cli.shell_integration import (
3
+ from pcd_cli.integrations.shell import (
4
4
  detect_shell,
5
+ invoking_shell,
5
6
  render_shell_integration,
6
7
  Shell,
7
- shell_integration_active,
8
8
  ShellIntegration,
9
9
  ShellIntegrationError,
10
10
  ShellIntegrationState,
@@ -27,7 +27,7 @@ def install_shell(shell: str | None) -> None:
27
27
  integration = _shell_integration(shell)
28
28
  if integration.install():
29
29
  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}")
30
+ click.echo(f"Reload the current shell with: {integration.reload_command()}")
31
31
  return
32
32
 
33
33
  state = integration.state()
@@ -47,14 +47,15 @@ def install_shell(shell: str | None) -> None:
47
47
  type=click.Choice([item.value for item in Shell], case_sensitive=False),
48
48
  )
49
49
  def shell_status(shell: str | None) -> None:
50
- """Show whether shell integration is configured and active."""
50
+ """Show shell configuration and whether this invocation used a wrapper."""
51
51
  integration = _shell_integration(shell)
52
52
  click.echo(f"Shell: {integration.shell.value}")
53
53
  click.echo(f"Config: {integration.config_path}")
54
54
  click.echo(f"Configured: {integration.state().value}")
55
55
 
56
- active = shell_integration_active()
57
- click.echo(f"Active in current shell: {'yes' if active else 'no'}")
56
+ wrapper: Shell | None = invoking_shell()
57
+ invoked = "no" if wrapper is None else f"yes ({wrapper.value})"
58
+ click.echo(f"Invoked through wrapper: {invoked}")
58
59
 
59
60
 
60
61
  @shell_commands.command("uninstall")
@@ -78,30 +79,19 @@ def uninstall_shell(shell: str | None) -> None:
78
79
  click.echo(f"Shell integration is not installed in {integration.config_path}")
79
80
 
80
81
 
81
- @shell_commands.command("print")
82
+ @shell_commands.command("init")
82
83
  @click.argument(
83
84
  "shell",
84
85
  required=False,
85
86
  type=click.Choice([item.value for item in Shell], case_sensitive=False),
86
87
  )
87
88
  @click.pass_context
88
- def print_shell_integration(ctx: click.Context, shell: str | None) -> None:
89
+ def init_shell(ctx: click.Context, shell: str | None) -> None:
89
90
  """Print shell integration for manual dotfile management."""
90
91
  selected = _selected_shell(shell)
91
92
  click.echo(render_shell_integration(selected, _registered_command_names(ctx)), nl=False)
92
93
 
93
94
 
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
-
104
-
105
95
  def _shell_integration(shell: str | None) -> ShellIntegration:
106
96
  return ShellIntegration.for_shell(_selected_shell(shell))
107
97
 
@@ -0,0 +1 @@
1
+ """External environment integrations."""
@@ -1,6 +1,7 @@
1
1
  """Generate, install, and inspect shell integration for pcd."""
2
2
 
3
3
  import os
4
+ import shlex
4
5
  import stat
5
6
  from dataclasses import dataclass
6
7
  from enum import StrEnum
@@ -11,6 +12,7 @@ from typing import Literal, Self
11
12
  from pcd_cli.filesystem import atomic_write, file_lock
12
13
 
13
14
  SHELL_MODE_ENV = "PCD_SHELL"
15
+ SHELL_WRAPPER_ENV = "PCD_WRAPPER"
14
16
  # Internal success status: stdout contains the destination path when this is returned.
15
17
  SHELL_CD_EXIT_CODE = 10
16
18
  _MANAGED_BLOCK_START = "# >>> pcd shell integration >>>"
@@ -49,6 +51,9 @@ class ShellIntegration:
49
51
  def state(self) -> ShellIntegrationState:
50
52
  return _integration_state(self._read(), self.shell)
51
53
 
54
+ def reload_command(self) -> str:
55
+ return f"source {shlex.quote(str(self.config_path))}"
56
+
52
57
  def install(self) -> bool:
53
58
  """Install the managed block. Return whether the config changed."""
54
59
  with file_lock(self.config_path):
@@ -128,6 +133,14 @@ def shell_integration_active() -> bool:
128
133
  return os.environ.get(SHELL_MODE_ENV) == "1"
129
134
 
130
135
 
136
+ def invoking_shell() -> Shell | None:
137
+ """Identify the wrapper for this invocation, not the parent shell's state."""
138
+ try:
139
+ return Shell(os.environ.get(SHELL_WRAPPER_ENV, ""))
140
+ except ValueError:
141
+ return None
142
+
143
+
131
144
  def inactive_shell_message() -> str:
132
145
  """Explain how to activate directory changes when the wrapper is not running."""
133
146
  try:
@@ -146,16 +159,16 @@ def inactive_shell_message() -> str:
146
159
  )
147
160
  return (
148
161
  f"Shell integration is configured in {integration.config_path} but is not active in "
149
- f"this shell. Restart it or run: exec {integration.shell.value}"
162
+ f"this shell. Reload it with: {integration.reload_command()}"
150
163
  )
151
164
 
152
165
 
153
166
  def render_managed_block(shell: Shell) -> str:
154
167
  """Render the small persistent block written into the shell startup file."""
155
168
  command = (
156
- f'eval "$(command pcd shell print {shell.value})"'
169
+ f'eval "$(command pcd shell init {shell.value})"'
157
170
  if shell is not Shell.FISH
158
- else f"command pcd shell print {shell.value} | source"
171
+ else f"command pcd shell init {shell.value} | source"
159
172
  )
160
173
  return f"{_MANAGED_BLOCK_START}\n{command}\n{_MANAGED_BLOCK_END}\n"
161
174
 
@@ -179,9 +192,8 @@ def _integration_state(content: str, shell: Shell) -> ShellIntegrationState:
179
192
  if bounds is not None:
180
193
  return ShellIntegrationState.MANAGED
181
194
 
182
- legacy = f"pcd shell-init {shell.value}"
183
- current = f"pcd shell print {shell.value}"
184
- if legacy in content or current in content:
195
+ current = f"pcd shell init {shell.value}"
196
+ if current in content:
185
197
  return ShellIntegrationState.MANUAL
186
198
  return ShellIntegrationState.ABSENT
187
199
 
@@ -214,6 +226,7 @@ def _posix(completion: Literal["bash_source", "zsh_source"], commands: tuple[str
214
226
  return dedent(
215
227
  f"""\
216
228
  pcd() {{
229
+ local -x {SHELL_WRAPPER_ENV}={completion.removesuffix("_source")}
217
230
  if [ -n "${{_PCD_COMPLETE:-}}" ]; then
218
231
  command pcd "$@"
219
232
  return $?
@@ -260,6 +273,7 @@ def _fish(commands: tuple[str, ...]) -> str:
260
273
  return dedent(
261
274
  f"""\
262
275
  function pcd
276
+ set -lx {SHELL_WRAPPER_ENV} fish
263
277
  if set -q _PCD_COMPLETE
264
278
  command pcd $argv
265
279
  return $status
@@ -277,7 +291,10 @@ def _fish(commands: tuple[str, ...]) -> str:
277
291
  return $status
278
292
  end
279
293
 
280
- set -l output (env {SHELL_MODE_ENV}=1 command pcd $argv)
294
+ set -l output (begin
295
+ set -lx {SHELL_MODE_ENV} 1
296
+ command pcd $argv
297
+ end)
281
298
  set -l code $status
282
299
  if test $code -eq {SHELL_CD_EXIT_CODE}
283
300
  cd -- "$output"
@@ -7,12 +7,12 @@ from click.shell_completion import CompletionItem
7
7
 
8
8
  from pcd_cli.catalog import ProjectCatalog
9
9
  from pcd_cli.filesystem import format_path
10
- from pcd_cli.models import ExitCode, ProjectSource
11
- from pcd_cli.shell_integration import (
10
+ from pcd_cli.integrations.shell import (
12
11
  inactive_shell_message,
13
12
  SHELL_CD_EXIT_CODE,
14
13
  shell_integration_active,
15
14
  )
15
+ from pcd_cli.models import ExitCode, ProjectSource
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Sequence
@@ -65,7 +65,10 @@ def project_completions(value: str) -> list[CompletionItem]:
65
65
 
66
66
  def _open_project(catalog: ProjectCatalog, project: Project) -> None:
67
67
  if project.path.is_dir():
68
- catalog.record_usage(project)
68
+ try:
69
+ catalog.record_usage(project)
70
+ except OSError as exc:
71
+ click.echo(f"Warning: could not write usage history: {exc}", err=True)
69
72
  elif project.source is ProjectSource.MANUAL:
70
73
  click.echo(
71
74
  f"Project path no longer exists: {format_path(project.display_path)}",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes