pcd-cli 0.2.0__tar.gz → 0.2.1__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.0 → pcd_cli-0.2.1}/PKG-INFO +2 -5
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/README.txt +1 -4
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/pyproject.toml +1 -1
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cli/projects.py +70 -13
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cli/shell.py +17 -4
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/navigation.py +7 -4
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/picker.py +1 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/shell_integration.py +40 -6
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/LICENSE +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/__init__.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cache.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/catalog.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cli/__init__.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cli/app.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/cli/config.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/config.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/filesystem.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/history.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/models.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/scanner.py +0 -0
- {pcd_cli-0.2.0 → pcd_cli-0.2.1}/src/pcd_cli/search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pcd-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Fast project navigator for Bash, Zsh and Fish
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -103,10 +103,7 @@ SEE ALSO
|
|
|
103
103
|
FEATURES.txt
|
|
104
104
|
|
|
105
105
|
Planned changes:
|
|
106
|
-
ROADMAP.
|
|
107
|
-
|
|
108
|
-
Changelog:
|
|
109
|
-
CHANGELOG.md
|
|
106
|
+
ROADMAP.txt
|
|
110
107
|
|
|
111
108
|
License:
|
|
112
109
|
MIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import assert_never, ClassVar, Literal, NamedTuple, TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
@@ -10,8 +10,74 @@ from pcd_cli.models import ExitCode, Project, ProjectSource
|
|
|
10
10
|
from pcd_cli.navigation import navigate_to_project, project_completions, select_project
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
13
15
|
from pcd_cli.catalog import ProjectCatalog
|
|
14
16
|
|
|
17
|
+
type ProjectSourceLabel = Literal["manual", "scanned"]
|
|
18
|
+
type ProjectStatusLabel = Literal["available", "missing"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProjectListRow(NamedTuple):
|
|
22
|
+
name: str
|
|
23
|
+
path: str
|
|
24
|
+
source: ProjectSourceLabel
|
|
25
|
+
status: ProjectStatusLabel
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_project(cls, project: Project) -> ProjectListRow:
|
|
29
|
+
return cls(
|
|
30
|
+
name=project.name,
|
|
31
|
+
path=format_path(project.display_path),
|
|
32
|
+
source=cls._source_label(project.source),
|
|
33
|
+
status=cls._status_label(project.path),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _source_label(source: ProjectSource) -> ProjectSourceLabel:
|
|
38
|
+
match source:
|
|
39
|
+
case ProjectSource.DISCOVERED:
|
|
40
|
+
return "scanned"
|
|
41
|
+
case ProjectSource.MANUAL:
|
|
42
|
+
return "manual"
|
|
43
|
+
case _:
|
|
44
|
+
assert_never(source)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _status_label(path: Path) -> ProjectStatusLabel:
|
|
48
|
+
if path.is_dir():
|
|
49
|
+
return "available"
|
|
50
|
+
else:
|
|
51
|
+
return "missing"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProjectListTable:
|
|
55
|
+
HEADERS: ClassVar[tuple[str, str, str, str]] = ("NAME", "PATH", "SOURCE", "STATUS")
|
|
56
|
+
|
|
57
|
+
_rows: tuple[ProjectListRow, ...]
|
|
58
|
+
|
|
59
|
+
def __init__(self, projects: Sequence[Project]) -> None:
|
|
60
|
+
self._rows = tuple(ProjectListRow.from_project(project) for project in projects)
|
|
61
|
+
|
|
62
|
+
def render(self) -> str:
|
|
63
|
+
if self._rows:
|
|
64
|
+
return self._render_table()
|
|
65
|
+
else:
|
|
66
|
+
return "No projects found."
|
|
67
|
+
|
|
68
|
+
def _render_table(self) -> str:
|
|
69
|
+
table = (self.HEADERS, *self._rows)
|
|
70
|
+
columns = zip(*table, strict=True)
|
|
71
|
+
widths = tuple(max(len(value) for value in column) for column in columns)
|
|
72
|
+
|
|
73
|
+
return "\n".join(self._render_row(row, widths) for row in table)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _render_row(row: tuple[str, ...], widths: tuple[int, ...]) -> str:
|
|
77
|
+
return " ".join(
|
|
78
|
+
value.ljust(width) for value, width in zip(row, widths, strict=True)
|
|
79
|
+
).rstrip()
|
|
80
|
+
|
|
15
81
|
|
|
16
82
|
@click.command(hidden=True)
|
|
17
83
|
@click.argument("query")
|
|
@@ -101,7 +167,7 @@ def remove(catalog: ProjectCatalog, query: str) -> None:
|
|
|
101
167
|
click.echo(f"Project not found: {query}", err=True)
|
|
102
168
|
raise click.exceptions.Exit(ExitCode.NOT_FOUND)
|
|
103
169
|
|
|
104
|
-
selected =
|
|
170
|
+
selected = select_project(catalog, matches, query)
|
|
105
171
|
if selected is None:
|
|
106
172
|
return
|
|
107
173
|
|
|
@@ -124,17 +190,8 @@ def remove(catalog: ProjectCatalog, query: str) -> None:
|
|
|
124
190
|
@click.pass_obj
|
|
125
191
|
def list_projects(catalog: ProjectCatalog) -> None:
|
|
126
192
|
"""List all known projects."""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
click.echo("No projects found.")
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
name_width = max(len(project.name) for project in projects)
|
|
133
|
-
for item in projects:
|
|
134
|
-
status = "ok" if item.path.is_dir() else "missing"
|
|
135
|
-
click.echo(
|
|
136
|
-
f"{item.name:<{name_width}} {format_path(item.display_path)} {item.source.value:<10} {status}"
|
|
137
|
-
)
|
|
193
|
+
table = ProjectListTable(catalog.projects())
|
|
194
|
+
click.echo(table.render())
|
|
138
195
|
|
|
139
196
|
|
|
140
197
|
@click.command()
|
|
@@ -84,17 +84,22 @@ def uninstall_shell(shell: str | None) -> None:
|
|
|
84
84
|
required=False,
|
|
85
85
|
type=click.Choice([item.value for item in Shell], case_sensitive=False),
|
|
86
86
|
)
|
|
87
|
-
|
|
87
|
+
@click.pass_context
|
|
88
|
+
def print_shell_integration(ctx: click.Context, shell: str | None) -> None:
|
|
88
89
|
"""Print shell integration for manual dotfile management."""
|
|
89
90
|
selected = _selected_shell(shell)
|
|
90
|
-
click.echo(render_shell_integration(selected), nl=False)
|
|
91
|
+
click.echo(render_shell_integration(selected, _registered_command_names(ctx)), nl=False)
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
@click.command("shell-init", hidden=True)
|
|
94
95
|
@click.argument("shell", type=click.Choice([item.value for item in Shell], case_sensitive=False))
|
|
95
|
-
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def shell_init(ctx: click.Context, shell: str) -> None:
|
|
96
98
|
"""Backward-compatible alias for `pcd shell print`."""
|
|
97
|
-
click.echo(
|
|
99
|
+
click.echo(
|
|
100
|
+
render_shell_integration(Shell(shell.casefold()), _registered_command_names(ctx)),
|
|
101
|
+
nl=False,
|
|
102
|
+
)
|
|
98
103
|
|
|
99
104
|
|
|
100
105
|
def _shell_integration(shell: str | None) -> ShellIntegration:
|
|
@@ -109,3 +114,11 @@ def _selected_shell(shell: str | None) -> Shell:
|
|
|
109
114
|
return detect_shell()
|
|
110
115
|
except ShellIntegrationError as exc:
|
|
111
116
|
raise click.UsageError(str(exc)) from exc
|
|
117
|
+
|
|
118
|
+
|
|
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 ()
|
|
123
|
+
|
|
124
|
+
return tuple(root.command.list_commands(root))
|
|
@@ -31,7 +31,7 @@ def navigate_to_project(
|
|
|
31
31
|
click.echo(f"Project not found{suffix}", err=True)
|
|
32
32
|
raise click.exceptions.Exit(ExitCode.NOT_FOUND)
|
|
33
33
|
|
|
34
|
-
selected =
|
|
34
|
+
selected = select_project(catalog, matches, query or "")
|
|
35
35
|
if selected is not None:
|
|
36
36
|
_open_project(catalog, selected)
|
|
37
37
|
|
|
@@ -41,10 +41,13 @@ def select_project(
|
|
|
41
41
|
projects: Sequence[Project],
|
|
42
42
|
query: str = "",
|
|
43
43
|
) -> Project | None:
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if len(projects) == 1:
|
|
45
|
+
return projects[0]
|
|
46
|
+
else:
|
|
47
|
+
# Import lazily so shell completion never initializes terminal UI machinery.
|
|
48
|
+
from pcd_cli.picker import ProjectPicker
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
return ProjectPicker(projects, catalog.history.load(), query).run()
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def project_completions(value: str) -> list[CompletionItem]:
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from enum import StrEnum
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from textwrap import dedent
|
|
9
|
-
from typing import Self
|
|
9
|
+
from typing import Literal, Self
|
|
10
10
|
|
|
11
11
|
from pcd_cli.filesystem import atomic_write, file_lock
|
|
12
12
|
|
|
@@ -160,9 +160,18 @@ def render_managed_block(shell: Shell) -> str:
|
|
|
160
160
|
return f"{_MANAGED_BLOCK_START}\n{command}\n{_MANAGED_BLOCK_END}\n"
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def render_shell_integration(shell: Shell) -> str:
|
|
163
|
+
def render_shell_integration(shell: Shell, direct_commands: tuple[str, ...] = ()) -> str:
|
|
164
164
|
"""Generate shell integration for directory changes and Click completion."""
|
|
165
|
-
|
|
165
|
+
commands = tuple(sorted(set(direct_commands)))
|
|
166
|
+
match shell:
|
|
167
|
+
case Shell.FISH:
|
|
168
|
+
return _fish(commands)
|
|
169
|
+
case Shell.BASH:
|
|
170
|
+
return _posix("bash_source", commands)
|
|
171
|
+
case Shell.ZSH:
|
|
172
|
+
return _posix("zsh_source", commands)
|
|
173
|
+
case _:
|
|
174
|
+
raise ValueError(f"Unsupported shell: {shell}")
|
|
166
175
|
|
|
167
176
|
|
|
168
177
|
def _integration_state(content: str, shell: Shell) -> ShellIntegrationState:
|
|
@@ -200,8 +209,8 @@ def _managed_block_bounds(content: str) -> tuple[int, int] | None:
|
|
|
200
209
|
return start, end
|
|
201
210
|
|
|
202
211
|
|
|
203
|
-
def _posix(
|
|
204
|
-
|
|
212
|
+
def _posix(completion: Literal["bash_source", "zsh_source"], commands: tuple[str, ...]) -> str:
|
|
213
|
+
direct_patterns = "|".join(("-*", *commands))
|
|
205
214
|
return dedent(
|
|
206
215
|
f"""\
|
|
207
216
|
pcd() {{
|
|
@@ -210,6 +219,18 @@ def _posix(shell: Shell) -> str:
|
|
|
210
219
|
return $?
|
|
211
220
|
fi
|
|
212
221
|
|
|
222
|
+
if [ "$#" -eq 0 ]; then
|
|
223
|
+
command pcd
|
|
224
|
+
return $?
|
|
225
|
+
fi
|
|
226
|
+
case "$1" in
|
|
227
|
+
--project|--project=*) : ;;
|
|
228
|
+
{direct_patterns})
|
|
229
|
+
command pcd "$@"
|
|
230
|
+
return $?
|
|
231
|
+
;;
|
|
232
|
+
esac
|
|
233
|
+
|
|
213
234
|
local output code
|
|
214
235
|
if output="$({SHELL_MODE_ENV}=1 command pcd "$@")"; then
|
|
215
236
|
code=0
|
|
@@ -234,7 +255,8 @@ def _posix(shell: Shell) -> str:
|
|
|
234
255
|
)
|
|
235
256
|
|
|
236
257
|
|
|
237
|
-
def _fish() -> str:
|
|
258
|
+
def _fish(commands: tuple[str, ...]) -> str:
|
|
259
|
+
direct_patterns = " ".join(("'-*'", *commands))
|
|
238
260
|
return dedent(
|
|
239
261
|
f"""\
|
|
240
262
|
function pcd
|
|
@@ -243,6 +265,18 @@ def _fish() -> str:
|
|
|
243
265
|
return $status
|
|
244
266
|
end
|
|
245
267
|
|
|
268
|
+
if test (count $argv) -eq 0
|
|
269
|
+
command pcd
|
|
270
|
+
return $status
|
|
271
|
+
end
|
|
272
|
+
switch $argv[1]
|
|
273
|
+
case '--project' '--project=*'
|
|
274
|
+
true
|
|
275
|
+
case {direct_patterns}
|
|
276
|
+
command pcd $argv
|
|
277
|
+
return $status
|
|
278
|
+
end
|
|
279
|
+
|
|
246
280
|
set -l output (env {SHELL_MODE_ENV}=1 command pcd $argv)
|
|
247
281
|
set -l code $status
|
|
248
282
|
if test $code -eq {SHELL_CD_EXIT_CODE}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|