1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""Default landing page for the pgt TUI."""
from __future__ import annotations
import time
from pathlib import Path
from typing import Literal
from pyratatui import Constraint, Direction, Frame, Layout, Paragraph, Rect
from pygittools.actions import find_project_root, load_action_sets
from pygittools.tui.branches import current_branch_name
from pygittools.tui.changes_list import ChangesList
from pygittools.tui.commit_draft import load_commit_draft
from pygittools.tui.git_colors import current_theme, focus_style
from pygittools.tui.input_context import InputContext, InputMode, input_context, nav_context
from pygittools.tui.keys import KeyLike, is_quit
from pygittools.tui.log_list import CommitLogList
from pygittools.tui.project_label import repo_project_label
from pygittools.tui.tabs import render_tab_row
from pygittools.tui.tasks_list import TasksBoardList
from pygittools.tui.types import Page, PageAction, PageContext, PageResult
from pygittools.tui.worktrees_list import WorktreesList
_TABS: tuple[str, ...] = ("Status", "Log", "Tasks", "Worktrees")
_HeaderFocus = Literal["changes", "repo", "branch", "actions"]
_HomeTab = Literal["status", "log", "tasks", "worktrees"]
_TAB_KEYS: tuple[_HomeTab, ...] = ("status", "log", "tasks", "worktrees")
_IDLE_REFRESH_S = 10.0
class HomePage:
title = "pgt"
def __init__(self) -> None:
self._ctx: PageContext | None = None
self._changes = ChangesList()
self._log = CommitLogList()
self._tasks = TasksBoardList()
self._worktrees = WorktreesList()
self._active_tab: _HomeTab = "status"
self._header_focus: _HeaderFocus = "changes"
self._input_mode: InputMode = nav_context()
self._action_sets: tuple[str, ...] = ()
self._last_refresh_at = 0.0
@property
def header_focus(self) -> _HeaderFocus:
return self._header_focus
def set_header_focus(self, focus: _HeaderFocus) -> None:
self._header_focus = focus
@property
def page_context(self) -> PageContext | None:
return self._ctx
@property
def changes(self) -> ChangesList:
return self._changes
@property
def log(self) -> CommitLogList:
return self._log
@property
def tasks(self) -> TasksBoardList:
return self._tasks
@property
def worktrees(self) -> WorktreesList:
return self._worktrees
@property
def active_tab(self) -> _HomeTab:
return self._active_tab
@property
def has_actions(self) -> bool:
return bool(self._action_sets)
def set_active_tab(self, tab: _HomeTab) -> None:
self._active_tab = tab
if tab != "status":
if isinstance(self._input_mode, InputContext):
self._input_mode = nav_context()
if self._header_focus == "actions":
self._header_focus = "changes"
def refresh(self) -> None:
self._refresh()
def _refresh(self) -> None:
ctx = self._ctx
if ctx is None:
return
self._changes.refresh(ctx.repo)
self._log.refresh(ctx.repo)
self._tasks.refresh(ctx.repo)
self._worktrees.refresh(ctx.repo)
self._action_sets = _action_set_names(ctx)
self._last_refresh_at = time.monotonic()
def on_enter(self, ctx: PageContext) -> None:
self._ctx = ctx
self._active_tab = "status"
self._header_focus = "changes"
self._input_mode = nav_context()
self._changes.set_commit_message(load_commit_draft(ctx.repo_path, ctx.repo))
self._refresh()
def on_idle(self) -> None:
if time.monotonic() - self._last_refresh_at >= _IDLE_REFRESH_S:
self._refresh()
def status_text(self) -> str:
return self._input_mode.status_hint(self)
def draw(self, frame: Frame, area: Rect) -> None:
ctx = self._ctx
if ctx is None or area.height <= 0:
return
project_label = repo_project_label(ctx.repo_path)
branch = current_branch_name(ctx.repo)
show_actions = self._active_tab == "status"
constraints = [Constraint.length(1), Constraint.length(1)]
if show_actions:
constraints.append(Constraint.length(1))
constraints.append(Constraint.fill(1))
chunks = Layout().direction(Direction.Vertical).constraints(constraints).split(area)
try:
active_index = _TAB_KEYS.index(self._active_tab)
except ValueError:
active_index = 0
render_tab_row(
frame,
chunks[0],
project_label,
_TABS,
active_index,
prefix_focused=self._header_focus == "repo",
)
theme = current_theme()
branch_style = theme.branch
if self._header_focus == "branch":
branch_style = focus_style(branch_style)
branch_label = f"Branch: {branch}"
if self._active_tab == "tasks":
branch_label = f"{branch_label} — [Sync tasks]"
elif self._active_tab == "worktrees":
branch_label = f"{branch_label} — [Add worktree]"
frame.render_widget(Paragraph.from_string(branch_label).style(branch_style), chunks[1])
content = chunks[-1]
if show_actions:
if self._action_sets:
actions_label = f"Actions: {', '.join(self._action_sets)}"
actions_style = theme.header
else:
actions_label = "No Action Groups"
actions_style = theme.log_date.dim()
if self._header_focus == "actions":
actions_style = focus_style(actions_style)
frame.render_widget(Paragraph.from_string(actions_label).style(actions_style), chunks[2])
content_highlight = self._header_focus == "changes"
commit_input = isinstance(self._input_mode, InputContext)
if self._active_tab == "status":
self._changes.draw(
frame,
content,
highlight=content_highlight,
commit_input=commit_input,
)
elif self._active_tab == "log":
self._log.draw(frame, content, highlight=content_highlight)
elif self._active_tab == "tasks":
self._tasks.draw(frame, content, highlight=content_highlight)
else:
self._worktrees.draw(frame, content, highlight=content_highlight)
def handle_key(self, key: KeyLike) -> PageResult:
if is_quit(key) and not isinstance(self._input_mode, InputContext):
return PageResult(action=PageAction.QUIT)
dispatch = self._input_mode.dispatch_key(self, key)
if dispatch.next_mode == "input":
self._input_mode = input_context()
elif dispatch.next_mode == "nav":
self._input_mode = nav_context()
if dispatch.refresh_log and self._ctx is not None:
self._log.refresh(self._ctx.repo)
if dispatch.page_result is not None:
return dispatch.page_result
return PageResult()
def home_page() -> Page:
return HomePage()
def _action_set_names(ctx: PageContext) -> tuple[str, ...]:
workdir = Path(ctx.repo.workdir) if ctx.repo.workdir else Path(ctx.repo_path)
if workdir.name == ".git":
workdir = workdir.parent
root = find_project_root(workdir)
if root is None:
return ()
try:
return tuple(action_set.name for action_set in load_action_sets(root))
except (OSError, ValueError):
return ()