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
"""Filterable local branch picker."""
from __future__ import annotations
from pyratatui import (
Constraint,
Direction,
Frame,
Layout,
List,
ListItem,
ListState,
Paragraph,
Rect,
TextPrompt,
TextState,
)
from pygittools.tui.branches import (
checkout_branch,
create_branch_from_current,
current_branch_name,
list_local_branches,
)
from pygittools.tui.git_colors import current_theme, focus_style
from pygittools.tui.keys import KeyLike, is_down, is_enter, is_esc, is_quit, is_up
from pygittools.tui.types import PageAction, PageContext, PageResult
class BranchPickerPage:
title = "branches"
def __init__(self) -> None:
self._ctx: PageContext | None = None
self._filter_state = TextState()
self._filter_state.focus()
self._branches: list[str] = []
self._filtered: list[str] = []
self._list_state = ListState()
self._focus_filter = True
self._error: str | None = None
def on_enter(self, ctx: PageContext) -> None:
self._ctx = ctx
self._filter_state.set_value("")
self._filter_state.focus()
self._focus_filter = True
self._error = None
self._list_state.select(0)
self._reload_branches()
def status_text(self) -> str:
if self._error:
return self._error
if not self._filtered:
if self._filter_state.value().strip():
return "Enter create branch — type name — Esc back"
return "No matching branches — type to filter, Esc back"
return "Enter checkout — type to filter — j/k move — Esc back"
def draw(self, frame: Frame, area: Rect) -> None:
if area.height <= 0:
return
theme = current_theme()
chunks = (
Layout()
.direction(Direction.Vertical)
.constraints([Constraint.length(1), Constraint.length(1), Constraint.fill(1)])
.split(area)
)
frame.render_widget(Paragraph.from_string("Switch branch").style(theme.header), chunks[0])
if self._focus_filter:
self._filter_state.focus()
else:
self._filter_state.blur()
frame.render_text_prompt(TextPrompt("Filter: "), chunks[1], self._filter_state)
current = current_branch_name(self._ctx.repo) if self._ctx is not None else ""
items: list[ListItem] = []
selected = self._list_state.selected or 0
for index, branch in enumerate(self._filtered):
marker = "* " if branch == current else " "
focused = not self._focus_filter and index == selected
style = focus_style(theme.normal) if focused else theme.normal
items.append(ListItem(f"{marker}{branch}", style))
if not items:
items.append(ListItem("(no matches)", theme.log_date))
frame.render_stateful_list(
List(items).highlight_style(theme.focus).highlight_symbol(""),
chunks[2],
self._list_state,
)
def handle_key(self, key: KeyLike) -> PageResult:
if is_esc(key) or (not self._focus_filter and is_quit(key)):
return PageResult(action=PageAction.POP)
if self._error is not None:
self._error = None
ctx = self._ctx
if ctx is None:
return PageResult()
if self._focus_filter:
if is_down(key) and self._filtered:
self._focus_filter = False
self._list_state.select(0)
return PageResult()
if is_enter(key):
if not self._filtered:
return self._try_create_branch(ctx)
self._focus_filter = False
self._list_state.select(0)
return PageResult()
self._filter_state.focus()
self._filter_state.handle_key(key)
self._apply_filter()
return PageResult()
if is_up(key):
selected = self._list_state.selected or 0
if selected <= 0:
self._focus_filter = True
return PageResult()
self._list_state.select(selected - 1)
return PageResult()
if is_down(key):
selected = self._list_state.selected or 0
self._list_state.select(min(max(0, len(self._filtered) - 1), selected + 1))
return PageResult()
if is_enter(key) and self._filtered:
branch = self._filtered[self._list_state.selected or 0]
ok, error = checkout_branch(ctx.repo, branch)
if ok:
return PageResult(action=PageAction.POP)
self._error = error
return PageResult()
# Typing jumps back to filter.
self._focus_filter = True
self._filter_state.focus()
self._filter_state.handle_key(key)
self._apply_filter()
return PageResult()
def _try_create_branch(self, ctx: PageContext) -> PageResult:
ok, error = create_branch_from_current(ctx.repo, self._filter_state.value())
if ok:
return PageResult(action=PageAction.POP)
self._error = error
return PageResult()
def _reload_branches(self) -> None:
if self._ctx is None:
self._branches = []
self._filtered = []
return
self._branches = list_local_branches(self._ctx.repo)
self._apply_filter()
def _apply_filter(self) -> None:
needle = self._filter_state.value().casefold()
if needle:
self._filtered = [branch for branch in self._branches if needle in branch.casefold()]
else:
self._filtered = list(self._branches)
selected = self._list_state.selected or 0
self._list_state.select(min(selected, max(0, len(self._filtered) - 1)))
def branch_picker_page() -> BranchPickerPage:
return BranchPickerPage()