@@ -29,11 +29,12 @@ func (i beanItem) FilterValue() string { return i.bean.Title + " " + i.bean.ID }
2929
3030// itemDelegate handles rendering of list items
3131type itemDelegate struct {
32- cfg * config.Config
33- hasTags bool
34- width int
35- cols ui.ResponsiveColumns // cached responsive columns
36- idColWidth int // ID column width (accounts for tree prefix)
32+ cfg * config.Config
33+ hasTags bool
34+ width int
35+ cols ui.ResponsiveColumns // cached responsive columns
36+ idColWidth int // ID column width (accounts for tree prefix)
37+ selectedBeans * map [string ]bool // pointer to marked beans for multi-select
3738}
3839
3940func newItemDelegate (cfg * config.Config ) itemDelegate {
@@ -64,6 +65,12 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
6465 }
6566 maxTitleWidth := max (0 , m .Width ()- baseWidth )
6667
68+ // Check if bean is marked for multi-select
69+ var isMarked bool
70+ if d .selectedBeans != nil {
71+ isMarked = (* d .selectedBeans )[item .bean .ID ]
72+ }
73+
6774 str := ui .RenderBeanRow (
6875 item .bean .ID ,
6976 item .bean .Status ,
@@ -78,6 +85,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
7885 MaxTitleWidth : maxTitleWidth ,
7986 ShowCursor : true ,
8087 IsSelected : index == m .Index (),
88+ IsMarked : isMarked ,
8189 Tags : item .bean .Tags ,
8290 ShowTags : d .cols .ShowTags ,
8391 TagsColWidth : d .cols .Tags ,
@@ -107,10 +115,14 @@ type listModel struct {
107115
108116 // Active filters
109117 tagFilter string // if set, only show beans with this tag
118+
119+ // Multi-select state
120+ selectedBeans map [string ]bool // IDs of beans marked for multi-edit
110121}
111122
112123func newListModel (resolver * graph.Resolver , cfg * config.Config ) listModel {
113- delegate := newItemDelegate (cfg )
124+ selectedBeans := make (map [string ]bool )
125+ delegate := itemDelegate {cfg : cfg , selectedBeans : & selectedBeans }
114126
115127 l := list .New ([]list.Item {}, delegate , 0 , 0 )
116128 l .Title = "Beans"
@@ -123,9 +135,10 @@ func newListModel(resolver *graph.Resolver, cfg *config.Config) listModel {
123135 l .Styles .FilterCursor = lipgloss .NewStyle ().Foreground (ui .ColorPrimary )
124136
125137 return listModel {
126- list : l ,
127- resolver : resolver ,
128- config : cfg ,
138+ list : l ,
139+ resolver : resolver ,
140+ config : cfg ,
141+ selectedBeans : selectedBeans ,
129142 }
130143}
131144
@@ -251,52 +264,119 @@ func (m listModel) Update(msg tea.Msg) (listModel, tea.Cmd) {
251264 case tea.KeyMsg :
252265 if m .list .FilterState () != list .Filtering {
253266 switch msg .String () {
267+ case " " :
268+ // Toggle selection for multi-select
269+ if item , ok := m .list .SelectedItem ().(beanItem ); ok {
270+ if m .selectedBeans [item .bean .ID ] {
271+ delete (m .selectedBeans , item .bean .ID )
272+ } else {
273+ m .selectedBeans [item .bean .ID ] = true
274+ }
275+ }
276+ return m , nil
254277 case "enter" :
255278 if item , ok := m .list .SelectedItem ().(beanItem ); ok {
256279 return m , func () tea.Msg {
257280 return selectBeanMsg {bean : item .bean }
258281 }
259282 }
260283 case "p" :
261- // Open parent picker for selected bean
262- if item , ok := m .list .SelectedItem ().(beanItem ); ok {
284+ // Open parent picker for selected bean(s)
285+ if len (m .selectedBeans ) > 0 {
286+ // Multi-select mode
287+ ids := make ([]string , 0 , len (m .selectedBeans ))
288+ types := make ([]string , 0 , len (m .selectedBeans ))
289+ for id := range m .selectedBeans {
290+ ids = append (ids , id )
291+ // Find the bean to get its type
292+ for _ , item := range m .list .Items () {
293+ if bi , ok := item .(beanItem ); ok && bi .bean .ID == id {
294+ types = append (types , bi .bean .Type )
295+ break
296+ }
297+ }
298+ }
263299 return m , func () tea.Msg {
264300 return openParentPickerMsg {
265- beanID : item .bean .ID ,
301+ beanIDs : ids ,
302+ beanTitle : fmt .Sprintf ("%d selected beans" , len (ids )),
303+ beanTypes : types ,
304+ }
305+ }
306+ } else if item , ok := m .list .SelectedItem ().(beanItem ); ok {
307+ return m , func () tea.Msg {
308+ return openParentPickerMsg {
309+ beanIDs : []string {item .bean .ID },
266310 beanTitle : item .bean .Title ,
267- beanType : item .bean .Type ,
311+ beanTypes : [] string { item .bean .Type } ,
268312 currentParent : item .bean .Parent ,
269313 }
270314 }
271315 }
272316 case "s" :
273- // Open status picker for selected bean
274- if item , ok := m .list .SelectedItem ().(beanItem ); ok {
317+ // Open status picker for selected bean(s)
318+ if len (m .selectedBeans ) > 0 {
319+ // Multi-select mode
320+ ids := make ([]string , 0 , len (m .selectedBeans ))
321+ for id := range m .selectedBeans {
322+ ids = append (ids , id )
323+ }
275324 return m , func () tea.Msg {
276325 return openStatusPickerMsg {
277- beanID : item .bean .ID ,
326+ beanIDs : ids ,
327+ beanTitle : fmt .Sprintf ("%d selected beans" , len (ids )),
328+ }
329+ }
330+ } else if item , ok := m .list .SelectedItem ().(beanItem ); ok {
331+ return m , func () tea.Msg {
332+ return openStatusPickerMsg {
333+ beanIDs : []string {item .bean .ID },
278334 beanTitle : item .bean .Title ,
279335 currentStatus : item .bean .Status ,
280336 }
281337 }
282338 }
283339 case "t" :
284- // Open type picker for selected bean
285- if item , ok := m .list .SelectedItem ().(beanItem ); ok {
340+ // Open type picker for selected bean(s)
341+ if len (m .selectedBeans ) > 0 {
342+ // Multi-select mode
343+ ids := make ([]string , 0 , len (m .selectedBeans ))
344+ for id := range m .selectedBeans {
345+ ids = append (ids , id )
346+ }
347+ return m , func () tea.Msg {
348+ return openTypePickerMsg {
349+ beanIDs : ids ,
350+ beanTitle : fmt .Sprintf ("%d selected beans" , len (ids )),
351+ }
352+ }
353+ } else if item , ok := m .list .SelectedItem ().(beanItem ); ok {
286354 return m , func () tea.Msg {
287355 return openTypePickerMsg {
288- beanID : item .bean .ID ,
356+ beanIDs : [] string { item .bean .ID } ,
289357 beanTitle : item .bean .Title ,
290358 currentType : item .bean .Type ,
291359 }
292360 }
293361 }
294362 case "P" :
295- // Open priority picker for selected bean
296- if item , ok := m .list .SelectedItem ().(beanItem ); ok {
363+ // Open priority picker for selected bean(s)
364+ if len (m .selectedBeans ) > 0 {
365+ // Multi-select mode
366+ ids := make ([]string , 0 , len (m .selectedBeans ))
367+ for id := range m .selectedBeans {
368+ ids = append (ids , id )
369+ }
297370 return m , func () tea.Msg {
298371 return openPriorityPickerMsg {
299- beanID : item .bean .ID ,
372+ beanIDs : ids ,
373+ beanTitle : fmt .Sprintf ("%d selected beans" , len (ids )),
374+ }
375+ }
376+ } else if item , ok := m .list .SelectedItem ().(beanItem ); ok {
377+ return m , func () tea.Msg {
378+ return openPriorityPickerMsg {
379+ beanIDs : []string {item .bean .ID },
300380 beanTitle : item .bean .Title ,
301381 currentPriority : item .bean .Priority ,
302382 }
@@ -329,7 +409,12 @@ func (m listModel) Update(msg tea.Msg) (listModel, tea.Cmd) {
329409 }
330410 }
331411 case "esc" , "backspace" :
332- // If we have an active filter, clear it instead of quitting
412+ // First clear selection if any beans are selected
413+ if len (m .selectedBeans ) > 0 {
414+ clear (m .selectedBeans )
415+ return m , nil
416+ }
417+ // Then clear active filter if any
333418 if m .hasActiveFilter () {
334419 return m , func () tea.Msg {
335420 return clearFilterMsg {}
@@ -347,11 +432,12 @@ func (m listModel) Update(msg tea.Msg) (listModel, tea.Cmd) {
347432// updateDelegate updates the list delegate with current responsive columns
348433func (m * listModel ) updateDelegate () {
349434 delegate := itemDelegate {
350- cfg : m .config ,
351- hasTags : m .hasTags ,
352- width : m .width ,
353- cols : m .cols ,
354- idColWidth : m .idColWidth ,
435+ cfg : m .config ,
436+ hasTags : m .hasTags ,
437+ width : m .width ,
438+ cols : m .cols ,
439+ idColWidth : m .idColWidth ,
440+ selectedBeans : & m .selectedBeans ,
355441 }
356442 m .list .SetDelegate (delegate )
357443}
@@ -381,10 +467,28 @@ func (m listModel) View() string {
381467
382468 content := border .Render (m .list .View ())
383469
384- // Footer - show different help based on filter state
470+ // Footer - show different help based on filter/selection state
385471 var help string
386- if m .hasActiveFilter () {
387- help = helpKeyStyle .Render ("enter" ) + " " + helpStyle .Render ("view" ) + " " +
472+
473+ // Show selection count if any beans are selected
474+ var selectionPrefix string
475+ if len (m .selectedBeans ) > 0 {
476+ selectionStyle := lipgloss .NewStyle ().Foreground (ui .ColorWarning ).Bold (true )
477+ selectionPrefix = selectionStyle .Render (fmt .Sprintf ("(%d selected) " , len (m .selectedBeans )))
478+ }
479+
480+ if len (m .selectedBeans ) > 0 {
481+ // When beans are selected, show esc to clear selection
482+ help = helpKeyStyle .Render ("space" ) + " " + helpStyle .Render ("toggle" ) + " " +
483+ helpKeyStyle .Render ("s" ) + " " + helpStyle .Render ("status" ) + " " +
484+ helpKeyStyle .Render ("t" ) + " " + helpStyle .Render ("type" ) + " " +
485+ helpKeyStyle .Render ("P" ) + " " + helpStyle .Render ("priority" ) + " " +
486+ helpKeyStyle .Render ("esc" ) + " " + helpStyle .Render ("clear selection" ) + " " +
487+ helpKeyStyle .Render ("?" ) + " " + helpStyle .Render ("help" ) + " " +
488+ helpKeyStyle .Render ("q" ) + " " + helpStyle .Render ("quit" )
489+ } else if m .hasActiveFilter () {
490+ help = helpKeyStyle .Render ("space" ) + " " + helpStyle .Render ("select" ) + " " +
491+ helpKeyStyle .Render ("enter" ) + " " + helpStyle .Render ("view" ) + " " +
388492 helpKeyStyle .Render ("c" ) + " " + helpStyle .Render ("create" ) + " " +
389493 helpKeyStyle .Render ("e" ) + " " + helpStyle .Render ("edit" ) + " " +
390494 helpKeyStyle .Render ("s" ) + " " + helpStyle .Render ("status" ) + " " +
@@ -396,7 +500,8 @@ func (m listModel) View() string {
396500 helpKeyStyle .Render ("?" ) + " " + helpStyle .Render ("help" ) + " " +
397501 helpKeyStyle .Render ("q" ) + " " + helpStyle .Render ("quit" )
398502 } else {
399- help = helpKeyStyle .Render ("enter" ) + " " + helpStyle .Render ("view" ) + " " +
503+ help = helpKeyStyle .Render ("space" ) + " " + helpStyle .Render ("select" ) + " " +
504+ helpKeyStyle .Render ("enter" ) + " " + helpStyle .Render ("view" ) + " " +
400505 helpKeyStyle .Render ("c" ) + " " + helpStyle .Render ("create" ) + " " +
401506 helpKeyStyle .Render ("e" ) + " " + helpStyle .Render ("edit" ) + " " +
402507 helpKeyStyle .Render ("s" ) + " " + helpStyle .Render ("status" ) + " " +
@@ -409,6 +514,6 @@ func (m listModel) View() string {
409514 helpKeyStyle .Render ("q" ) + " " + helpStyle .Render ("quit" )
410515 }
411516
412- return content + "\n " + help
517+ return content + "\n " + selectionPrefix + help
413518}
414519
0 commit comments