Skip to content

Commit ea67d1f

Browse files
committed
add branch column, horizontal scrolling, scrollable header row
1 parent 6c133b9 commit ea67d1f

1 file changed

Lines changed: 71 additions & 39 deletions

File tree

scripts/ajay-github-repo-manager

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,18 @@ def gh_api(endpoint):
9595

9696

9797
def get_head_info(repo):
98-
"""Get HEAD SHA and commit date. Returns (sha, date_str) or (None, None)."""
98+
"""Get HEAD SHA, commit date, and branch. Returns (sha, date_str, branch) or (None, None, None)."""
99+
branch = "main"
99100
data = gh_api(f"repos/{repo}/commits/main")
100101
if not data or "sha" not in data:
102+
branch = "master"
101103
data = gh_api(f"repos/{repo}/commits/master")
102104
if data and "sha" in data:
103105
sha = data["sha"]
104106
date_raw = data.get("commit", {}).get("committer", {}).get("date", "")
105-
# "2026-04-15T12:37:52Z" -> "2026-04-15 12:37"
106107
date_str = date_raw.replace("T", " ")[:16] if date_raw else ""
107-
return sha, date_str
108-
return None, None
108+
return sha, date_str, branch
109+
return None, None, None
109110

110111

111112
def get_ci_runs(repo):
@@ -263,15 +264,17 @@ def sub_status_text(pinned, latest):
263264

264265
COL_OWNER = 12
265266
COL_REPO = 24
267+
COL_BRANCH = 9
266268
COL_COMMIT = 9
267269
COL_DATE = 18
268270

269271

270-
def repo_label(owner, name, head=None, date=None, ci_ok=None, subs_ok=None):
272+
def repo_label(owner, name, branch=None, head=None, date=None, ci_ok=None, subs_ok=None):
271273
"""Build a rich Text label for a repo tree node with fixed-width columns."""
272274
t = Text()
273275
t.append(f"{owner:<{COL_OWNER}s}", style="dim")
274276
t.append(f"{name:<{COL_REPO}s}", style="bold")
277+
t.append(f"{branch or '…':<{COL_BRANCH}s}", style="dim")
275278
t.append(f"{head[:7] if head else '…':<{COL_COMMIT}s}", style="dim")
276279
t.append(f"{date or '…':<{COL_DATE}s}", style="dim")
277280
if ci_ok is None and subs_ok is None:
@@ -285,21 +288,23 @@ def repo_label(owner, name, head=None, date=None, ci_ok=None, subs_ok=None):
285288
return t
286289

287290

288-
SORT_COLUMNS = ["owner", "repo", "commit", "updated", "status"]
289-
COL_HEADER_PAD = 4 # left padding on #col-header
291+
SORT_COLUMNS = ["owner", "repo", "branch", "commit", "updated", "status"]
292+
COL_HEADER_PAD = 7 # tree root indent + padding in header label
290293

291294

292295
def col_header_text(sort_key="owner", sort_ascending=True):
293296
"""Build the column header row with sort indicator."""
294297
arrow = " ▲" if sort_ascending else " ▼"
298+
# Pad to align with child nodes (tree guide adds ~5 chars for children vs root)
299+
t = Text(" ")
295300
cols = [
296301
("owner", COL_OWNER),
297302
("repo", COL_REPO),
303+
("branch", COL_BRANCH),
298304
("commit", COL_COMMIT),
299305
("updated", COL_DATE),
300306
("status", 0),
301307
]
302-
t = Text()
303308
for name, width in cols:
304309
label = name + (arrow if name == sort_key else "")
305310
if width:
@@ -312,7 +317,7 @@ def col_header_text(sort_key="owner", sort_ascending=True):
312317
def col_from_click_x(x):
313318
"""Determine which column was clicked based on x offset."""
314319
x -= COL_HEADER_PAD
315-
boundaries = [COL_OWNER, COL_REPO, COL_COMMIT, COL_DATE]
320+
boundaries = [COL_OWNER, COL_REPO, COL_BRANCH, COL_COMMIT, COL_DATE]
316321
pos = 0
317322
for i, w in enumerate(boundaries):
318323
pos += w
@@ -381,7 +386,9 @@ class RepoTree(Tree):
381386
event.prevent_default()
382387
event.stop()
383388
elif event.key == "left":
384-
if node.is_expanded:
389+
if node == self.root:
390+
pass # don't collapse root (header row)
391+
elif node.is_expanded:
385392
node.collapse()
386393
elif node.parent and node.parent != self.root:
387394
self.select_node(node.parent)
@@ -405,12 +412,11 @@ class RepoStatusApp(App):
405412
background: $surface;
406413
}
407414
#col-header {
408-
padding: 0 2 0 4;
409-
color: $text 60%;
410-
height: auto;
415+
display: none;
411416
}
412417
Tree {
413418
padding: 0 2;
419+
overflow-x: auto;
414420
}
415421
Tree > .tree--cursor {
416422
background: $accent 30%;
@@ -431,6 +437,8 @@ class RepoStatusApp(App):
431437
Binding("ctrl+right", "expand_level", "Level +", show=False),
432438
Binding("ctrl+left", "collapse_level", "Level -", show=False),
433439
Binding("s", "toggle_submodules", "Toggle subs"),
440+
Binding("alt+right", "scroll_right_fast", "Scroll right", show=False),
441+
Binding("alt+left", "scroll_left_fast", "Scroll left", show=False),
434442
]
435443

