Skip to content

Commit 8a0daf4

Browse files
committed
fix(tui): fix blocking picker extra row and height issues
- Pass pending state pointer to delegate for live rendering - Remove SetItems call that was corrupting list state - Fix list height calculation to account for description line (-9 not -7) - Simplify toggle logic by reading state directly in delegate
1 parent cea0a19 commit 8a0daf4

File tree

1 file changed

+25
-42
lines changed

1 file changed

+25
-42
lines changed

internal/tui/blockingpicker.go

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ type openBlockingPickerMsg struct {
3535

3636
// blockingItem wraps a bean to implement list.Item for the blocking picker
3737
type blockingItem struct {
38-
bean *bean.Bean
39-
cfg *config.Config
40-
isBlocking bool // true if current bean is blocking this one (pending state)
38+
bean *bean.Bean
39+
cfg *config.Config
4140
}
4241

4342
func (i blockingItem) Title() string { return i.bean.Title }
@@ -46,7 +45,8 @@ func (i blockingItem) FilterValue() string { return i.bean.Title + " " + i.bean.
4645

4746
// blockingItemDelegate handles rendering of blocking picker items
4847
type blockingItemDelegate struct {
49-
cfg *config.Config
48+
cfg *config.Config
49+
pendingBlocking *map[string]bool // pointer to pending state for live updates
5050
}
5151

5252
func (d blockingItemDelegate) Height() int { return 1 }
@@ -66,9 +66,10 @@ func (d blockingItemDelegate) Render(w io.Writer, m list.Model, index int, listI
6666
cursor = " "
6767
}
6868

69-
// Show blocking indicator
69+
// Show blocking indicator - read from pending state for live updates
70+
isBlocking := (*d.pendingBlocking)[item.bean.ID]
7071
var blockingIndicator string
71-
if item.isBlocking {
72+
if isBlocking {
7273
blockingIndicator = lipgloss.NewStyle().Foreground(ui.ColorDanger).Bold(true).Render("● ") // Red dot for blocking
7374
} else {
7475
blockingIndicator = lipgloss.NewStyle().Foreground(ui.ColorMuted).Render("○ ") // Empty circle for not blocking
@@ -90,14 +91,14 @@ func (d blockingItemDelegate) Render(w io.Writer, m list.Model, index int, listI
9091

9192
// blockingPickerModel is the model for the blocking picker view
9293
type blockingPickerModel struct {
93-
list list.Model
94-
beanID string // the bean we're setting blocking for
95-
beanTitle string // the bean's title
96-
originalBlocking map[string]bool // original state (for computing diff)
97-
pendingBlocking map[string]bool // pending state (toggled by space)
98-
cfg *config.Config
99-
width int
100-
height int
94+
list list.Model
95+
beanID string // the bean we're setting blocking for
96+
beanTitle string // the bean's title
97+
originalBlocking map[string]bool // original state (for computing diff)
98+
pendingBlocking map[string]bool // pending state (toggled by space)
99+
cfg *config.Config
100+
width int
101+
height int
101102
}
102103

103104
func newBlockingPickerModel(beanID, beanTitle string, currentBlocking []string, resolver *graph.Resolver, cfg *config.Config, width, height int) blockingPickerModel {
@@ -134,23 +135,24 @@ func newBlockingPickerModel(beanID, beanTitle string, currentBlocking []string,
134135
return strings.ToLower(eligibleBeans[i].Title) < strings.ToLower(eligibleBeans[j].Title)
135136
})
136137

137-
delegate := blockingItemDelegate{cfg: cfg}
138-
139138
// Build items list
140139
items := make([]list.Item, 0, len(eligibleBeans))
141140
for _, b := range eligibleBeans {
142141
items = append(items, blockingItem{
143-
bean: b,
144-
cfg: cfg,
145-
isBlocking: pendingBlocking[b.ID],
142+
bean: b,
143+
cfg: cfg,
146144
})
147145
}
148146

149147
// Calculate modal dimensions (60% width, 60% height, with min/max constraints)
150148
modalWidth := max(40, min(80, width*60/100))
151149
modalHeight := max(10, min(20, height*60/100))
152150
listWidth := modalWidth - 6
153-
listHeight := modalHeight - 7
151+
// Account for: header(1) + subtitle(1) + blank(1) + blank(1) + description(1) + blank(1) + help(1) + border(2) = 9
152+
listHeight := modalHeight - 9
153+
154+
// Create delegate with pointer to pending state (so it can read live updates)
155+
delegate := blockingItemDelegate{cfg: cfg, pendingBlocking: &pendingBlocking}
154156

155157
l := list.New(items, delegate, listWidth, listHeight)
156158
l.Title = "Manage Blocking"
@@ -189,41 +191,22 @@ func (m blockingPickerModel) Update(msg tea.Msg) (blockingPickerModel, tea.Cmd)
189191
modalWidth := max(40, min(80, msg.Width*60/100))
190192
modalHeight := max(10, min(20, msg.Height*60/100))
191193
listWidth := modalWidth - 6
192-
listHeight := modalHeight - 7
194+
listHeight := modalHeight - 9 // Account for description line
193195
m.list.SetSize(listWidth, listHeight)
194196

195197
case tea.KeyMsg:
196198
if m.list.FilterState() != list.Filtering {
197199
switch msg.String() {
198200
case " ":
199-
// Toggle the selected item locally
201+
// Toggle the selected item's pending state
202+
// The delegate reads from pendingBlocking directly, so no need to update items
200203
if item, ok := m.list.SelectedItem().(blockingItem); ok {
201204
targetID := item.bean.ID
202-
currentIndex := m.list.Index()
203-
204-
// Toggle pending state
205205
if m.pendingBlocking[targetID] {
206206
delete(m.pendingBlocking, targetID)
207207
} else {
208208
m.pendingBlocking[targetID] = true
209209
}
210-
211-
// Update the list items to reflect new state
212-
oldItems := m.list.Items()
213-
newItems := make([]list.Item, len(oldItems))
214-
for i, listItem := range oldItems {
215-
if bi, ok := listItem.(blockingItem); ok {
216-
newItems[i] = blockingItem{
217-
bean: bi.bean,
218-
cfg: bi.cfg,
219-
isBlocking: m.pendingBlocking[bi.bean.ID],
220-
}
221-
}
222-
}
223-
m.list.SetItems(newItems)
224-
225-
// Restore selection position
226-
m.list.Select(currentIndex)
227210
}
228211
return m, nil
229212

0 commit comments

Comments
 (0)