Coverage for src/workstack/status/renderers/simple.py: 72%
127 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
1"""Simple text-based status renderer."""
3import click
5from workstack.status.models.status_data import StatusData
8class SimpleRenderer:
9 """Renders status information as simple formatted text."""
11 def render(self, status: StatusData) -> None:
12 """Render status data to console.
14 Args:
15 status: Status data to render
16 """
17 self._render_header(status)
18 self._render_plan(status)
19 self._render_stack(status)
20 self._render_pr_status(status)
21 self._render_git_status(status)
22 self._render_related_worktrees(status)
24 def _render_file_list(self, files: list[str], *, max_files: int = 3) -> None:
25 """Render a list of files with truncation.
27 Args:
28 files: List of file paths
29 max_files: Maximum number of files to display
30 """
31 for file in files[:max_files]:
32 click.echo(f" {file}")
34 if len(files) > max_files:
35 remaining = len(files) - max_files
36 click.echo(
37 click.style(
38 f" ... and {remaining} more",
39 fg="white",
40 dim=True,
41 )
42 )
44 def _render_header(self, status: StatusData) -> None:
45 """Render worktree header section.
47 Args:
48 status: Status data
49 """
50 wt = status.worktree_info
52 # Title
53 name_color = "green" if wt.is_root else "cyan"
54 click.echo(click.style(f"Worktree: {wt.name}", fg=name_color, bold=True))
56 # Location
57 click.echo(click.style(f"Location: {wt.path}", fg="white", dim=True))
59 # Branch
60 if wt.branch:
61 click.echo(click.style(f"Branch: {wt.branch}", fg="yellow"))
62 else:
63 click.echo(click.style("Branch: (detached HEAD)", fg="red", dim=True))
65 click.echo()
67 def _render_plan(self, status: StatusData) -> None:
68 """Render plan file section if available.
70 Args:
71 status: Status data
72 """
73 if status.plan is None or not status.plan.exists:
74 return
76 click.echo(click.style("Plan:", fg="bright_magenta", bold=True))
78 if status.plan.first_lines:
79 for line in status.plan.first_lines:
80 click.echo(f" {line}")
82 click.echo(
83 click.style(
84 f" ({status.plan.line_count} lines in .PLAN.md)",
85 fg="white",
86 dim=True,
87 )
88 )
89 click.echo()
91 def _render_stack(self, status: StatusData) -> None:
92 """Render Graphite stack section if available.
94 Args:
95 status: Status data
96 """
97 if status.stack_position is None:
98 return
100 stack = status.stack_position
102 click.echo(click.style("Stack Position:", fg="blue", bold=True))
104 # Show position in stack
105 if stack.is_trunk:
106 click.echo(" This is a trunk branch")
107 else:
108 if stack.parent_branch:
109 parent = click.style(stack.parent_branch, fg="yellow")
110 click.echo(f" Parent: {parent}")
112 if stack.children_branches:
113 children = ", ".join(click.style(c, fg="yellow") for c in stack.children_branches)
114 click.echo(f" Children: {children}")
116 # Show stack visualization
117 if len(stack.stack) > 1:
118 click.echo()
119 click.echo(click.style(" Stack:", fg="white", dim=True))
120 for branch in reversed(stack.stack):
121 is_current = branch == stack.current_branch
123 if is_current:
124 marker = click.style("◉", fg="bright_green")
125 branch_text = click.style(branch, fg="bright_green", bold=True)
126 else:
127 marker = click.style("◯", fg="bright_black")
128 branch_text = branch
130 click.echo(f" {marker} {branch_text}")
132 click.echo()
134 def _render_pr_status(self, status: StatusData) -> None:
135 """Render PR status section if available.
137 Args:
138 status: Status data
139 """
140 if status.pr_status is None:
141 return
143 pr = status.pr_status
145 click.echo(click.style("Pull Request:", fg="blue", bold=True))
147 # PR number and state
148 pr_link = click.style(f"#{pr.number}", fg="cyan")
149 state_color = (
150 "green" if pr.state == "OPEN" else "red" if pr.state == "CLOSED" else "magenta"
151 )
152 state_text = click.style(pr.state, fg=state_color)
153 click.echo(f" {pr_link} {state_text}")
155 # Draft status
156 if pr.is_draft:
157 click.echo(click.style(" Draft PR", fg="yellow"))
159 # Checks status
160 if pr.checks_passing is not None:
161 if pr.checks_passing:
162 click.echo(click.style(" Checks: passing", fg="green"))
163 else:
164 click.echo(click.style(" Checks: failing", fg="red"))
166 # Ready to merge
167 if pr.ready_to_merge:
168 click.echo(click.style(" ✓ Ready to merge", fg="green", bold=True))
170 # URL
171 click.echo(click.style(f" {pr.url}", fg="white", dim=True))
173 click.echo()
175 def _render_git_status(self, status: StatusData) -> None:
176 """Render git status section.
178 Args:
179 status: Status data
180 """
181 if status.git_status is None:
182 return
184 git = status.git_status
186 click.echo(click.style("Git Status:", fg="blue", bold=True))
188 # Clean/dirty status
189 if git.clean:
190 click.echo(click.style(" Working tree clean", fg="green"))
191 else:
192 click.echo(click.style(" Working tree has changes:", fg="yellow"))
194 if git.staged_files:
195 click.echo(click.style(" Staged:", fg="green"))
196 self._render_file_list(git.staged_files, max_files=3)
198 if git.modified_files:
199 click.echo(click.style(" Modified:", fg="yellow"))
200 self._render_file_list(git.modified_files, max_files=3)
202 if git.untracked_files:
203 click.echo(click.style(" Untracked:", fg="red"))
204 self._render_file_list(git.untracked_files, max_files=3)
206 # Ahead/behind
207 if git.ahead > 0 or git.behind > 0:
208 parts = []
209 if git.ahead > 0:
210 parts.append(click.style(f"{git.ahead} ahead", fg="green"))
211 if git.behind > 0:
212 parts.append(click.style(f"{git.behind} behind", fg="red"))
214 click.echo(f" Branch: {', '.join(parts)}")
216 # Recent commits
217 if git.recent_commits:
218 click.echo()
219 click.echo(click.style(" Recent commits:", fg="white", dim=True))
220 for commit in git.recent_commits[:3]:
221 sha = click.style(commit.sha, fg="yellow")
222 message = commit.message[:60]
223 if len(commit.message) > 60:
224 message += "..."
225 click.echo(f" {sha} {message}")
227 click.echo()
229 def _render_related_worktrees(self, status: StatusData) -> None:
230 """Render related worktrees section.
232 Args:
233 status: Status data
234 """
235 if not status.related_worktrees:
236 return
238 click.echo(click.style("Related Worktrees:", fg="blue", bold=True))
240 for wt in status.related_worktrees[:5]:
241 name_color = "green" if wt.is_root else "cyan"
242 name_part = click.style(wt.name, fg=name_color)
244 if wt.branch:
245 branch_part = click.style(f"[{wt.branch}]", fg="yellow", dim=True)
246 click.echo(f" {name_part} {branch_part}")
247 else:
248 click.echo(f" {name_part}")
250 if len(status.related_worktrees) > 5:
251 remaining = len(status.related_worktrees) - 5
252 click.echo(
253 click.style(
254 f" ... and {remaining} more",
255 fg="white",
256 dim=True,
257 )
258 )
260 click.echo()