|
8 | 8 | "os" |
9 | 9 | "slices" |
10 | 10 | "strings" |
| 11 | + "time" |
11 | 12 |
|
12 | 13 | "charm.land/bubbles/v2/textinput" |
13 | 14 | tea "charm.land/bubbletea/v2" |
@@ -71,6 +72,9 @@ type Model interface { |
71 | 72 | SetPreferredWidth(width int) |
72 | 73 | // ClampWidth ensures width is within valid bounds for the given window width |
73 | 74 | ClampWidth(width, windowInnerWidth int) int |
| 75 | + // HandleTitleClick handles a click on the title area and returns true if |
| 76 | + // edit mode should start (on double-click) |
| 77 | + HandleTitleClick() bool |
74 | 78 | // BeginTitleEdit starts inline editing of the session title |
75 | 79 | BeginTitleEdit() |
76 | 80 | // IsEditingTitle returns true if the title is being edited |
@@ -130,6 +134,7 @@ type model struct { |
130 | 134 | preferredWidth int // user's preferred width (persisted across collapse/expand) |
131 | 135 | editingTitle bool // true when inline title editing is active |
132 | 136 | titleInput textinput.Model |
| 137 | + lastTitleClickTime time.Time // for double-click detection on title |
133 | 138 | } |
134 | 139 |
|
135 | 140 | // Option is a functional option for configuring the sidebar. |
@@ -284,63 +289,77 @@ type ClickResult int |
284 | 289 | const ( |
285 | 290 | ClickNone ClickResult = iota |
286 | 291 | ClickStar |
287 | | - ClickPencil |
| 292 | + ClickTitle // Click on the title area (use double-click to edit) |
288 | 293 | ) |
289 | 294 |
|
290 | | -// pencilIconWidth is the click target width for the pencil edit icon (includes padding) |
291 | | -const pencilIconWidth = 3 // " ✎" = space + pencil character |
292 | | - |
293 | 295 | // HandleClick checks if click is on the star or title and returns true if it was |
294 | 296 | // x and y are coordinates relative to the sidebar's top-left corner |
295 | 297 | // This does NOT toggle the state - caller should handle that |
296 | 298 | func (m *model) HandleClick(x, y int) bool { |
297 | 299 | return m.HandleClickType(x, y) != ClickNone |
298 | 300 | } |
299 | 301 |
|
300 | | -// HandleClickType returns what was clicked (star, pencil icon, or nothing) |
| 302 | +// HandleClickType returns what was clicked (star, title, or nothing) |
301 | 303 | func (m *model) HandleClickType(x, y int) ClickResult { |
302 | 304 | // Account for left padding |
303 | 305 | adjustedX := x - m.layoutCfg.PaddingLeft |
| 306 | + if adjustedX < 0 { |
| 307 | + return ClickNone |
| 308 | + } |
304 | 309 |
|
305 | 310 | if m.mode == ModeCollapsed { |
306 | | - // In collapsed mode, star is at the beginning of first line (y=0) |
307 | | - if y == 0 { |
308 | | - if m.sessionHasContent && adjustedX >= 0 && adjustedX <= starClickWidth { |
| 311 | + // In collapsed mode, title starts at line 0 |
| 312 | + titleLines := m.titleLineCount() |
| 313 | + |
| 314 | + // Check if click is within the title area (line 0 to titleLines-1) |
| 315 | + if y >= 0 && y < titleLines { |
| 316 | + // Check if click is on the star (first line only, first few chars) |
| 317 | + if y == 0 && m.sessionHasContent && adjustedX <= starClickWidth { |
309 | 318 | return ClickStar |
310 | 319 | } |
311 | | - // Check if click is on pencil icon (at the end of the title line) |
312 | | - // Pencil is only shown when title has been generated |
313 | | - if m.titleGenerated { |
314 | | - starWidth := lipgloss.Width(m.starIndicator()) |
315 | | - titleWidth := lipgloss.Width(m.sessionTitle) |
316 | | - pencilStart := starWidth + titleWidth |
317 | | - if adjustedX >= pencilStart && adjustedX < pencilStart+pencilIconWidth { |
318 | | - return ClickPencil |
319 | | - } |
| 320 | + // Click is on title area (for double-click to edit) |
| 321 | + if m.titleGenerated && !m.editingTitle { |
| 322 | + return ClickTitle |
320 | 323 | } |
321 | 324 | } |
322 | 325 | return ClickNone |
323 | 326 | } |
324 | 327 |
|
325 | | - // In vertical mode, the title line is at verticalStarY |
326 | | - if y == verticalStarY { |
327 | | - if m.sessionHasContent && adjustedX >= 0 && adjustedX <= starClickWidth { |
| 328 | + // In vertical mode, the title starts at verticalStarY |
| 329 | + scrollOffset := m.scrollbar.GetScrollOffset() |
| 330 | + contentY := y + scrollOffset // Convert viewport Y to content Y |
| 331 | + titleLines := m.titleLineCount() |
| 332 | + |
| 333 | + // Check if click is within the title area |
| 334 | + if contentY >= verticalStarY && contentY < verticalStarY+titleLines { |
| 335 | + // Check if click is on the star (first line only, first few chars) |
| 336 | + if contentY == verticalStarY && m.sessionHasContent && adjustedX <= starClickWidth { |
328 | 337 | return ClickStar |
329 | 338 | } |
330 | | - // Check if click is on pencil icon (at the end of the title line) |
331 | | - // Pencil is only shown when title has been generated |
332 | | - if m.titleGenerated { |
333 | | - starWidth := lipgloss.Width(m.starIndicator()) |
334 | | - titleWidth := lipgloss.Width(m.sessionTitle) |
335 | | - pencilStart := starWidth + titleWidth |
336 | | - if adjustedX >= pencilStart && adjustedX < pencilStart+pencilIconWidth { |
337 | | - return ClickPencil |
338 | | - } |
| 339 | + // Click is on title area (for double-click to edit) |
| 340 | + if m.titleGenerated && !m.editingTitle { |
| 341 | + return ClickTitle |
339 | 342 | } |
340 | 343 | } |
341 | 344 | return ClickNone |
342 | 345 | } |
343 | 346 |
|
| 347 | +// titleLineCount returns the number of lines the title occupies when rendered. |
| 348 | +func (m *model) titleLineCount() int { |
| 349 | + if !m.titleGenerated || m.sessionTitle == "" { |
| 350 | + return 1 |
| 351 | + } |
| 352 | + contentWidth := m.contentWidth(false) |
| 353 | + if contentWidth <= 0 { |
| 354 | + return 1 |
| 355 | + } |
| 356 | + // Calculate width: star + title |
| 357 | + starWidth := lipgloss.Width(m.starIndicator()) |
| 358 | + titleWidth := lipgloss.Width(m.sessionTitle) |
| 359 | + totalWidth := starWidth + titleWidth |
| 360 | + return max(1, (totalWidth+contentWidth-1)/contentWidth) |
| 361 | +} |
| 362 | + |
344 | 363 | // LoadFromSession loads sidebar state from a restored session |
345 | 364 | func (m *model) LoadFromSession(sess *session.Session) { |
346 | 365 | if sess == nil { |
@@ -656,12 +675,7 @@ func (m *model) computeCollapsedLayout(contentWidth int) collapsedLayout { |
656 | 675 | titleWithStar = star + m.titleInput.View() |
657 | 676 | case m.titleRegenerating: |
658 | 677 | titleWithStar = star + m.spinner.View() + styles.MutedStyle.Render(" Generating title…") |
659 | | - case m.titleGenerated: |
660 | | - // Title has been generated - show with pencil icon |
661 | | - pencilIcon := styles.MutedStyle.Render(" ✎") |
662 | | - titleWithStar = star + m.sessionTitle + pencilIcon |
663 | 678 | default: |
664 | | - // Title not yet generated - show without pencil icon |
665 | 679 | titleWithStar = star + m.sessionTitle |
666 | 680 | } |
667 | 681 | h := collapsedLayout{ |
@@ -993,12 +1007,7 @@ func (m *model) sessionInfo(contentWidth int) string { |
993 | 1007 | case m.titleRegenerating: |
994 | 1008 | // Show spinner while regenerating title |
995 | 1009 | titleLine = star + m.spinner.View() + styles.MutedStyle.Render(" Generating title…") |
996 | | - case m.titleGenerated: |
997 | | - // Title has been generated - show with pencil icon for editing |
998 | | - pencilIcon := styles.MutedStyle.Render(" ✎") |
999 | | - titleLine = star + m.sessionTitle + pencilIcon |
1000 | 1010 | default: |
1001 | | - // Title not yet generated - show title without pencil icon |
1002 | 1011 | titleLine = star + m.sessionTitle |
1003 | 1012 | } |
1004 | 1013 |
|
@@ -1260,10 +1269,32 @@ func (m *model) ClampWidth(width, windowInnerWidth int) int { |
1260 | 1269 | return max(MinWidth, min(width, maxWidth)) |
1261 | 1270 | } |
1262 | 1271 |
|
| 1272 | +// HandleTitleClick handles a click on the title area and returns true if |
| 1273 | +// edit mode should start (on double-click). |
| 1274 | +func (m *model) HandleTitleClick() bool { |
| 1275 | + now := time.Now() |
| 1276 | + if now.Sub(m.lastTitleClickTime) < styles.DoubleClickThreshold { |
| 1277 | + m.lastTitleClickTime = time.Time{} // Reset to prevent triple-click |
| 1278 | + return true |
| 1279 | + } |
| 1280 | + m.lastTitleClickTime = now |
| 1281 | + return false |
| 1282 | +} |
| 1283 | + |
1263 | 1284 | // BeginTitleEdit starts inline editing of the session title |
1264 | 1285 | func (m *model) BeginTitleEdit() { |
1265 | 1286 | m.editingTitle = true |
1266 | 1287 | m.titleInput.SetValue(m.sessionTitle) |
| 1288 | + |
| 1289 | + // Calculate and set the input width based on current sidebar width |
| 1290 | + contentWidth := m.contentWidth(false) |
| 1291 | + starWidth := lipgloss.Width(m.starIndicator()) |
| 1292 | + inputWidth := contentWidth - starWidth - 1 |
| 1293 | + if inputWidth < 10 { |
| 1294 | + inputWidth = 10 // Minimum usable width |
| 1295 | + } |
| 1296 | + m.titleInput.SetWidth(inputWidth) |
| 1297 | + |
1267 | 1298 | m.titleInput.Focus() |
1268 | 1299 | m.titleInput.CursorEnd() |
1269 | 1300 | } |
|
0 commit comments