436444
async def action_quit(self) -> None:
@@ -444,8 +452,9 @@ class RepoStatusApp(App):
444452
def compose(self) -> ComposeResult:
445453
yield Header()
446454
yield Static(col_header_text(), id="col-header")
447-
tree: RepoTree = RepoTree("Repos", id="repo-tree")
448-
tree.show_root = False
455+
tree: RepoTree = RepoTree(col_header_text(), id="repo-tree")
456+
tree.show_root = True
457+
tree.root.expand()
449458
tree.guide_depth = 3
450459
yield tree
451460
yield Static("", id="level-indicator")
@@ -534,6 +543,9 @@ class RepoStatusApp(App):
534543
sub.collapse()
535544
else: # level 3
536545
node.expand_all()
546+
# Keep root (header row) always expanded
547+
tree = self.query_one("#repo-tree", RepoTree)
548+
tree.root.expand()
537549
self._update_level_indicator()
538550

539551
def action_toggle_submodules(self) -> None:
@@ -554,26 +566,41 @@ class RepoStatusApp(App):
554566
sub_node.parent.expand()
555567
sub_node.expand_all()
556568

569+
def action_scroll_right_fast(self) -> None:
570+
tree = self.query_one("#repo-tree", RepoTree)
571+
tree.scroll_relative(x=10)
572+
573+
def action_scroll_left_fast(self) -> None:
574+
tree = self.query_one("#repo-tree", RepoTree)
575+
tree.scroll_relative(x=-10)
576+
557577
def on_click(self, event) -> None:
558-
"""Handle clicks on the column header for sorting."""
578+
"""Handle clicks on the column header (tree root) for sorting."""
579+
tree = self.query_one("#repo-tree", RepoTree)
580+
# Check if click is on the root node (header row) by checking y position
581+
# The root node is at y=0 in the tree's content area
559582
widget = event.widget
560-
# Walk up to find if click was on #col-header
561583
while widget is not None:
562-
if hasattr(widget, "id") and widget.id == "col-header":
563-
col = col_from_click_x(event.x)
564-
if col == self._sort_key:
565-
self._sort_ascending = not self._sort_ascending
566-
else:
567-
self._sort_key = col
568-
self._sort_ascending = True
569-
self._update_col_header()
570-
self._rebuild_tree()
584+
if hasattr(widget, "id") and widget.id == "repo-tree":
585+
# Determine if click was on the root/header line
586+
_, scroll_y = tree.scroll_offset
587+
click_line = event.y + scroll_y
588+
if click_line == 0: # root node is line 0
589+
col = col_from_click_x(event.x)
590+
if col == self._sort_key:
591+
self._sort_ascending = not self._sort_ascending
592+
else:
593+
self._sort_key = col
594+
self._sort_ascending = True
595+
self._update_col_header()
596+
self._rebuild_tree()
571597
return
572598
widget = getattr(widget, "parent", None)
573599

574600
def _update_col_header(self) -> None:
575-
header = self.query_one("#col-header", Static)
576-
header.update(col_header_text(self._sort_key, self._sort_ascending))
601+
label = col_header_text(self._sort_key, self._sort_ascending)
602+
tree = self.query_one("#repo-tree", RepoTree)
603+
tree.root.set_label(label)
577604

578605
def _sort_value(self, repo, key):
579606
"""Extract a sortable value for a repo by column key."""
@@ -583,6 +610,8 @@ class RepoStatusApp(App):
583610
return owner.lower()
584611
elif key == "repo":
585612
return name.lower()
613+
elif key == "branch":
614+
return data.get("branch") or ""
586615
elif key == "commit":
587616
return data.get("sha") or ""
588617
elif key == "updated":
@@ -619,13 +648,14 @@ class RepoStatusApp(App):
619648
data = self._repo_data.get(repo, {})
620649
sha = data.get("sha")
621650
date = data.get("date")
651+
branch = data.get("branch")
622652
ci_runs = data.get("ci_runs")
623653
submodules = data.get("submodules")
624654
sha_cache = data.get("sha_cache", {})
625655
ci_ok_val = data.get("ci_ok")
626656
subs_ok_val = data.get("subs_ok")
627657

