gitdirector 1.7.2__tar.gz → 1.7.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.
- {gitdirector-1.7.2 → gitdirector-1.7.4}/PKG-INFO +5 -2
- {gitdirector-1.7.2 → gitdirector-1.7.4}/README.md +3 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/pyproject.toml +2 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/capture.py +1 -2
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/gd_send.py +2 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/gd_tmux.py +6 -19
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/help.py +4 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/info.py +11 -20
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/link.py +15 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/__init__.py +1 -2
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app.py +65 -20
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_groups.py +1 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_panels.py +24 -18
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_repos.py +26 -20
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_sessions.py +4 -7
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_ui.py +10 -0
- gitdirector-1.7.4/src/gitdirector/commands/tui/panel_view.py +211 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/panels.py +126 -21
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/commit.py +23 -12
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/diff.py +18 -27
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/panels.py +16 -6
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/sessions.py +31 -9
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/terminal_widget.py +6 -2
- gitdirector-1.7.4/src/gitdirector/commands/unlink.py +48 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/config.py +45 -17
- gitdirector-1.7.4/src/gitdirector/integrations/tmux/__init__.py +108 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/core.py +31 -10
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/monitor.py +6 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/panels.py +13 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/manager.py +83 -1
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/repo.py +7 -0
- gitdirector-1.7.2/src/gitdirector/commands/tui/panel_view.py +0 -478
- gitdirector-1.7.2/src/gitdirector/commands/unlink.py +0 -48
- gitdirector-1.7.2/src/gitdirector/integrations/tmux/__init__.py +0 -5
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/__init__.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/cli.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/__init__.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/autoclean.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/cd.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/completion.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/listt.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/pull.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/reset.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/status.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/constants.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/diff_renderer.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/__init__.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/_shared.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/diff_files.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/groups.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/repos.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/session_actions.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/table_text.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/commands/tui/terminal_caps.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/github_credential_helper.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/info.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/integrations/__init__.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/storage.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/ui_theme.py +0 -0
- {gitdirector-1.7.2 → gitdirector-1.7.4}/src/gitdirector/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: gitdirector
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.4
|
|
4
4
|
Summary: A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place.
|
|
5
5
|
Keywords: git,repository,manager,cli,synchronization,batch
|
|
6
6
|
Author: Anito Anto
|
|
@@ -24,6 +24,7 @@ Requires-Dist: rich>=12.0
|
|
|
24
24
|
Requires-Dist: textual>=8.2.1
|
|
25
25
|
Requires-Dist: tiktoken>=0.5.0
|
|
26
26
|
Requires-Dist: pyte>=0.8.2
|
|
27
|
+
Requires-Dist: pathspec>=1.0.0
|
|
27
28
|
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
|
28
29
|
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
29
30
|
Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
|
|
@@ -67,7 +68,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
|
|
|
67
68
|
| Command | Description |
|
|
68
69
|
| ---------------------------------------------------------- | ------------------------------------------------------ |
|
|
69
70
|
| `gitdirector console` | Open the interactive TUI dashboard |
|
|
70
|
-
| `gitdirector link PATH [--discover]` | Link a repository or discover all under a path
|
|
71
|
+
| `gitdirector link PATH [--discover]` | Link a repository or discover all under a path (no-op if none) |
|
|
71
72
|
| `gitdirector unlink PATH\|NAME [--discover]` | Unlink a repository by path, name, or all under a path |
|
|
72
73
|
| `gitdirector list` | List all tracked repositories with live status |
|
|
73
74
|
| `gitdirector status` | Show repositories with staged/unstaged files |
|
|
@@ -89,6 +90,8 @@ gitdirector link /path/to/repo
|
|
|
89
90
|
gitdirector link /path/to/folder --discover # recursively find and link all repos
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
`link --discover` succeeds without changes when the path contains no new repositories.
|
|
94
|
+
|
|
92
95
|
### console
|
|
93
96
|
|
|
94
97
|
```bash
|
|
@@ -25,7 +25,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
|
|
|
25
25
|
| Command | Description |
|
|
26
26
|
| ---------------------------------------------------------- | ------------------------------------------------------ |
|
|
27
27
|
| `gitdirector console` | Open the interactive TUI dashboard |
|
|
28
|
-
| `gitdirector link PATH [--discover]` | Link a repository or discover all under a path
|
|
28
|
+
| `gitdirector link PATH [--discover]` | Link a repository or discover all under a path (no-op if none) |
|
|
29
29
|
| `gitdirector unlink PATH\|NAME [--discover]` | Unlink a repository by path, name, or all under a path |
|
|
30
30
|
| `gitdirector list` | List all tracked repositories with live status |
|
|
31
31
|
| `gitdirector status` | Show repositories with staged/unstaged files |
|
|
@@ -47,6 +47,8 @@ gitdirector link /path/to/repo
|
|
|
47
47
|
gitdirector link /path/to/folder --discover # recursively find and link all repos
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`link --discover` succeeds without changes when the path contains no new repositories.
|
|
51
|
+
|
|
50
52
|
### console
|
|
51
53
|
|
|
52
54
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitdirector"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.4"
|
|
8
8
|
description = "A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -37,6 +37,7 @@ dependencies = [
|
|
|
37
37
|
"textual>=8.2.1",
|
|
38
38
|
"tiktoken>=0.5.0",
|
|
39
39
|
"pyte>=0.8.2",
|
|
40
|
+
"pathspec>=1.0.0",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.optional-dependencies]
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
import click
|
|
19
19
|
|
|
20
20
|
from ..integrations.tmux import capture_pane
|
|
21
|
+
from ..integrations.tmux.core import _parse_gd_session_name
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _resolve_session_name(name: str) -> str:
|
|
@@ -26,8 +27,6 @@ def _resolve_session_name(name: str) -> str:
|
|
|
26
27
|
gd-capture refuses anything that doesn't match the ``gd/{repo}/{purpose}/{N}``
|
|
27
28
|
shape — that way a typo can't be silently routed to the wrong session.
|
|
28
29
|
"""
|
|
29
|
-
from ..integrations.tmux import _parse_gd_session_name
|
|
30
|
-
|
|
31
30
|
parsed = _parse_gd_session_name(name)
|
|
32
31
|
if parsed is None:
|
|
33
32
|
raise click.BadParameter(
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
|
-
from ..integrations.tmux import
|
|
5
|
+
from ..integrations.tmux import send_key_to_session, send_text_to_session
|
|
6
|
+
from ..integrations.tmux.core import _parse_gd_session_name
|
|
6
7
|
|
|
7
8
|
SUPPORTED_KEYS = ("C-c",)
|
|
8
9
|
|
|
@@ -3,7 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
5
|
from ..manager import RepositoryManager
|
|
6
|
-
from ..storage import normalize_repository_path
|
|
7
6
|
from . import console
|
|
8
7
|
from .completion import complete_repository_names
|
|
9
8
|
|
|
@@ -17,24 +16,12 @@ def _resolve_repo(target: str) -> Path | None:
|
|
|
17
16
|
or ambiguous targets.
|
|
18
17
|
"""
|
|
19
18
|
manager = RepositoryManager()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
or candidate.is_absolute()
|
|
26
|
-
or target.startswith("~")
|
|
27
|
-
or candidate.exists()
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if is_path_like:
|
|
31
|
-
normalized = normalize_repository_path(candidate)
|
|
32
|
-
if manager.config.has_repository(normalized):
|
|
33
|
-
return normalized
|
|
34
|
-
console.print(f"\n [red]No tracked repository at path: {normalized}[/red]\n")
|
|
19
|
+
repo_path, matches, path_attempted = manager.resolve_repository_target(target)
|
|
20
|
+
if repo_path is not None:
|
|
21
|
+
return repo_path
|
|
22
|
+
if path_attempted:
|
|
23
|
+
console.print(f"\n [red]No tracked repository at path: {target}[/red]\n")
|
|
35
24
|
return None
|
|
36
|
-
|
|
37
|
-
matches = [r for r in manager.config.repositories if r.name == target]
|
|
38
25
|
if not matches:
|
|
39
26
|
console.print(f"\n [red]No tracked repository named: {target}[/red]\n")
|
|
40
27
|
return None
|
|
@@ -45,7 +32,7 @@ def _resolve_repo(target: str) -> Path | None:
|
|
|
45
32
|
f"{paths_list}\n"
|
|
46
33
|
)
|
|
47
34
|
return None
|
|
48
|
-
return
|
|
35
|
+
return None
|
|
49
36
|
|
|
50
37
|
|
|
51
38
|
def register(cli: click.Group):
|
|
@@ -26,7 +26,10 @@ def show_help():
|
|
|
26
26
|
cmd_table.add_column("desc", style="dim")
|
|
27
27
|
|
|
28
28
|
for cmd, desc in [
|
|
29
|
-
(
|
|
29
|
+
(
|
|
30
|
+
"link PATH [--discover]",
|
|
31
|
+
"Link a repository or discover all repos under a path (no-op if none)",
|
|
32
|
+
),
|
|
30
33
|
("unlink PATH|NAME [--discover]", "Unlink a repository or all repos under a path"),
|
|
31
34
|
("list", "List all tracked repositories"),
|
|
32
35
|
("status", "Show status summary and per-repo details"),
|
|
@@ -46,29 +46,20 @@ def register(cli: click.Group):
|
|
|
46
46
|
def info(target: str, full: bool):
|
|
47
47
|
"""Show file statistics for a repository."""
|
|
48
48
|
manager = RepositoryManager()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
target_lower = target.lower()
|
|
56
|
-
exact = [r for r in repos if r.name.lower() == target_lower]
|
|
57
|
-
if exact:
|
|
58
|
-
matches = exact
|
|
59
|
-
else:
|
|
60
|
-
matches = [r for r in repos if target_lower in r.name.lower()]
|
|
61
|
-
|
|
49
|
+
repo_path, matches, _path_attempted = manager.resolve_repository_target(
|
|
50
|
+
target,
|
|
51
|
+
allow_untracked_git_path=True,
|
|
52
|
+
fuzzy_names=True,
|
|
53
|
+
)
|
|
54
|
+
if repo_path is None:
|
|
62
55
|
if not matches:
|
|
63
56
|
console.print(f"\n [red]Repository '{target}' not found[/red]\n")
|
|
64
57
|
raise SystemExit(1)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
raise SystemExit(1)
|
|
71
|
-
repo_path = matches[0]
|
|
58
|
+
console.print(f"\n [red]Multiple repositories match '{target}':[/red]")
|
|
59
|
+
for match in matches:
|
|
60
|
+
console.print(f" {match}")
|
|
61
|
+
console.print()
|
|
62
|
+
raise SystemExit(1)
|
|
72
63
|
|
|
73
64
|
result = gather_repo_info(repo_path, full=full)
|
|
74
65
|
_render_info_cli(result, repo_path.name, repo_path)
|
|
@@ -28,6 +28,21 @@ def register(cli: click.Group):
|
|
|
28
28
|
else:
|
|
29
29
|
for repo_path in repos:
|
|
30
30
|
console.print(f" [green]+[/green] {repo_path}")
|
|
31
|
+
elif (
|
|
32
|
+
discover
|
|
33
|
+
and not repos
|
|
34
|
+
and message
|
|
35
|
+
in {
|
|
36
|
+
"No git repositories found",
|
|
37
|
+
"No new repositories found",
|
|
38
|
+
}
|
|
39
|
+
):
|
|
40
|
+
console.print(f" {message}")
|
|
41
|
+
for repo_path in skipped:
|
|
42
|
+
console.print(
|
|
43
|
+
f" [dim yellow]\\[skipped][/dim yellow] "
|
|
44
|
+
f"[bright_black]{repo_path}[/bright_black]"
|
|
45
|
+
)
|
|
31
46
|
else:
|
|
32
47
|
console.print(f" [red]{message}[/red]")
|
|
33
48
|
console.print()
|
|
@@ -13,7 +13,7 @@ from .constants import (
|
|
|
13
13
|
_changes_label,
|
|
14
14
|
_changes_sort_key,
|
|
15
15
|
)
|
|
16
|
-
from .panel_view import
|
|
16
|
+
from .panel_view import PaneWidget
|
|
17
17
|
from .panels import Panel, PanelStore
|
|
18
18
|
from .screens import SortMenuScreen
|
|
19
19
|
from .screens.diff import DiffReviewScreen
|
|
@@ -51,7 +51,6 @@ __all__ = [
|
|
|
51
51
|
"Panel",
|
|
52
52
|
"PanelActionMenuScreen",
|
|
53
53
|
"PanelStore",
|
|
54
|
-
"PanelViewScreen",
|
|
55
54
|
"_PANELS_SORT_COLUMN_NAMES",
|
|
56
55
|
"PullLoadingScreen",
|
|
57
56
|
"PullResultScreen",
|
|
@@ -126,25 +126,18 @@ class GitDirectorConsole(
|
|
|
126
126
|
padding: 0 1;
|
|
127
127
|
scrollbar-size-horizontal: 0;
|
|
128
128
|
}
|
|
129
|
-
#
|
|
130
|
-
|
|
129
|
+
#repo-table,
|
|
130
|
+
#sessions-table,
|
|
131
|
+
#panels-table,
|
|
132
|
+
#no-repos-message,
|
|
133
|
+
#no-sessions-message,
|
|
134
|
+
#no-panels-message {
|
|
131
135
|
display: none;
|
|
132
|
-
align: center middle;
|
|
133
|
-
color: $text-muted;
|
|
134
|
-
padding: 2 4;
|
|
135
|
-
content-align: center middle;
|
|
136
|
-
}
|
|
137
|
-
#no-sessions-message {
|
|
138
|
-
height: 1fr;
|
|
139
|
-
display: none;
|
|
140
|
-
align: center middle;
|
|
141
|
-
color: $text-muted;
|
|
142
|
-
padding: 2 4;
|
|
143
|
-
content-align: center middle;
|
|
144
136
|
}
|
|
137
|
+
#no-repos-message,
|
|
138
|
+
#no-sessions-message,
|
|
145
139
|
#no-panels-message {
|
|
146
140
|
height: 1fr;
|
|
147
|
-
display: none;
|
|
148
141
|
align: center middle;
|
|
149
142
|
color: $text-muted;
|
|
150
143
|
padding: 2 4;
|
|
@@ -204,7 +197,7 @@ class GitDirectorConsole(
|
|
|
204
197
|
Binding("1", "tab_repos", "Repos", show=False),
|
|
205
198
|
Binding("2", "tab_sessions", "Sessions", show=False),
|
|
206
199
|
Binding("3", "tab_panels", "Panels", show=False),
|
|
207
|
-
Binding("space", "toggle_group", "Toggle
|
|
200
|
+
Binding("space", "toggle_group", "Toggle", show=True),
|
|
208
201
|
Binding("n", "new_panel", "New Panel", show=True),
|
|
209
202
|
]
|
|
210
203
|
|
|
@@ -493,7 +486,7 @@ class GitDirectorConsole(
|
|
|
493
486
|
skip_config_sync: bool = False,
|
|
494
487
|
callback: Callable[[object], None] | None = None,
|
|
495
488
|
) -> None:
|
|
496
|
-
from ...integrations.tmux import _parse_gd_session_name
|
|
489
|
+
from ...integrations.tmux.core import _parse_gd_session_name
|
|
497
490
|
|
|
498
491
|
title = "session"
|
|
499
492
|
loading_hint = "waiting for session to initialize\u2026"
|
|
@@ -677,7 +670,11 @@ class GitDirectorConsole(
|
|
|
677
670
|
def action_show_info(self) -> None:
|
|
678
671
|
if self._active_tab != "repos":
|
|
679
672
|
return
|
|
680
|
-
|
|
673
|
+
group = self._get_selected_group()
|
|
674
|
+
if group is not None:
|
|
675
|
+
screen = RepoInfoScreen(f"{group.name} ({group.repo_count} repos)", group.path)
|
|
676
|
+
self.push_screen(screen)
|
|
677
|
+
self._gather_and_show_group_info(group, screen)
|
|
681
678
|
return
|
|
682
679
|
path = self._get_selected_path()
|
|
683
680
|
if path is None:
|
|
@@ -690,7 +687,7 @@ class GitDirectorConsole(
|
|
|
690
687
|
"""Open the description editor for the currently highlighted session row."""
|
|
691
688
|
if self._active_tab != "sessions":
|
|
692
689
|
return
|
|
693
|
-
from ...integrations.tmux import _get_session_description
|
|
690
|
+
from ...integrations.tmux.core import _get_session_description
|
|
694
691
|
|
|
695
692
|
table = self.query_one("#sessions-table", DataTable)
|
|
696
693
|
session_name = self._get_selected_row_key(table)
|
|
@@ -705,7 +702,7 @@ class GitDirectorConsole(
|
|
|
705
702
|
def _handle_description_edit(self, session_name: str, value: str | None) -> None:
|
|
706
703
|
if value is None:
|
|
707
704
|
return
|
|
708
|
-
from ...integrations.tmux import _set_session_description
|
|
705
|
+
from ...integrations.tmux.core import _set_session_description
|
|
709
706
|
|
|
710
707
|
_set_session_description(session_name, value)
|
|
711
708
|
for entry in self._sessions_entries:
|
|
@@ -740,6 +737,54 @@ class GitDirectorConsole(
|
|
|
740
737
|
return
|
|
741
738
|
self.call_from_thread(screen.populate, result)
|
|
742
739
|
|
|
740
|
+
@work(thread=True)
|
|
741
|
+
def _gather_and_show_group_info(self, group, screen: RepoInfoScreen) -> None:
|
|
742
|
+
worker = self._current_worker_or_none()
|
|
743
|
+
if self._background_shutdown_requested(worker):
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
from ...info import FileTypeInfo, RepoInfoResult, gather_repo_info
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
results = [gather_repo_info(path) for path in group.repositories]
|
|
750
|
+
except Exception as exc:
|
|
751
|
+
if self._background_shutdown_requested(worker):
|
|
752
|
+
return
|
|
753
|
+
self.call_from_thread(screen.show_error, str(exc))
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
file_types: dict[str, tuple[int, int, int, bool]] = {}
|
|
757
|
+
for result in results:
|
|
758
|
+
for file_type in result.file_types:
|
|
759
|
+
count, lines, tokens, has_text = file_types.get(
|
|
760
|
+
file_type.extension, (0, 0, 0, False)
|
|
761
|
+
)
|
|
762
|
+
if file_type.line_count is not None:
|
|
763
|
+
lines += file_type.line_count
|
|
764
|
+
tokens += file_type.token_count or 0
|
|
765
|
+
has_text = True
|
|
766
|
+
file_types[file_type.extension] = (count + file_type.count, lines, tokens, has_text)
|
|
767
|
+
|
|
768
|
+
aggregate = RepoInfoResult(
|
|
769
|
+
total_files=sum(result.total_files for result in results),
|
|
770
|
+
file_types=sorted(
|
|
771
|
+
[
|
|
772
|
+
FileTypeInfo(
|
|
773
|
+
extension, count, lines if has_text else None, tokens if has_text else None
|
|
774
|
+
)
|
|
775
|
+
for extension, (count, lines, tokens, has_text) in file_types.items()
|
|
776
|
+
],
|
|
777
|
+
key=lambda file_type: file_type.line_count or 0,
|
|
778
|
+
reverse=True,
|
|
779
|
+
),
|
|
780
|
+
total_lines=sum(result.total_lines for result in results),
|
|
781
|
+
total_tokens=sum(result.total_tokens for result in results),
|
|
782
|
+
max_depth=max((result.max_depth for result in results), default=0),
|
|
783
|
+
)
|
|
784
|
+
if self._background_shutdown_requested(worker):
|
|
785
|
+
return
|
|
786
|
+
self.call_from_thread(screen.populate, aggregate)
|
|
787
|
+
|
|
743
788
|
def _push_info_screen(self, name: str, path: Path, result) -> None:
|
|
744
789
|
self.push_screen(RepoInfoScreen(name, path))
|
|
745
790
|
total = len(self._results)
|
|
@@ -44,7 +44,7 @@ def detect_repo_groups(repo_paths: Iterable[Path]) -> list[RepoGroup]:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def group_session_repo_label(path: Path) -> str:
|
|
47
|
-
from ...integrations.tmux import _sanitize_repo_name
|
|
47
|
+
from ...integrations.tmux.core import _sanitize_repo_name
|
|
48
48
|
|
|
49
49
|
return f"group_{_sanitize_repo_name(path.name) or 'repo'}"
|
|
50
50
|
|
|
@@ -71,7 +71,7 @@ class ConsolePanelsMixin:
|
|
|
71
71
|
def _panel_matches_search(
|
|
72
72
|
self, panel: Panel, query: str, live_sessions: set[str] | None = None
|
|
73
73
|
) -> bool:
|
|
74
|
-
from ...integrations.tmux import make_panel_session_name
|
|
74
|
+
from ...integrations.tmux.core import make_panel_session_name
|
|
75
75
|
|
|
76
76
|
normalized_query = query.replace("×", "x")
|
|
77
77
|
live_panes = self._live_panel_pane_count(panel, live_sessions)
|
|
@@ -88,7 +88,7 @@ class ConsolePanelsMixin:
|
|
|
88
88
|
return any(normalized_query in haystack.replace("×", "x") for haystack in haystacks)
|
|
89
89
|
|
|
90
90
|
def _panel_sort_key_func(self):
|
|
91
|
-
from ...integrations.tmux import make_panel_session_name
|
|
91
|
+
from ...integrations.tmux.core import make_panel_session_name
|
|
92
92
|
|
|
93
93
|
col = self._panels_sort_column
|
|
94
94
|
if col == 1:
|
|
@@ -115,7 +115,7 @@ class ConsolePanelsMixin:
|
|
|
115
115
|
return lambda panel: panel.name.lower()
|
|
116
116
|
|
|
117
117
|
def _apply_panels_filter_and_sort(self, live_sessions: set[str] | None = None) -> None:
|
|
118
|
-
from ...integrations.tmux import _list_sessions, make_panel_session_name
|
|
118
|
+
from ...integrations.tmux.core import _list_sessions, make_panel_session_name
|
|
119
119
|
|
|
120
120
|
try:
|
|
121
121
|
table = self.query_one("#panels-table", DataTable)
|
|
@@ -128,7 +128,6 @@ class ConsolePanelsMixin:
|
|
|
128
128
|
preserved_row_key, preserved_row_index, restore_focus = self._capture_table_selection(
|
|
129
129
|
table
|
|
130
130
|
)
|
|
131
|
-
table.clear()
|
|
132
131
|
no_msg = self.query_one("#no-panels-message", Static)
|
|
133
132
|
|
|
134
133
|
panels = list(self._panels_entries)
|
|
@@ -148,12 +147,10 @@ class ConsolePanelsMixin:
|
|
|
148
147
|
|
|
149
148
|
panels.sort(key=self._panel_sort_key_func(), reverse=self._panels_sort_reverse)
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
table.display = True
|
|
156
|
-
no_msg.display = False
|
|
150
|
+
is_empty = not panels and total == 0 and not self._search_query
|
|
151
|
+
self._set_table_empty_state(table, no_msg, is_empty=is_empty)
|
|
152
|
+
table.clear()
|
|
153
|
+
if not is_empty:
|
|
157
154
|
for panel in panels:
|
|
158
155
|
filled = self._live_panel_pane_count(panel, live_sessions)
|
|
159
156
|
total_panes = panel.total_panes
|
|
@@ -279,19 +276,23 @@ class ConsolePanelsMixin:
|
|
|
279
276
|
def _do_rename_panel(self, old_name: str, new_name: str | None) -> None:
|
|
280
277
|
if new_name is None or new_name == old_name:
|
|
281
278
|
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
279
|
+
validation_message = CreatePanelScreen.validate_new_panel_name(
|
|
280
|
+
self._panel_store,
|
|
281
|
+
new_name,
|
|
282
|
+
current_name=old_name,
|
|
283
|
+
)
|
|
284
|
+
if validation_message:
|
|
285
|
+
self._update_status(validation_message)
|
|
285
286
|
return
|
|
286
287
|
self._panel_store.rename(old_name, new_name)
|
|
287
288
|
self._load_panels()
|
|
288
289
|
|
|
289
290
|
def _open_panel(self, panel_name: str) -> None:
|
|
290
|
-
from ...integrations.tmux import
|
|
291
|
+
from ...integrations.tmux import rebuild_panel_tmux_session
|
|
292
|
+
from ...integrations.tmux.core import (
|
|
291
293
|
_protect_session,
|
|
292
294
|
_session_exists,
|
|
293
295
|
make_panel_session_name,
|
|
294
|
-
rebuild_panel_tmux_session,
|
|
295
296
|
)
|
|
296
297
|
|
|
297
298
|
panel = self._panel_store.get(panel_name)
|
|
@@ -369,6 +370,11 @@ class ConsolePanelsMixin:
|
|
|
369
370
|
)
|
|
370
371
|
|
|
371
372
|
def _do_delete_panel(self, confirmed: bool, panel_name: str) -> None:
|
|
372
|
-
if confirmed:
|
|
373
|
-
|
|
374
|
-
|
|
373
|
+
if not confirmed:
|
|
374
|
+
return
|
|
375
|
+
if not self._panel_store.delete(panel_name):
|
|
376
|
+
self._update_status(
|
|
377
|
+
f"Panel '{panel_name}' could not be deleted because its tmux session is active"
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
self._load_panels()
|
|
@@ -98,8 +98,8 @@ class ConsoleReposMixin:
|
|
|
98
98
|
|
|
99
99
|
def _populate_initial_rows(self) -> None:
|
|
100
100
|
table = self.query_one("#repo-table", DataTable)
|
|
101
|
-
self.query_one("#no-repos-message", Static)
|
|
102
|
-
table
|
|
101
|
+
no_msg = self.query_one("#no-repos-message", Static)
|
|
102
|
+
self._set_table_empty_state(table, no_msg, is_empty=False)
|
|
103
103
|
preserved_row_key = None
|
|
104
104
|
preserved_row_index = None
|
|
105
105
|
restore_focus = False
|
|
@@ -133,8 +133,10 @@ class ConsoleReposMixin:
|
|
|
133
133
|
logger.debug("Failed to update repo row %s", row_key, exc_info=True)
|
|
134
134
|
|
|
135
135
|
def _show_no_repos(self) -> None:
|
|
136
|
-
self.query_one("#repo-table", DataTable)
|
|
137
|
-
self.query_one("#no-repos-message", Static)
|
|
136
|
+
table = self.query_one("#repo-table", DataTable)
|
|
137
|
+
no_msg = self.query_one("#no-repos-message", Static)
|
|
138
|
+
self._set_table_empty_state(table, no_msg, is_empty=True)
|
|
139
|
+
table.clear()
|
|
138
140
|
self._visible_repo_count = 0
|
|
139
141
|
self._visible_group_count = 0
|
|
140
142
|
self._update_status("No repositories linked")
|
|
@@ -197,19 +199,17 @@ class ConsoleReposMixin:
|
|
|
197
199
|
marker = "▸" if self._repo_group_is_collapsed(group) else "▾"
|
|
198
200
|
return f"[bold cyan]{marker} {escape(group.name)}[/bold cyan]"
|
|
199
201
|
|
|
200
|
-
def _repo_group_count_label(self,
|
|
201
|
-
repo_label = "repo" if
|
|
202
|
-
|
|
203
|
-
return f"{total} {repo_label}"
|
|
204
|
-
return f"{shown}/{total} {repo_label}"
|
|
202
|
+
def _repo_group_count_label(self, group: RepoGroup) -> str:
|
|
203
|
+
repo_label = "repo" if group.repo_count == 1 else "repos"
|
|
204
|
+
return f"[bold cyan][{group.repo_count} {repo_label}][/bold cyan]"
|
|
205
205
|
|
|
206
|
-
def _add_repo_group_row(self, table: DataTable, group: RepoGroup
|
|
206
|
+
def _add_repo_group_row(self, table: DataTable, group: RepoGroup) -> None:
|
|
207
207
|
table.add_row(
|
|
208
208
|
self._repo_group_label(group),
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
209
|
+
self._repo_group_count_label(group),
|
|
210
|
+
"",
|
|
211
|
+
"",
|
|
212
|
+
"",
|
|
213
213
|
str(group.path),
|
|
214
214
|
key=self._group_row_key(group.path),
|
|
215
215
|
)
|
|
@@ -262,7 +262,7 @@ class ConsoleReposMixin:
|
|
|
262
262
|
shown_group_count += 1
|
|
263
263
|
shown_repo_count += len(group_paths)
|
|
264
264
|
grouped_paths.update(group_paths)
|
|
265
|
-
self._add_repo_group_row(table, group
|
|
265
|
+
self._add_repo_group_row(table, group)
|
|
266
266
|
if self._repo_group_is_collapsed(group):
|
|
267
267
|
continue
|
|
268
268
|
for path in sorted(group_paths, key=lambda item: item.name.lower()):
|
|
@@ -296,7 +296,7 @@ class ConsoleReposMixin:
|
|
|
296
296
|
shown_group_count += 1
|
|
297
297
|
shown_repo_count += len(group_infos)
|
|
298
298
|
grouped_paths.update(info.path for info in group_infos)
|
|
299
|
-
self._add_repo_group_row(table, group
|
|
299
|
+
self._add_repo_group_row(table, group)
|
|
300
300
|
if self._repo_group_is_collapsed(group):
|
|
301
301
|
continue
|
|
302
302
|
group_infos.sort(key=key_func, reverse=self._sort_reverse)
|
|
@@ -314,8 +314,7 @@ class ConsoleReposMixin:
|
|
|
314
314
|
|
|
315
315
|
def _apply_filter_and_sort(self) -> None:
|
|
316
316
|
table = self.query_one("#repo-table", DataTable)
|
|
317
|
-
self.query_one("#no-repos-message", Static)
|
|
318
|
-
table.display = True
|
|
317
|
+
no_msg = self.query_one("#no-repos-message", Static)
|
|
319
318
|
preserved_row_key = None
|
|
320
319
|
preserved_row_index = None
|
|
321
320
|
restore_focus = False
|
|
@@ -328,7 +327,14 @@ class ConsoleReposMixin:
|
|
|
328
327
|
infos = list(self._results.values())
|
|
329
328
|
total = len(infos)
|
|
330
329
|
infos = self._filter_repo_infos_for_search(infos)
|
|
331
|
-
self.
|
|
330
|
+
is_empty = total == 0 and not self._repo_paths and not self._search_query
|
|
331
|
+
self._set_table_empty_state(table, no_msg, is_empty=is_empty)
|
|
332
|
+
table.clear()
|
|
333
|
+
if is_empty:
|
|
334
|
+
self._visible_repo_count = 0
|
|
335
|
+
self._visible_group_count = 0
|
|
336
|
+
else:
|
|
337
|
+
self._render_repo_info_rows(table, infos)
|
|
332
338
|
|
|
333
339
|
if self._resume_selection_tab == "repos":
|
|
334
340
|
self._restore_resume_selection("repos")
|
|
@@ -369,7 +375,7 @@ class ConsoleReposMixin:
|
|
|
369
375
|
|
|
370
376
|
msg += " ↑↓/jk navigate [enter] actions"
|
|
371
377
|
if group_count:
|
|
372
|
-
msg += " [space] toggle
|
|
378
|
+
msg += " [space] toggle"
|
|
373
379
|
msg += " g git / search s sort r refresh q quit"
|
|
374
380
|
if self._search_query:
|
|
375
381
|
msg += " [esc] clear search"
|
|
@@ -97,7 +97,6 @@ class ConsoleSessionsMixin:
|
|
|
97
97
|
preserved_row_key, preserved_row_index, restore_focus = self._capture_table_selection(
|
|
98
98
|
table
|
|
99
99
|
)
|
|
100
|
-
table.clear()
|
|
101
100
|
no_msg = self.query_one("#no-sessions-message", Static)
|
|
102
101
|
|
|
103
102
|
entries = list(self._sessions_entries)
|
|
@@ -130,12 +129,10 @@ class ConsoleSessionsMixin:
|
|
|
130
129
|
)
|
|
131
130
|
entries.sort(key=key_func, reverse=self._sessions_sort_reverse)
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
table.display = True
|
|
138
|
-
no_msg.display = False
|
|
132
|
+
is_empty = not entries and total == 0 and not self._search_query
|
|
133
|
+
self._set_table_empty_state(table, no_msg, is_empty=is_empty)
|
|
134
|
+
table.clear()
|
|
135
|
+
if not is_empty:
|
|
139
136
|
for entry in entries:
|
|
140
137
|
status = entry.get("status", "running")
|
|
141
138
|
description = _wrap_session_description(
|
|
@@ -24,6 +24,16 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class ConsoleUIHelpersMixin:
|
|
27
|
+
def _set_table_empty_state(
|
|
28
|
+
self,
|
|
29
|
+
table: DataTable,
|
|
30
|
+
empty_message: Static,
|
|
31
|
+
*,
|
|
32
|
+
is_empty: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
table.display = not is_empty
|
|
35
|
+
empty_message.display = is_empty
|
|
36
|
+
|
|
27
37
|
def _compose_status_message(self, message: str) -> str:
|
|
28
38
|
notice = getattr(self, "_update_notice", None)
|
|
29
39
|
if not notice:
|