@@ -2,11 +2,13 @@ package dialog
22
33import (
44 "context"
5+ "strings"
56
67 "charm.land/bubbles/v2/help"
78 "charm.land/bubbles/v2/key"
89 "charm.land/bubbles/v2/textinput"
910 tea "charm.land/bubbletea/v2"
11+ "charm.land/lipgloss/v2"
1012 "github.com/charmbracelet/crush/internal/session"
1113 "github.com/charmbracelet/crush/internal/ui/common"
1214 "github.com/charmbracelet/crush/internal/ui/list"
@@ -43,6 +45,9 @@ type Session struct {
4345 Previous key.Binding
4446 UpDown key.Binding
4547 Delete key.Binding
48+ Rename key.Binding
49+ ConfirmRename key.Binding
50+ CancelRename key.Binding
4651 ConfirmDelete key.Binding
4752 CancelDelete key.Binding
4853 Close key.Binding
@@ -103,6 +108,18 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
103108 key.WithKeys("ctrl+x"),
104109 key.WithHelp("ctrl+x", "delete"),
105110 )
111+ s.keyMap.Rename = key.NewBinding(
112+ key.WithKeys("ctrl+r"),
113+ key.WithHelp("ctrl+r", "rename"),
114+ )
115+ s.keyMap.ConfirmRename = key.NewBinding(
116+ key.WithKeys("enter"),
117+ key.WithHelp("enter", "confirm"),
118+ )
119+ s.keyMap.CancelRename = key.NewBinding(
120+ key.WithKeys("esc"),
121+ key.WithHelp("esc", "cancel"),
122+ )
106123 s.keyMap.ConfirmDelete = key.NewBinding(
107124 key.WithKeys("y"),
108125 key.WithHelp("y", "delete"),
@@ -129,15 +146,40 @@ func (s *Session) HandleMsg(msg tea.Msg) Action {
129146 case sessionsModeDeleting:
130147 switch {
131148 case key.Matches(msg, s.keyMap.ConfirmDelete):
132− return s.confirmDeleteSession()
149+ action := s.confirmDeleteSession()
150+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
151+ s.list.SelectFirst()
152+ s.list.ScrollToSelected()
153+ return action
133154 case key.Matches(msg, s.keyMap.CancelDelete):
134155 s.sessionsMode = sessionsModeNormal
135156 s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
136157 }
158+ case sessionsModeUpdating:
159+ switch {
160+ case key.Matches(msg, s.keyMap.ConfirmRename):
161+ action := s.confirmRenameSession()
162+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
163+ return action
164+ case key.Matches(msg, s.keyMap.CancelRename):
165+ s.sessionsMode = sessionsModeNormal
166+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
167+ default:
168+ item := s.list.SelectedItem()
169+ if item == nil {
170+ return nil
171+ }
172+ if sessionItem, ok := item.(*SessionItem); ok {
173+ return sessionItem.HandleInput(msg)
174+ }
175+ }
137176 default:
138177 switch {
139178 case key.Matches(msg, s.keyMap.Close):
140179 return ActionClose{}
180+ case key.Matches(msg, s.keyMap.Rename):
181+ s.sessionsMode = sessionsModeUpdating
182+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeUpdating, s.sessions...)...)
141183 case key.Matches(msg, s.keyMap.Delete):
142184 if s.isCurrentSessionBusy() {
143185 return ActionCmd{uiutil.ReportWarn("Agent is busy, please wait...")}
@@ -218,6 +260,51 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
218260 rc.TitleGradientToColor = t.Dialog.Sessions.DeletingTitleGradientToColor
219261 rc.ViewStyle = t.Dialog.Sessions.DeletingView
220262 rc.AddPart(t.Dialog.Sessions.DeletingMessage.Render("Delete this session?"))
263+ case sessionsModeUpdating:
264+ rc.TitleStyle = t.Dialog.Sessions.UpdatingTitle
265+ rc.TitleGradientFromColor = t.Dialog.Sessions.UpdatingTitleGradientFromColor
266+ rc.TitleGradientToColor = t.Dialog.Sessions.UpdatingTitleGradientToColor
267+ rc.ViewStyle = t.Dialog.Sessions.UpdatingView
268+ message := t.Dialog.Sessions.UpdatingMessage.Render("Rename this session?")
269+ rc.AddPart(message)
270+ item := s.selectedSessionItem()
271+ if item == nil {
272+ return nil
273+ }
274+ cur = item.Cursor()
275+ if cur == nil {
276+ break
277+ }
278+
279+ start, end := s.list.VisibleItemIndices()
280+ selectedIndex := s.list.Selected()
281+
282+ titleStyle := t.Dialog.Sessions.UpdatingTitle
283+ dialogStyle := t.Dialog.Sessions.UpdatingView
284+ inputStyle := t.Dialog.InputPrompt
285+
286+ // Adjust cursor position to account for dialog layout + message
287+ cur.X += inputStyle.GetBorderLeftSize() +
288+ inputStyle.GetMarginLeft() +
289+ inputStyle.GetPaddingLeft() +
290+ dialogStyle.GetBorderLeftSize() +
291+ dialogStyle.GetPaddingLeft() +
292+ dialogStyle.GetMarginLeft()
293+ cur.Y += titleStyle.GetVerticalFrameSize() +
294+ inputStyle.GetBorderTopSize() +
295+ inputStyle.GetMarginTop() +
296+ inputStyle.GetPaddingTop() +
297+ inputStyle.GetBorderBottomSize() +
298+ inputStyle.GetMarginBottom() +
299+ inputStyle.GetPaddingBottom() +
300+ dialogStyle.GetPaddingTop() +
301+ dialogStyle.GetBorderTopSize() +
302+ lipgloss.Height(message) - 1
303+
304+ // move the cursor by one down until we see the selectedIndex
305+ for ; start <= end && start != selectedIndex && selectedIndex > -1; start++ {
306+ cur.Y += 1
307+ }
221308 default:
222309 inputView := t.Dialog.InputPrompt.Render(s.input.View())
223310 cur = s.Cursor()
@@ -260,9 +347,6 @@ func (s *Session) removeSession(id string) {
260347 newSessions = append(newSessions, sess)
261348 }
262349 s.sessions = newSessions
263− s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
264− s.list.SelectFirst()
265− s.list.ScrollToSelected()
266350}
267351
268352func (s *Session) deleteSessionCmd(id string) tea.Cmd {
@@ -275,6 +359,42 @@ func (s *Session) deleteSessionCmd(id string) tea.Cmd {
275359 }
276360}
277361
362+func (s *Session) confirmRenameSession() Action {
363+ sessionItem := s.selectedSessionItem()
364+ s.sessionsMode = sessionsModeNormal
365+ if sessionItem == nil {
366+ return nil
367+ }
368+
369+ newTitle := strings.TrimSpace(sessionItem.InputValue())
370+ if newTitle == "" {
371+ return nil
372+ }
373+ session := sessionItem.Session
374+ session.Title = newTitle
375+ s.updateSession(session)
376+ return ActionCmd{s.updateSessionCmd(session)}
377+}
378+
379+func (s *Session) updateSession(session session.Session) {
380+ for existingID, sess := range s.sessions {
381+ if sess.ID == session.ID {
382+ s.sessions[existingID] = session
383+ break
384+ }
385+ }
386+}
387+
388+func (s *Session) updateSessionCmd(session session.Session) tea.Cmd {
389+ return func() tea.Msg {
390+ _, err := s.com.App.Sessions.Save(context.TODO(), session)
391+ if err != nil {
392+ return uiutil.NewErrorMsg(err)
393+ }
394+ return nil
395+ }
396+}
397+
278398func (s *Session) isCurrentSessionBusy() bool {
279399 sessionItem := s.selectedSessionItem()
280400 if sessionItem == nil {
@@ -296,11 +416,17 @@ func (s *Session) ShortHelp() []key.Binding {
296416 s.keyMap.ConfirmDelete,
297417 s.keyMap.CancelDelete,
298418 }
419+ case sessionsModeUpdating:
420+ return []key.Binding{
421+ s.keyMap.ConfirmRename,
422+ s.keyMap.CancelRename,
423+ }
299424 default:
300425 return []key.Binding{
301426 s.keyMap.UpDown,
302− s.keyMap.Select,
427+ s.keyMap.Rename,
303428 s.keyMap.Delete,
429+ s.keyMap.Select,
304430 s.keyMap.Close,
305431 }
306432 }
@@ -310,10 +436,10 @@ func (s *Session) ShortHelp() []key.Binding {
310436func (s *Session) FullHelp() [][]key.Binding {
311437 m := [][]key.Binding{}
312438 slice := []key.Binding{
313− s.keyMap.Select,
314− s.keyMap.Next,
315− s.keyMap.Previous,
439+ s.keyMap.UpDown,
440+ s.keyMap.Rename,
316441 s.keyMap.Delete,
442+ s.keyMap.Select,
317443 s.keyMap.Close,
318444 }
319445
@@ -323,6 +449,11 @@ func (s *Session) FullHelp() [][]key.Binding {
323449 s.keyMap.ConfirmDelete,
324450 s.keyMap.CancelDelete,
325451 }
452+ case sessionsModeUpdating:
453+ slice = []key.Binding{
454+ s.keyMap.ConfirmRename,
455+ s.keyMap.CancelRename,
456+ }
326457 }
327458 for i := 0; i < len(slice); i += 4 {
328459 end := min(i+4, len(slice))