@@ -13,6 +13,7 @@ import (
1313)
1414
1515const (
16+ closeButton = " [x]"
1617 defaultDuration = 3 * time .Second
1718 notificationPadding = 2
1819 maxNotificationWidth = 80 // Maximum width to prevent covering too much screen
@@ -30,6 +31,25 @@ const (
3031 TypeError
3132)
3233
34+ // persistent returns true for notification types that stay until manually dismissed.
35+ func (t Type ) persistent () bool {
36+ return t == TypeWarning || t == TypeError
37+ }
38+
39+ // style returns the lipgloss style for this notification type.
40+ func (t Type ) style () lipgloss.Style {
41+ switch t {
42+ case TypeError :
43+ return styles .NotificationErrorStyle
44+ case TypeWarning :
45+ return styles .NotificationWarningStyle
46+ case TypeInfo :
47+ return styles .NotificationInfoStyle
48+ default :
49+ return styles .NotificationStyle
50+ }
51+ }
52+
3353type ShowMsg struct {
3454 Text string
3555 Type Type // Defaults to TypeSuccess for backward compatibility
@@ -40,39 +60,41 @@ type HideMsg struct {
4060}
4161
4262func SuccessCmd (text string ) tea.Cmd {
43- return core .CmdHandler (ShowMsg {
44- Text : text ,
45- Type : TypeSuccess ,
46- })
63+ return core .CmdHandler (ShowMsg {Text : text , Type : TypeSuccess })
4764}
4865
4966func WarningCmd (text string ) tea.Cmd {
50- return core .CmdHandler (ShowMsg {
51- Text : text ,
52- Type : TypeWarning ,
53- })
67+ return core .CmdHandler (ShowMsg {Text : text , Type : TypeWarning })
5468}
5569
5670func InfoCmd (text string ) tea.Cmd {
57- return core .CmdHandler (ShowMsg {
58- Text : text ,
59- Type : TypeInfo ,
60- })
71+ return core .CmdHandler (ShowMsg {Text : text , Type : TypeInfo })
6172}
6273
6374func ErrorCmd (text string ) tea.Cmd {
64- return core .CmdHandler (ShowMsg {
65- Text : text ,
66- Type : TypeError ,
67- })
75+ return core .CmdHandler (ShowMsg {Text : text , Type : TypeError })
6876}
6977
7078// notificationItem represents a single notification
7179type notificationItem struct {
72- ID uint64
73- Text string
74- Type Type
75- TimerCmd tea.Cmd
80+ ID uint64
81+ Text string
82+ Type Type
83+ }
84+
85+ // render returns the styled view string for this notification item,
86+ // including a close button for persistent notifications.
87+ func (item notificationItem ) render (maxWidth int ) string {
88+ text := item .Text
89+ if item .Type .persistent () {
90+ text += closeButton
91+ }
92+
93+ style := item .Type .style ()
94+ if lipgloss .Width (text ) > maxWidth {
95+ return style .Width (maxWidth ).Render (text )
96+ }
97+ return style .Render (text )
7698}
7799
78100// Manager represents a notification manager that displays
@@ -110,19 +132,17 @@ func (n *Manager) Update(msg tea.Msg) (Manager, tea.Cmd) {
110132 notifType = TypeError
111133 }
112134 }
113- item := notificationItem {
114- ID : id ,
115- Text : msg .Text ,
116- Type : notifType ,
117- }
118-
119- item .TimerCmd = tea .Tick (defaultDuration , func (t time.Time ) tea.Msg {
120- return HideMsg {ID : id }
121- })
122135
136+ item := notificationItem {ID : id , Text : msg .Text , Type : notifType }
123137 n .items = append ([]notificationItem {item }, n .items ... )
124138
125- return * n , item .TimerCmd
139+ var cmd tea.Cmd
140+ if ! notifType .persistent () {
141+ cmd = tea .Tick (defaultDuration , func (t time.Time ) tea.Msg {
142+ return HideMsg {ID : id }
143+ })
144+ }
145+ return * n , cmd
126146
127147 case HideMsg :
128148 if msg .ID == 0 {
@@ -143,49 +163,24 @@ func (n *Manager) Update(msg tea.Msg) (Manager, tea.Cmd) {
143163 return * n , nil
144164}
145165
166+ // maxWidth returns the effective maximum width for notification text.
167+ func (n * Manager ) maxWidth () int {
168+ if n .width > 0 {
169+ return max (1 , min (maxNotificationWidth , n .width - notificationPadding * 2 ))
170+ }
171+ return maxNotificationWidth
172+ }
173+
146174func (n * Manager ) View () string {
147175 if len (n .items ) == 0 {
148176 return ""
149177 }
150178
151- var views []string
179+ mw := n .maxWidth ()
180+ views := make ([]string , 0 , len (n .items ))
152181 for i := len (n .items ) - 1 ; i >= 0 ; i -- {
153- item := n .items [i ]
154-
155- // Select style based on notification type
156- var style lipgloss.Style
157- switch item .Type {
158- case TypeError :
159- style = styles .NotificationErrorStyle
160- case TypeWarning :
161- style = styles .NotificationWarningStyle
162- case TypeInfo :
163- style = styles .NotificationInfoStyle
164- default :
165- style = styles .NotificationStyle
166- }
167-
168- // Apply max width constraint and word wrapping
169- text := item .Text
170- maxWidth := maxNotificationWidth
171- if n .width > 0 {
172- // Use smaller of maxNotificationWidth or available width minus padding
173- maxWidth = min (maxNotificationWidth , n .width - notificationPadding * 2 )
174- }
175-
176- // Only constrain width if text actually exceeds maxWidth
177- textWidth := lipgloss .Width (text )
178- var view string
179- if textWidth > maxWidth {
180- // Wrap text using lipgloss Width style - lipgloss will automatically wrap
181- view = style .Width (maxWidth ).Render (text )
182- } else {
183- // Use natural width for short text
184- view = style .Render (text )
185- }
186- views = append (views , view )
182+ views = append (views , n .items [i ].render (mw ))
187183 }
188-
189184 return lipgloss .JoinVertical (lipgloss .Right , views ... )
190185}
191186
@@ -215,3 +210,34 @@ func (n *Manager) position() (row, col int) {
215210func (n * Manager ) Open () bool {
216211 return len (n .items ) > 0
217212}
213+
214+ // HandleClick checks if the given screen coordinates hit a persistent
215+ // notification and dismisses it. Returns a command if a notification
216+ // was dismissed, nil otherwise.
217+ func (n * Manager ) HandleClick (x , y int ) tea.Cmd {
218+ if len (n .items ) == 0 {
219+ return nil
220+ }
221+
222+ row , col := n .position ()
223+ mw := n .maxWidth ()
224+ notifY := row
225+
226+ // Walk items bottom-to-top (same render order as View)
227+ for i := len (n .items ) - 1 ; i >= 0 ; i -- {
228+ item := n .items [i ]
229+ view := item .render (mw )
230+ viewHeight := lipgloss .Height (view )
231+
232+ if item .Type .persistent () {
233+ viewWidth := lipgloss .Width (view )
234+ if y >= notifY && y < notifY + viewHeight && x >= col && x < col + viewWidth {
235+ return core .CmdHandler (HideMsg {ID : item .ID })
236+ }
237+ }
238+
239+ notifY += viewHeight
240+ }
241+
242+ return nil
243+ }
0 commit comments