628-
label = repo_label(owner, name, sha, date, ci_ok_val, subs_ok_val)
658+
label = repo_label(owner, name, branch, sha, date, ci_ok_val, subs_ok_val)
629659
node = tree.root.add(label, data={"repo": repo}, expand=False)
630660
self._repo_nodes[repo] = node
631661

@@ -671,7 +701,7 @@ class RepoStatusApp(App):
671701

672702
# Phase 1: HEAD + CI (parallel) — update labels as they arrive
673703
update_status(f"Fetching HEAD + CI… (0/{total * 2})")
674-
head_cache = {} # repo -> (sha, date_str)
704+
head_cache = {} # repo -> (sha, date_str, branch)
675705
ci_cache = {}
676706
ready = set()
677707

@@ -690,20 +720,21 @@ class RepoStatusApp(App):
690720
return
691721
kind, repo = all_futures[future]
692722
if kind == "head":
693-
head_cache[repo] = future.result() # (sha, date)
723+
head_cache[repo] = future.result() # (sha, date, branch)
694724
else:
695725
ci_cache[repo] = future.result()
696726
done += 1
697727
update_status(f"Fetching HEAD + CI… ({done}/{total * 2})")
698728

699729
if repo not in ready and repo in head_cache and repo in ci_cache:
700730
ready.add(repo)
701-
sha, date = head_cache[repo]
731+
sha, date, branch = head_cache[repo]
702732
self.call_from_thread(
703733
self._update_repo_label,
704734
repo,
705735
sha,
706736
date,
737+
branch,
707738
ci_cache.get(repo, []),
708739
)
709740

@@ -758,7 +789,7 @@ class RepoStatusApp(App):
758789
pool.shutdown(wait=False, cancel_futures=True)
759790
return
760791
ext_repo = futures[future]
761-
sha, date = future.result()
792+
sha, date, branch = future.result()
762793
if sha:
763794
sha_cache[ext_repo] = sha
764795
done += 1
@@ -813,23 +844,23 @@ class RepoStatusApp(App):
813844
text = f"{frame} {text}"
814845
self.sub_title = text
815846

816-
def _update_repo_label(self, repo, head, date, ci_runs) -> None:
817-
"""Update just the label (HEAD + CI + date), submodules still unknown."""
847+
def _update_repo_label(self, repo, head, date, branch, ci_runs) -> None:
848+
"""Update just the label (HEAD + CI + date + branch), submodules still unknown."""
818849
node = self._repo_nodes[repo]
819850
owner, name = repo.split("/")
820851
ci_ok_val = ci_ok(ci_runs)
821-
node.set_label(repo_label(owner, name, head, date, ci_ok_val))
852+
node.set_label(repo_label(owner, name, branch, head, date, ci_ok_val))
822853
# Cache for sorting
823854
self._repo_data.setdefault(repo, {}).update(
824-
sha=head, date=date, ci_runs=ci_runs, ci_ok=ci_ok_val
855+
sha=head, date=date, branch=branch, ci_runs=ci_runs, ci_ok=ci_ok_val
825856
)
826857

827858
def _update_repo_node(
828859
self, repo, sha_cache, head_cache, ci_runs, submodules
829860
) -> None:
830861
node = self._repo_nodes[repo]
831862
owner, name = repo.split("/")
832-
head_sha, head_date = head_cache.get(repo, (None, None))
863+
head_sha, head_date, head_branch = head_cache.get(repo, (None, None, None))
833864

834865
# Compute submodule staleness
835866
stale = 0
@@ -844,13 +875,14 @@ class RepoStatusApp(App):
844875
subs_ok_val = (stale == 0) if submodules else None
845876
ci_ok_val = ci_ok(ci_runs)
846877

847-
node.set_label(repo_label(owner, name, head_sha, head_date, ci_ok_val, subs_ok_val))
878+
node.set_label(repo_label(owner, name, head_branch, head_sha, head_date, ci_ok_val, subs_ok_val))
848879
node.remove_children()
849880

850881
# Cache for sorting
851882
self._repo_data[repo] = {
852883
"sha": head_sha,
853884
"date": head_date,
885+
"branch": head_branch,
854886
"ci_runs": ci_runs,
855887
"ci_ok": ci_ok_val,
856888
"submodules": submodules,

0 commit comments

Comments
 (0)