gitdirector 1.7.3__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.3 → gitdirector-1.7.4}/PKG-INFO +1 -1
- {gitdirector-1.7.3 → gitdirector-1.7.4}/pyproject.toml +1 -1
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app.py +53 -1
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_panels.py +8 -3
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_repos.py +10 -12
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/panels.py +14 -5
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/commit.py +23 -12
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/diff.py +18 -27
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/sessions.py +31 -9
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/__init__.py +2 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/core.py +4 -2
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/panels.py +6 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/README.md +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/__init__.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/cli.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/__init__.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/autoclean.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/capture.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/cd.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/completion.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/gd_send.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/gd_tmux.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/help.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/info.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/link.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/listt.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/pull.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/reset.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/status.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/__init__.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_groups.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_sessions.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/app_ui.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/constants.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/diff_renderer.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/panel_view.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/__init__.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/_shared.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/diff_files.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/groups.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/panels.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/repos.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/session_actions.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/table_text.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/terminal_caps.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/terminal_widget.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/unlink.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/config.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/github_credential_helper.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/info.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/integrations/__init__.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/integrations/tmux/monitor.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/manager.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/repo.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/storage.py +0 -0
- {gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/ui_theme.py +0 -0
- {gitdirector-1.7.3 → 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
|
|
@@ -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" }
|
|
@@ -670,7 +670,11 @@ class GitDirectorConsole(
|
|
|
670
670
|
def action_show_info(self) -> None:
|
|
671
671
|
if self._active_tab != "repos":
|
|
672
672
|
return
|
|
673
|
-
|
|
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)
|
|
674
678
|
return
|
|
675
679
|
path = self._get_selected_path()
|
|
676
680
|
if path is None:
|
|
@@ -733,6 +737,54 @@ class GitDirectorConsole(
|
|
|
733
737
|
return
|
|
734
738
|
self.call_from_thread(screen.populate, result)
|
|
735
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
|
+
|
|
736
788
|
def _push_info_screen(self, name: str, path: Path, result) -> None:
|
|
737
789
|
self.push_screen(RepoInfoScreen(name, path))
|
|
738
790
|
total = len(self._results)
|
|
@@ -370,6 +370,11 @@ class ConsolePanelsMixin:
|
|
|
370
370
|
)
|
|
371
371
|
|
|
372
372
|
def _do_delete_panel(self, confirmed: bool, panel_name: str) -> None:
|
|
373
|
-
if confirmed:
|
|
374
|
-
|
|
375
|
-
|
|
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()
|
|
@@ -199,19 +199,17 @@ class ConsoleReposMixin:
|
|
|
199
199
|
marker = "▸" if self._repo_group_is_collapsed(group) else "▾"
|
|
200
200
|
return f"[bold cyan]{marker} {escape(group.name)}[/bold cyan]"
|
|
201
201
|
|
|
202
|
-
def _repo_group_count_label(self,
|
|
203
|
-
repo_label = "repo" if
|
|
204
|
-
|
|
205
|
-
return f"{total} {repo_label}"
|
|
206
|
-
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]"
|
|
207
205
|
|
|
208
|
-
def _add_repo_group_row(self, table: DataTable, group: RepoGroup
|
|
206
|
+
def _add_repo_group_row(self, table: DataTable, group: RepoGroup) -> None:
|
|
209
207
|
table.add_row(
|
|
210
208
|
self._repo_group_label(group),
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
209
|
+
self._repo_group_count_label(group),
|
|
210
|
+
"",
|
|
211
|
+
"",
|
|
212
|
+
"",
|
|
215
213
|
str(group.path),
|
|
216
214
|
key=self._group_row_key(group.path),
|
|
217
215
|
)
|
|
@@ -264,7 +262,7 @@ class ConsoleReposMixin:
|
|
|
264
262
|
shown_group_count += 1
|
|
265
263
|
shown_repo_count += len(group_paths)
|
|
266
264
|
grouped_paths.update(group_paths)
|
|
267
|
-
self._add_repo_group_row(table, group
|
|
265
|
+
self._add_repo_group_row(table, group)
|
|
268
266
|
if self._repo_group_is_collapsed(group):
|
|
269
267
|
continue
|
|
270
268
|
for path in sorted(group_paths, key=lambda item: item.name.lower()):
|
|
@@ -298,7 +296,7 @@ class ConsoleReposMixin:
|
|
|
298
296
|
shown_group_count += 1
|
|
299
297
|
shown_repo_count += len(group_infos)
|
|
300
298
|
grouped_paths.update(info.path for info in group_infos)
|
|
301
|
-
self._add_repo_group_row(table, group
|
|
299
|
+
self._add_repo_group_row(table, group)
|
|
302
300
|
if self._repo_group_is_collapsed(group):
|
|
303
301
|
continue
|
|
304
302
|
group_infos.sort(key=key_func, reverse=self._sort_reverse)
|
|
@@ -627,13 +627,20 @@ class PanelStore:
|
|
|
627
627
|
with advisory_file_lock(self.lock_file):
|
|
628
628
|
write_yaml_atomic(self.panels_file, data)
|
|
629
629
|
|
|
630
|
-
def _kill_panel_sessions(self, panel_names: list[str]) ->
|
|
630
|
+
def _kill_panel_sessions(self, panel_names: list[str]) -> bool:
|
|
631
631
|
if not panel_names:
|
|
632
|
-
return
|
|
633
|
-
from ...integrations.tmux import kill_panel_tmux_session
|
|
632
|
+
return True
|
|
633
|
+
from ...integrations.tmux import kill_panel_tmux_session, panel_tmux_session_exists
|
|
634
634
|
|
|
635
635
|
for panel_name in panel_names:
|
|
636
|
-
kill_panel_tmux_session(panel_name)
|
|
636
|
+
if not kill_panel_tmux_session(panel_name) and panel_tmux_session_exists(panel_name):
|
|
637
|
+
return False
|
|
638
|
+
return True
|
|
639
|
+
|
|
640
|
+
def _sync_panel_tmux_config(self) -> None:
|
|
641
|
+
from ...integrations.tmux import sync_panel_tmux_config
|
|
642
|
+
|
|
643
|
+
sync_panel_tmux_config()
|
|
637
644
|
|
|
638
645
|
def _cleanup_inner_panel_sessions(self, session_names: list[str]) -> None:
|
|
639
646
|
if not session_names:
|
|
@@ -682,10 +689,12 @@ class PanelStore:
|
|
|
682
689
|
for i, p in enumerate(self._panels):
|
|
683
690
|
if p.name == name:
|
|
684
691
|
inner_sessions = [s for s in p.panes.values() if s]
|
|
692
|
+
if not self._kill_panel_sessions([name]):
|
|
693
|
+
return False
|
|
685
694
|
self._panels.pop(i)
|
|
686
695
|
self._save()
|
|
687
|
-
self._kill_panel_sessions([name])
|
|
688
696
|
self._cleanup_inner_panel_sessions(inner_sessions)
|
|
697
|
+
self._sync_panel_tmux_config()
|
|
689
698
|
return True
|
|
690
699
|
return False
|
|
691
700
|
|
|
@@ -29,7 +29,7 @@ from textual.app import ComposeResult
|
|
|
29
29
|
from textual.binding import Binding
|
|
30
30
|
from textual.containers import Container, Vertical
|
|
31
31
|
from textual.screen import ModalScreen
|
|
32
|
-
from textual.widgets import
|
|
32
|
+
from textual.widgets import LoadingIndicator, OptionList, Static, TextArea
|
|
33
33
|
from textual.widgets.option_list import Option
|
|
34
34
|
|
|
35
35
|
from ..constants import _MODAL_BINDINGS, _MODAL_CSS
|
|
@@ -186,12 +186,16 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
|
|
|
186
186
|
}
|
|
187
187
|
#commit-message-input {
|
|
188
188
|
width: 1fr;
|
|
189
|
-
height:
|
|
189
|
+
height: auto;
|
|
190
|
+
min-height: 3;
|
|
191
|
+
max-height: 10;
|
|
190
192
|
border: none;
|
|
191
193
|
background: $boost;
|
|
192
194
|
color: $text;
|
|
193
195
|
margin: 0 0 1 0;
|
|
194
196
|
padding: 0 1;
|
|
197
|
+
scrollbar-size-vertical: 0;
|
|
198
|
+
overflow-y: hidden;
|
|
195
199
|
}
|
|
196
200
|
#commit-message-input:focus {
|
|
197
201
|
background: $boost;
|
|
@@ -232,27 +236,34 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
|
|
|
232
236
|
f"[dim]\u00b7 {self.file_count} {noun}[/dim]",
|
|
233
237
|
id="commit-message-stats",
|
|
234
238
|
)
|
|
235
|
-
yield
|
|
239
|
+
yield TextArea(
|
|
240
|
+
"",
|
|
236
241
|
placeholder="Commit message\u2026",
|
|
237
242
|
id="commit-message-input",
|
|
238
243
|
)
|
|
239
244
|
yield _CommitActionOptionList(
|
|
240
|
-
Option("[white]\u2713[/white] [bold]Commit[/bold]", id="commit"),
|
|
241
245
|
Option("[cyan]\u2191[/cyan] [bold]Commit and push[/bold]", id="commit_push"),
|
|
246
|
+
Option("[white]\u2713[/white] [bold]Commit[/bold]", id="commit"),
|
|
242
247
|
id="commit-message-action-list",
|
|
243
248
|
)
|
|
244
249
|
yield Static(
|
|
245
|
-
"type message [tab] switch [\u2191\u2193/jk] pick action"
|
|
246
|
-
" [enter] confirm [esc] cancel",
|
|
250
|
+
"type message \\[tab] switch [\u2191\u2193/jk] pick action"
|
|
251
|
+
" [ctrl+enter] confirm \\[esc] cancel",
|
|
247
252
|
id="commit-message-hint",
|
|
248
253
|
)
|
|
249
254
|
|
|
250
255
|
def on_mount(self) -> None:
|
|
251
|
-
self.query_one("#commit-message-input",
|
|
256
|
+
message = self.query_one("#commit-message-input", TextArea)
|
|
257
|
+
message.focus()
|
|
258
|
+
self.call_after_refresh(self._resize_message_input)
|
|
252
259
|
|
|
253
|
-
def
|
|
254
|
-
event.
|
|
255
|
-
|
|
260
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
261
|
+
if event.text_area.id == "commit-message-input":
|
|
262
|
+
self.call_after_refresh(self._resize_message_input)
|
|
263
|
+
|
|
264
|
+
def _resize_message_input(self) -> None:
|
|
265
|
+
message = self.query_one("#commit-message-input", TextArea)
|
|
266
|
+
message.styles.height = min(10, max(3, message.virtual_size.height))
|
|
256
267
|
|
|
257
268
|
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
258
269
|
# Click / enter on an option also confirms; the current
|
|
@@ -278,7 +289,7 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
|
|
|
278
289
|
|
|
279
290
|
def action_focus_message(self) -> None:
|
|
280
291
|
self._in_message = True
|
|
281
|
-
self.query_one("#commit-message-input",
|
|
292
|
+
self.query_one("#commit-message-input", TextArea).focus()
|
|
282
293
|
|
|
283
294
|
def action_focus_toggle(self) -> None:
|
|
284
295
|
"""Toggle focus between the message input and the action picker."""
|
|
@@ -301,7 +312,7 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
|
|
|
301
312
|
self.query_one("#commit-message-action-list", OptionList).action_cursor_up()
|
|
302
313
|
|
|
303
314
|
def _message(self) -> str:
|
|
304
|
-
return self.query_one("#commit-message-input",
|
|
315
|
+
return self.query_one("#commit-message-input", TextArea).text.strip()
|
|
305
316
|
|
|
306
317
|
def _message_valid(self) -> bool:
|
|
307
318
|
msg = self._message()
|
|
@@ -114,21 +114,20 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
114
114
|
#diff-files-pane {
|
|
115
115
|
width: 52;
|
|
116
116
|
height: 1fr;
|
|
117
|
-
border-right: solid $
|
|
117
|
+
border-right: solid $boost;
|
|
118
118
|
padding: 0 0;
|
|
119
119
|
background: $surface;
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#diff-content-pane.--files-focused {
|
|
128
|
-
border-left: solid $accent;
|
|
121
|
+
.diff-pane-label {
|
|
122
|
+
height: 1;
|
|
123
|
+
padding: 0 1;
|
|
124
|
+
background: $boost;
|
|
125
|
+
color: $text-muted;
|
|
126
|
+
text-style: bold;
|
|
129
127
|
}
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
.diff-pane-label.--focused {
|
|
129
|
+
background: $accent;
|
|
130
|
+
color: $text;
|
|
132
131
|
}
|
|
133
132
|
#diff-files-list {
|
|
134
133
|
width: 1fr;
|
|
@@ -149,12 +148,6 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
149
148
|
height: 1fr;
|
|
150
149
|
padding: 0 0;
|
|
151
150
|
}
|
|
152
|
-
#diff-content-pane.--files-focused {
|
|
153
|
-
border-left: solid $accent;
|
|
154
|
-
}
|
|
155
|
-
#diff-content-pane.--diff-focused {
|
|
156
|
-
border-left: solid $secondary;
|
|
157
|
-
}
|
|
158
151
|
#diff-content-scroll {
|
|
159
152
|
width: 1fr;
|
|
160
153
|
height: 1fr;
|
|
@@ -235,8 +228,10 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
235
228
|
yield Static("", id="diff-summary")
|
|
236
229
|
with Horizontal(id="diff-body"):
|
|
237
230
|
with Vertical(id="diff-files-pane"):
|
|
231
|
+
yield Static("FILES", id="diff-files-pane-label", classes="diff-pane-label")
|
|
238
232
|
yield FileTileList(id="diff-files-list")
|
|
239
233
|
with Vertical(id="diff-content-pane"):
|
|
234
|
+
yield Static("DIFF", id="diff-content-pane-label", classes="diff-pane-label")
|
|
240
235
|
with _DiffContentScroll(id="diff-content-scroll"):
|
|
241
236
|
yield Static(id="diff-content")
|
|
242
237
|
with Vertical(id="diff-loading"):
|
|
@@ -244,11 +239,11 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
244
239
|
yield Static("Loading diff\u2026", id="diff-loading-text")
|
|
245
240
|
yield Static("", id="diff-empty")
|
|
246
241
|
yield Static(
|
|
247
|
-
"[bold]tab[/bold]
|
|
242
|
+
"[bold]tab[/bold] toggle focus "
|
|
248
243
|
"[bold]j/k[/bold] line "
|
|
249
244
|
"[bold]J/K[/bold] page "
|
|
250
245
|
"[bold]h/l[/bold] horizontal "
|
|
251
|
-
"[bold]
|
|
246
|
+
"[bold]\\[ ][/bold] cycle "
|
|
252
247
|
"[bold]g[/bold] commit "
|
|
253
248
|
"[bold]r[/bold] refresh "
|
|
254
249
|
"[bold]esc[/bold] close",
|
|
@@ -495,17 +490,16 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
495
490
|
if not self._files:
|
|
496
491
|
target = _FOCUS_FILES
|
|
497
492
|
try:
|
|
498
|
-
# Toggle the focus-class on the two panes so the
|
|
499
|
-
# divider border can shift colour to signal which side
|
|
500
|
-
# has focus. We touch *both* panes on every focus
|
|
501
|
-
# change so the stale classes from the previous focus
|
|
502
|
-
# don't linger.
|
|
503
493
|
files_pane = self.query_one("#diff-files-pane")
|
|
504
494
|
content_pane = self.query_one("#diff-content-pane")
|
|
495
|
+
files_label = self.query_one("#diff-files-pane-label")
|
|
496
|
+
content_label = self.query_one("#diff-content-pane-label")
|
|
505
497
|
files_pane.set_class(False, "--files-focused")
|
|
506
498
|
files_pane.set_class(False, "--diff-focused")
|
|
507
499
|
content_pane.set_class(False, "--files-focused")
|
|
508
500
|
content_pane.set_class(False, "--diff-focused")
|
|
501
|
+
files_label.set_class(target == _FOCUS_FILES, "--focused")
|
|
502
|
+
content_label.set_class(target == _FOCUS_DIFF, "--focused")
|
|
509
503
|
if target == _FOCUS_DIFF:
|
|
510
504
|
content_pane.set_class(True, "--diff-focused")
|
|
511
505
|
files_pane.set_class(True, "--diff-focused")
|
|
@@ -568,9 +562,6 @@ class DiffReviewScreen(ModalScreen[None]):
|
|
|
568
562
|
if new_index == files_list.index:
|
|
569
563
|
return
|
|
570
564
|
files_list.index = new_index
|
|
571
|
-
if self._focus_target != _FOCUS_FILES:
|
|
572
|
-
self._focus_target = _FOCUS_FILES
|
|
573
|
-
self._apply_focus()
|
|
574
565
|
|
|
575
566
|
def action_cursor_down(self) -> None:
|
|
576
567
|
if self._focus_target == _FOCUS_FILES or not self._files:
|
|
@@ -5,9 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
8
9
|
from textual.containers import Vertical
|
|
9
10
|
from textual.screen import ModalScreen
|
|
10
|
-
from textual.widgets import
|
|
11
|
+
from textual.widgets import OptionList, Static, TextArea
|
|
11
12
|
from textual.widgets.option_list import Option
|
|
12
13
|
|
|
13
14
|
from ..constants import _MODAL_BINDINGS, _MODAL_CSS
|
|
@@ -143,6 +144,13 @@ class SelectSessionScreen(ModalScreen[str | None]):
|
|
|
143
144
|
__all__ = ["EditSessionDescriptionScreen", "RemoveSessionScreen", "SelectSessionScreen"]
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
class DescriptionTextArea(TextArea):
|
|
148
|
+
BINDINGS = [*TextArea.BINDINGS, Binding("enter", "submit", show=False, priority=True)]
|
|
149
|
+
|
|
150
|
+
def action_submit(self) -> None:
|
|
151
|
+
self.screen.action_submit()
|
|
152
|
+
|
|
153
|
+
|
|
146
154
|
class EditSessionDescriptionScreen(ModalScreen[str | None]):
|
|
147
155
|
"""Modal for editing the description stored on a tmux session.
|
|
148
156
|
|
|
@@ -165,12 +173,16 @@ class EditSessionDescriptionScreen(ModalScreen[str | None]):
|
|
|
165
173
|
}
|
|
166
174
|
EditSessionDescriptionScreen #description-input {
|
|
167
175
|
width: 1fr;
|
|
168
|
-
height:
|
|
176
|
+
height: auto;
|
|
177
|
+
min-height: 3;
|
|
178
|
+
max-height: 10;
|
|
169
179
|
border: none;
|
|
170
180
|
background: $boost;
|
|
171
181
|
color: $text;
|
|
172
182
|
margin: 1 0;
|
|
173
183
|
padding: 0 1;
|
|
184
|
+
scrollbar-size-vertical: 0;
|
|
185
|
+
overflow-y: hidden;
|
|
174
186
|
}
|
|
175
187
|
#description-session-name {
|
|
176
188
|
text-align: center;
|
|
@@ -190,22 +202,32 @@ class EditSessionDescriptionScreen(ModalScreen[str | None]):
|
|
|
190
202
|
with Vertical(id="menu-container"):
|
|
191
203
|
yield Static("[bold white]Edit Description[/bold white]", id="menu-title")
|
|
192
204
|
yield Static(f"[dim]{self.session_name}[/dim]", id="description-session-name")
|
|
193
|
-
yield
|
|
194
|
-
|
|
205
|
+
yield DescriptionTextArea(
|
|
206
|
+
self._initial_value,
|
|
195
207
|
placeholder="description (leave empty to reset to '-')",
|
|
196
208
|
id="description-input",
|
|
197
209
|
)
|
|
198
210
|
yield Static("\\[enter] save \\[esc] cancel", id="menu-hint")
|
|
199
211
|
|
|
200
212
|
def on_mount(self) -> None:
|
|
201
|
-
inp = self.query_one("#description-input",
|
|
213
|
+
inp = self.query_one("#description-input", TextArea)
|
|
202
214
|
inp.focus()
|
|
203
215
|
if self._initial_value:
|
|
204
|
-
|
|
216
|
+
lines = self._initial_value.splitlines() or [""]
|
|
217
|
+
inp.move_cursor((len(lines) - 1, len(lines[-1])))
|
|
218
|
+
self.call_after_refresh(self._resize_description_input)
|
|
219
|
+
|
|
220
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
221
|
+
if event.text_area.id == "description-input":
|
|
222
|
+
self.call_after_refresh(self._resize_description_input)
|
|
223
|
+
|
|
224
|
+
def _resize_description_input(self) -> None:
|
|
225
|
+
inp = self.query_one("#description-input", TextArea)
|
|
226
|
+
inp.styles.height = min(10, max(3, inp.virtual_size.height))
|
|
205
227
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
self.dismiss(
|
|
228
|
+
def action_submit(self) -> None:
|
|
229
|
+
inp = self.query_one("#description-input", TextArea)
|
|
230
|
+
self.dismiss(inp.text.strip())
|
|
209
231
|
|
|
210
232
|
def action_cancel(self) -> None:
|
|
211
233
|
self.dismiss(None)
|
|
@@ -78,6 +78,7 @@ from .panels import (
|
|
|
78
78
|
cleanup_temp_panel_tmux_session,
|
|
79
79
|
ensure_temp_panel_tmux_session,
|
|
80
80
|
kill_panel_tmux_session,
|
|
81
|
+
panel_tmux_session_exists,
|
|
81
82
|
rebuild_panel_tmux_session,
|
|
82
83
|
)
|
|
83
84
|
|
|
@@ -93,6 +94,7 @@ __all__ = [
|
|
|
93
94
|
"get_all_session_statuses",
|
|
94
95
|
"kill_all_gd_sessions",
|
|
95
96
|
"kill_panel_tmux_session",
|
|
97
|
+
"panel_tmux_session_exists",
|
|
96
98
|
"kill_tmux_session",
|
|
97
99
|
"launch_command_in_tmux_session",
|
|
98
100
|
"list_all_gd_sessions",
|
|
@@ -460,7 +460,7 @@ def kill_all_gd_sessions() -> list[str]:
|
|
|
460
460
|
session_names.update(
|
|
461
461
|
session_name
|
|
462
462
|
for session_name in _list_sessions()
|
|
463
|
-
if _is_persistent_panel_session(session_name)
|
|
463
|
+
if _is_persistent_panel_session(session_name) or _is_temp_panel_session(session_name)
|
|
464
464
|
)
|
|
465
465
|
except (TmuxError, OSError, ValueError):
|
|
466
466
|
logger.debug("Failed to enumerate GitDirector tmux sessions", exc_info=True)
|
|
@@ -582,7 +582,9 @@ def _sanitize_panel_name(name: str) -> str:
|
|
|
582
582
|
|
|
583
583
|
def _is_temp_panel_session(session_name: str) -> bool:
|
|
584
584
|
parts = session_name.split("/")
|
|
585
|
-
|
|
585
|
+
if parts[:3] != ["gd", "temp", "panel"]:
|
|
586
|
+
return False
|
|
587
|
+
return len(parts) > 4 or (len(parts) == 4 and parts[3].startswith("build-"))
|
|
586
588
|
|
|
587
589
|
|
|
588
590
|
def _is_persistent_panel_session(session_name: str) -> bool:
|
|
@@ -88,6 +88,12 @@ def kill_panel_tmux_session(panel_name: str) -> bool:
|
|
|
88
88
|
return kill_tmux_session(make_panel_session_name(panel_name))
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def panel_tmux_session_exists(panel_name: str) -> bool:
|
|
92
|
+
if not isinstance(panel_name, str) or not panel_name:
|
|
93
|
+
raise ValueError("panel_tmux_session_exists requires a non-empty panel name")
|
|
94
|
+
return _session_exists(make_panel_session_name(panel_name))
|
|
95
|
+
|
|
96
|
+
|
|
91
97
|
def _kill_tmux_session_by_name(intended_name: str) -> bool:
|
|
92
98
|
"""Kill a tmux session by the name we requested, tolerating ``.`` -> ``_``.
|
|
93
99
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gitdirector-1.7.3 → gitdirector-1.7.4}/src/gitdirector/commands/tui/screens/session_actions.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|