@@ -3,7 +3,9 @@ package tui
33import (
44 "context"
55
6+ "github.com/charmbracelet/bubbles/key"
67 "github.com/charmbracelet/bubbles/list"
8+ "github.com/charmbracelet/bubbles/spinner"
79 tea "github.com/charmbracelet/bubbletea"
810 "github.com/charmbracelet/lipgloss"
911 "github.com/hay-kot/hive/internal/core/config"
@@ -17,20 +19,23 @@ type UIState int
1719const (
1820 stateNormal UIState = iota
1921 stateConfirming
22+ stateLoading
2023)
2124
2225// Model is the main Bubble Tea model for the TUI.
2326type Model struct {
24- service * hive.Service
25- list list.Model
26- handler * KeybindingHandler
27- state UIState
28- modal Modal
29- pending Action
30- width int
31- height int
32- err error
33- quitting bool
27+ service * hive.Service
28+ list list.Model
29+ handler * KeybindingHandler
30+ state UIState
31+ modal Modal
32+ pending Action
33+ width int
34+ height int
35+ err error
36+ spinner spinner.Model
37+ loadingMessage string
38+ quitting bool
3439}
3540
3641// sessionsLoadedMsg is sent when sessions are loaded.
@@ -55,17 +60,27 @@ func New(service *hive.Service, cfg *config.Config) Model {
5560
5661 handler := NewKeybindingHandler (cfg .Keybindings , service )
5762
63+ // Add custom keybindings to list help
64+ l .AdditionalShortHelpKeys = func () []key.Binding {
65+ return handler .KeyBindings ()
66+ }
67+
68+ s := spinner .New ()
69+ s .Spinner = spinner .Dot
70+ s .Style = spinnerStyle
71+
5872 return Model {
5973 service : service ,
6074 list : l ,
6175 handler : handler ,
6276 state : stateNormal ,
77+ spinner : s ,
6378 }
6479}
6580
6681// Init initializes the model.
6782func (m Model ) Init () tea.Cmd {
68- return m .loadSessions ()
83+ return tea . Batch ( m .loadSessions (), m . spinner . Tick )
6984}
7085
7186// loadSessions returns a command that loads sessions from the service.
@@ -90,24 +105,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90105 case tea.WindowSizeMsg :
91106 m .width = msg .Width
92107 m .height = msg .Height
93- m .list .SetSize (msg .Width , msg .Height - 2 ) // Leave room for help bar
108+ m .list .SetSize (msg .Width , msg .Height )
94109 return m , nil
95110
96111 case sessionsLoadedMsg :
97112 if msg .err != nil {
98113 m .err = msg .err
114+ m .state = stateNormal
99115 return m , nil
100116 }
101117 items := make ([]list.Item , len (msg .sessions ))
102118 for i , s := range msg .sessions {
103119 items [i ] = SessionItem {Session : s }
104120 }
105121 m .list .SetItems (items )
122+ m .state = stateNormal
106123 return m , nil
107124
108125 case actionCompleteMsg :
109126 if msg .err != nil {
110127 m .err = msg .err
128+ m .state = stateNormal
129+ m .pending = Action {}
130+ return m , nil
111131 }
112132 m .state = stateNormal
113133 m .pending = Action {}
@@ -116,6 +136,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116136
117137 case tea.KeyMsg :
118138 return m .handleKey (msg )
139+
140+ case spinner.TickMsg :
141+ var cmd tea.Cmd
142+ m .spinner , cmd = m .spinner .Update (msg )
143+ return m , cmd
119144 }
120145
121146 var cmd tea.Cmd
@@ -164,6 +189,9 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
164189 m .modal = NewModal ("Confirm" , action .Confirm )
165190 return m , nil
166191 }
192+ // Show loading state for action
193+ m .state = stateLoading
194+ m .loadingMessage = "Processing..."
167195 return m , m .executeAction (action )
168196 }
169197
@@ -192,14 +220,22 @@ func (m Model) View() string {
192220 return ""
193221 }
194222
195- if m .err != nil {
196- return errorStyle .Render ("Error: " + m .err .Error ())
197- }
198-
199223 // Build main view
200- listView := m .list .View ()
201- helpBar := helpStyle .Render ("[q] quit " + m .handler .HelpString ())
202- mainView := lipgloss .JoinVertical (lipgloss .Left , listView , helpBar )
224+ mainView := m .list .View ()
225+
226+ // Overlay loading spinner if loading
227+ if m .state == stateLoading {
228+ w , h := m .width , m .height
229+ if w == 0 {
230+ w = 80
231+ }
232+ if h == 0 {
233+ h = 24
234+ }
235+ loadingView := lipgloss .JoinHorizontal (lipgloss .Left , m .spinner .View (), " " + m .loadingMessage )
236+ modal := NewModal ("" , loadingView )
237+ return modal .Overlay (mainView , w , h )
238+ }
203239
204240 // Overlay modal if confirming
205241 if m .state == stateConfirming {
0 commit comments