commit 7128ad8

delthas  ·  2025-11-12 15:02:23 +0000 UTC
parent 8a399a9
Support STATUSMSG-prefixed messages

Parse STATUSMSG prefixes in incoming messages, and push those messages
to the correct channel with the prefix indicated in the interface.

This also marks channel-context messages with a prefix.

Changes to ui.Line were added in order to support multiple styles
in the head.

Implements: https://todo.sr.ht/~delthas/senpai/183
Co-Authored-By: Taavi Väänänen <taavi@majava.org>
8 files changed,  +139, -126
M app.go
M app.go
+84, -99
  1@@ -484,7 +484,7 @@ func (app *App) ircLoop(netID string) {
  2 			if app.cfg.Debug {
  3 				app.queueStatusLine(netID, ui.Line{
  4 					At:   time.Now(),
  5-					Head: "IN --",
  6+					Head: ui.PlainString("IN --"),
  7 					Body: ui.PlainString(msg.String()),
  8 				})
  9 			}
 10@@ -498,16 +498,15 @@ func (app *App) ircLoop(netID string) {
 11 			content: nil,
 12 		})
 13 		app.queueStatusLine(netID, ui.Line{
 14-			Head:      "!!",
 15-			HeadColor: ui.ColorRed,
 16-			Body:      ui.PlainString("Connection lost"),
 17+			Head: ui.ColorString("!!", ui.ColorRed),
 18+			Body: ui.PlainString("Connection lost"),
 19 		})
 20 	}
 21 }
 22 
 23 func (app *App) connect(netID string) net.Conn {
 24 	app.queueStatusLine(netID, ui.Line{
 25-		Head: "--",
 26+		Head: ui.PlainString("--"),
 27 		Body: ui.PlainSprintf("Connecting to %s...", app.cfg.Addr),
 28 	})
 29 	conn, err := app.tryConnect()
 30@@ -515,9 +514,8 @@ func (app *App) connect(netID string) net.Conn {
 31 		return conn
 32 	}
 33 	app.queueStatusLine(netID, ui.Line{
 34-		Head:      "!!",
 35-		HeadColor: ui.ColorRed,
 36-		Body:      ui.PlainSprintf("Connection failed: %v", err),
 37+		Head: ui.ColorString("!!", ui.ColorRed),
 38+		Body: ui.PlainSprintf("Connection failed: %v", err),
 39 	})
 40 	return nil
 41 }
 42@@ -582,7 +580,7 @@ func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<-
 43 			}
 44 			app.queueStatusLine(netID, ui.Line{
 45 				At:   time.Now(),
 46-				Head: "OUT --",
 47+				Head: ui.PlainString("OUT --"),
 48 				Body: ui.PlainString(d.String()),
 49 			})
 50 			out <- msg
 51@@ -639,11 +637,10 @@ func (app *App) handleUIEvent(ev interface{}) bool {
 52 		if err := commandDoUpload(app, []string{ev.Path}); err != nil {
 53 			netID, buffer := app.win.CurrentBuffer()
 54 			app.win.AddLine(netID, buffer, ui.Line{
 55-				At:        time.Now(),
 56-				Head:      "!!",
 57-				HeadColor: ui.ColorRed,
 58-				Notify:    ui.NotifyUnread,
 59-				Body:      ui.PlainSprintf("SCREENSHOT: %s", err),
 60+				At:     time.Now(),
 61+				Head:   ui.ColorString("!!", ui.ColorRed),
 62+				Notify: ui.NotifyUnread,
 63+				Body:   ui.PlainSprintf("SCREENSHOT: %s", err),
 64 			})
 65 			break
 66 		}
 67@@ -669,7 +666,7 @@ func (app *App) handleUIEvent(ev interface{}) bool {
 68 				netID, buffer := app.win.CurrentBuffer()
 69 				app.win.AddLine(netID, buffer, ui.Line{
 70 					At:   time.Now(),
 71-					Head: "--",
 72+					Head: ui.PlainString("--"),
 73 					Body: ui.PlainString(fmt.Sprintf("File uploaded at: %v", ev.Location)),
 74 				})
 75 			}
 76@@ -677,10 +674,9 @@ func (app *App) handleUIEvent(ev interface{}) bool {
 77 			app.uploadingProgress = nil
 78 			netID, buffer := app.win.CurrentBuffer()
 79 			app.win.AddLine(netID, buffer, ui.Line{
 80-				At:        time.Now(),
 81-				Head:      "!!",
 82-				HeadColor: ui.ColorRed,
 83-				Body:      ui.PlainString(fmt.Sprintf("File upload failed: %v", ev.Error)),
 84+				At:   time.Now(),
 85+				Head: ui.ColorString("!!", ui.ColorRed),
 86+				Body: ui.PlainString(fmt.Sprintf("File upload failed: %v", ev.Error)),
 87 			})
 88 		} else {
 89 			app.uploadingProgress = &ev.Progress
 90@@ -699,7 +695,7 @@ func (app *App) handleUIEvent(ev interface{}) bool {
 91 					cNetID, cTarget := app.win.CurrentBuffer()
 92 					app.win.AddLine(cNetID, cTarget, ui.Line{
 93 						At:   time.Now(),
 94-						Head: "--",
 95+						Head: ui.PlainString("--"),
 96 						Body: ui.PlainString("An IRC link of a new channel was opened. Enter to add and join that channel."),
 97 					})
 98 					app.win.InputSet(fmt.Sprintf("/join %v", target))
 99@@ -712,7 +708,7 @@ func (app *App) handleUIEvent(ev interface{}) bool {
100 			netID, target := app.win.CurrentBuffer()
101 			app.win.AddLine(netID, target, ui.Line{
102 				At:   time.Now(),
103-				Head: "--",
104+				Head: ui.PlainString("--"),
105 				Body: ui.PlainString("An IRC link of a new network was opened. Enter to add and join that network."),
106 			})
107 			app.win.InputSet(fmt.Sprintf("/bouncer network create -addr %q", host))
108@@ -807,10 +803,9 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
109 				if failed {
110 					netID, buffer := app.win.CurrentBuffer()
111 					app.win.AddLine(netID, buffer, ui.Line{
112-						At:        time.Now(),
113-						Head:      "!!",
114-						HeadColor: ui.ColorRed,
115-						Body:      ui.PlainString(errNotSupported.Error()),
116+						At:   time.Now(),
117+						Head: ui.ColorString("!!", ui.ColorRed),
118+						Body: ui.PlainString(errNotSupported.Error()),
119 					})
120 				}
121 			} else {
122@@ -857,20 +852,19 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
123 					case 2:
124 						if _, err := getBouncerService(app); err != nil {
125 							app.win.AddLine(netID, target, ui.Line{
126-								At:        time.Now(),
127-								Head:      "--",
128-								HeadColor: ui.ColorRed,
129-								Body:      ui.PlainSprintf("Adding networks is not available: %v", err),
130+								At:   time.Now(),
131+								Head: ui.ColorString("--", ui.ColorRed),
132+								Body: ui.PlainSprintf("Adding networks is not available: %v", err),
133 							})
134 						} else {
135 							app.win.AddLine(netID, target, ui.Line{
136 								At:   time.Now(),
137-								Head: "--",
138+								Head: ui.PlainString("--"),
139 								Body: ui.PlainString("To join a network/server, use /bouncer network create -addr <address> [-name <name>]"),
140 							})
141 							app.win.AddLine(netID, target, ui.Line{
142 								At:   time.Now(),
143-								Head: "--",
144+								Head: ui.PlainString("--"),
145 								Body: ui.PlainString("For details, see /bouncer help network create"),
146 							})
147 							app.win.InputSet("/bouncer network create -addr ")
148@@ -878,14 +872,14 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
149 					case 4:
150 						app.win.AddLine(netID, target, ui.Line{
151 							At:   time.Now(),
152-							Head: "--",
153+							Head: ui.PlainString("--"),
154 							Body: ui.PlainString("To join a channel, use /join <#channel> [<password>]"),
155 						})
156 						app.win.InputSet("/join ")
157 					case 6:
158 						app.win.AddLine(netID, target, ui.Line{
159 							At:   time.Now(),
160-							Head: "--",
161+							Head: ui.PlainString("--"),
162 							Body: ui.PlainString("To message a user, use /query <user> [<message>]"),
163 						})
164 						app.win.InputSet("/query ")
165@@ -1000,11 +994,10 @@ func (app *App) handleAction(action string, args ...string) {
166 			for _, part := range strings.Split(input, "\n") {
167 				if err = app.handleInput(buffer, part); err != nil {
168 					app.win.AddLine(netID, buffer, ui.Line{
169-						At:        time.Now(),
170-						Head:      "!!",
171-						HeadColor: ui.ColorRed,
172-						Notify:    ui.NotifyUnread,
173-						Body:      ui.PlainSprintf("%q: %s", input, err),
174+						At:     time.Now(),
175+						Head:   ui.ColorString("!!", ui.ColorRed),
176+						Notify: ui.NotifyUnread,
177+						Body:   ui.PlainSprintf("%q: %s", input, err),
178 					})
179 					break
180 				}
181@@ -1030,11 +1023,10 @@ func (app *App) handleAction(action string, args ...string) {
182 	default:
183 		netID, buffer := app.win.CurrentBuffer()
184 		app.win.AddLine(netID, buffer, ui.Line{
185-			At:        time.Now(),
186-			Head:      "!!",
187-			HeadColor: ui.ColorRed,
188-			Notify:    ui.NotifyUnread,
189-			Body:      ui.PlainSprintf("shortcut: action %q does not exist", action),
190+			At:     time.Now(),
191+			Head:   ui.ColorString("!!", ui.ColorRed),
192+			Notify: ui.NotifyUnread,
193+			Body:   ui.PlainSprintf("shortcut: action %q does not exist", action),
194 		})
195 	}
196 }
197@@ -1453,10 +1445,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
198 	ev, err := s.HandleMessage(msg)
199 	if err != nil {
200 		app.win.AddLine(netID, "", ui.Line{
201-			Head:      "!!",
202-			HeadColor: ui.ColorRed,
203-			Notify:    ui.NotifyUnread,
204-			Body:      ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
205+			Head:   ui.ColorString("!!", ui.ColorRed),
206+			Notify: ui.NotifyUnread,
207+			Body:   ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
208 		})
209 		return
210 	}
211@@ -1505,7 +1496,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
212 		}
213 		app.addStatusLine(netID, ui.Line{
214 			At:   msg.TimeOrNow(),
215-			Head: "--",
216+			Head: ui.PlainString("--"),
217 			Body: ui.PlainString(body),
218 		})
219 		if !app.shownBouncerNotice && !s.IsBouncer() {
220@@ -1519,7 +1510,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
221 			} {
222 				app.addStatusLine(netID, ui.Line{
223 					At:   msg.TimeOrNow(),
224-					Head: "Bouncer --",
225+					Head: ui.PlainString("Bouncer --"),
226 					Body: ui.IRCString(line),
227 				})
228 			}
229@@ -1550,7 +1541,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
230 				cNetID, cTarget := app.win.CurrentBuffer()
231 				app.win.AddLine(cNetID, cTarget, ui.Line{
232 					At:   time.Now(),
233-					Head: "--",
234+					Head: ui.PlainString("--"),
235 					Body: ui.PlainString("An IRC link of a new channel was opened. Enter to add and join that channel."),
236 				})
237 				app.win.InputSet(fmt.Sprintf("/join %v", target))
238@@ -1574,8 +1565,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
239 		body.AddStyle(body.Len()-len(s.Nick()), textStyle)
240 		app.addStatusLine(netID, ui.Line{
241 			At:        msg.TimeOrNow(),
242-			Head:      "--",
243-			HeadColor: app.cfg.Colors.Status,
244+			Head:      ui.ColorString("--", app.cfg.Colors.Status),
245 			Body:      body.StyledString(),
246 			Highlight: true,
247 			Readable:  true,
248@@ -1679,10 +1669,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
249 			body = fmt.Sprintf("%s invited %s to join this channel", ev.Inviter, ev.Invitee)
250 		}
251 		app.win.AddLine(netID, buffer, ui.Line{
252-			At:        msg.TimeOrNow(),
253-			Head:      "--",
254-			HeadColor: app.cfg.Colors.Status,
255-			Notify:    notify,
256+			At:     msg.TimeOrNow(),
257+			Head:   ui.ColorString("--", app.cfg.Colors.Status),
258+			Notify: notify,
259 			Body: ui.Styled(body, vaxis.Style{
260 				Foreground: app.cfg.Colors.Status,
261 			}),
262@@ -1865,7 +1854,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
263 		netID, target := app.win.CurrentBuffer()
264 		app.win.AddLine(netID, target, ui.Line{
265 			At:   time.Now(),
266-			Head: "--",
267+			Head: ui.PlainString("--"),
268 			Body: ui.PlainString("An IRC link of a new network was opened. Enter to add and join that network."),
269 		})
270 		app.win.InputSet(fmt.Sprintf("/bouncer network create -addr %q", host))
271@@ -1876,9 +1865,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
272 				text += " -- " + item.Topic
273 			}
274 			app.addStatusLine(netID, ui.Line{
275-				At:        msg.TimeOrNow(),
276-				Head:      "List --",
277-				HeadColor: app.cfg.Colors.Status,
278+				At:   msg.TimeOrNow(),
279+				Head: ui.ColorString("List --", app.cfg.Colors.Status),
280 				Body: ui.Styled(text, vaxis.Style{
281 					Foreground: app.cfg.Colors.Status,
282 				}),
283@@ -1892,9 +1880,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
284 			head = "--"
285 		}
286 		app.addStatusLine(netID, ui.Line{
287-			At:        msg.TimeOrNow(),
288-			Head:      head,
289-			HeadColor: app.cfg.Colors.Status,
290+			At:   msg.TimeOrNow(),
291+			Head: ui.ColorString(head, app.cfg.Colors.Status),
292 			Body: ui.Styled(ev.Message, vaxis.Style{
293 				Foreground: app.cfg.Colors.Status,
294 			}),
295@@ -1905,9 +1892,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
296 		switch ev.Severity {
297 		case irc.SeverityNote:
298 			app.addStatusLine(netID, ui.Line{
299-				At:        msg.TimeOrNow(),
300-				Head:      fmt.Sprintf("(%s) --", ev.Code),
301-				HeadColor: app.cfg.Colors.Status,
302+				At:   msg.TimeOrNow(),
303+				Head: ui.ColorString(fmt.Sprintf("(%s) --", ev.Code), app.cfg.Colors.Status),
304 				Body: ui.Styled(ev.Message, vaxis.Style{
305 					Foreground: app.cfg.Colors.Status,
306 				}),
307@@ -1924,7 +1910,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
308 		}
309 		app.addStatusLine(netID, ui.Line{
310 			At:   msg.TimeOrNow(),
311-			Head: head,
312+			Head: ui.PlainString(head),
313 			Body: ui.PlainString(body),
314 		})
315 	}
316@@ -1997,10 +1983,9 @@ func (app *App) notifyHighlight(buffer, nick, content string, current bool) {
317 		if app.cfg.OnHighlightPath != "" {
318 			body := fmt.Sprintf("Unable to find on-highlight command at path: %q", path)
319 			app.addStatusLine(netID, ui.Line{
320-				At:        time.Now(),
321-				Head:      "!!",
322-				HeadColor: ui.ColorRed,
323-				Body:      ui.PlainString(body),
324+				At:   time.Now(),
325+				Head: ui.ColorString("!!", ui.ColorRed),
326+				Body: ui.PlainString(body),
327 			})
328 		}
329 		return
330@@ -2020,10 +2005,9 @@ func (app *App) notifyHighlight(buffer, nick, content string, current bool) {
331 	if err != nil {
332 		body := fmt.Sprintf("Failed to invoke on-highlight command at path: %v. Output: %q", err, string(output))
333 		app.addStatusLine(netID, ui.Line{
334-			At:        time.Now(),
335-			Head:      "!!",
336-			HeadColor: ui.ColorRed,
337-			Body:      ui.PlainString(body),
338+			At:   time.Now(),
339+			Head: ui.ColorString("!!", ui.ColorRed),
340+			Body: ui.PlainString(body),
341 		})
342 	}
343 }
344@@ -2113,8 +2097,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
345 		body.AddStyle(body.Len()-len(ev.User), textStyle)
346 		return ui.Line{
347 			At:        ev.Time,
348-			Head:      "--",
349-			HeadColor: app.cfg.Colors.Status,
350+			Head:      ui.ColorString("--", app.cfg.Colors.Status),
351 			Body:      body.StyledString(),
352 			Mergeable: true,
353 			Data:      []irc.Event{ev},
354@@ -2133,8 +2116,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
355 		body.WriteString(ev.User)
356 		return ui.Line{
357 			At:        ev.Time,
358-			Head:      "--",
359-			HeadColor: app.cfg.Colors.Status,
360+			Head:      ui.ColorString("--", app.cfg.Colors.Status),
361 			Body:      body.StyledString(),
362 			Mergeable: true,
363 			Data:      []irc.Event{ev},
364@@ -2153,8 +2135,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
365 		body.WriteString(ev.User)
366 		return ui.Line{
367 			At:        ev.Time,
368-			Head:      "--",
369-			HeadColor: app.cfg.Colors.Status,
370+			Head:      ui.ColorString("--", app.cfg.Colors.Status),
371 			Body:      body.StyledString(),
372 			Mergeable: true,
373 			Data:      []irc.Event{ev},
374@@ -2173,8 +2154,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
375 		body.WriteString(ev.User)
376 		return ui.Line{
377 			At:        ev.Time,
378-			Head:      "--",
379-			HeadColor: app.cfg.Colors.Status,
380+			Head:      ui.ColorString("--", app.cfg.Colors.Status),
381 			Body:      body.StyledString(),
382 			Mergeable: true,
383 			Data:      []irc.Event{ev},
384@@ -2185,10 +2165,9 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
385 		who := ui.IRCString(ev.Who).String()
386 		body := fmt.Sprintf("Topic changed by %s to: %s", who, topic)
387 		return ui.Line{
388-			At:        ev.Time,
389-			Head:      "--",
390-			HeadColor: app.cfg.Colors.Status,
391-			Notify:    ui.NotifyUnread,
392+			At:     ev.Time,
393+			Head:   ui.ColorString("--", app.cfg.Colors.Status),
394+			Notify: ui.NotifyUnread,
395 			Body: ui.Styled(body, vaxis.Style{
396 				Foreground: app.cfg.Colors.Status,
397 			}),
398@@ -2197,9 +2176,8 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
399 	case irc.ModeChangeEvent:
400 		body := fmt.Sprintf("[%s]", ev.Mode)
401 		return ui.Line{
402-			At:        ev.Time,
403-			Head:      "--",
404-			HeadColor: app.cfg.Colors.Status,
405+			At:   ev.Time,
406+			Head: ui.ColorString("--", app.cfg.Colors.Status),
407 			Body: ui.Styled(body, vaxis.Style{
408 				Foreground: app.cfg.Colors.Status,
409 			}),
410@@ -2318,12 +2296,21 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
411 		notification = ui.NotifyUnread
412 	}
413 
414-	head := ev.User
415-	var headColor vaxis.Color
416+	var head ui.StyledStringBuilder
417+	if ev.TargetPrefix != "" {
418+		head.WriteStyledString(ui.ColorString(ev.TargetPrefix, ui.ColorGreen))
419+	}
420+
421 	if isAction || isNotice {
422-		head = "*"
423+		if head.Len() == 0 {
424+			head.WriteStyledString(ui.PlainString("*"))
425+		}
426 	} else {
427-		headColor = app.win.IdentColor(app.cfg.Colors.Nicks, head, isFromSelf)
428+		if head.Len() > 0 {
429+			head.WriteStyledString(ui.PlainString(" "))
430+		}
431+		c := app.win.IdentColor(app.cfg.Colors.Nicks, ev.User, isFromSelf)
432+		head.WriteStyledString(ui.ColorString(ev.User, c))
433 	}
434 
435 	var body ui.StyledStringBuilder
436@@ -2351,8 +2338,7 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
437 
438 	line = ui.Line{
439 		At:        ev.Time,
440-		Head:      head,
441-		HeadColor: headColor,
442+		Head:      head.StyledString(),
443 		Notify:    notification,
444 		Body:      body.StyledString(),
445 		Highlight: hlLine,
446@@ -2549,9 +2535,8 @@ func (app *App) printTopic(netID, buffer string) (ok bool) {
447 		body = fmt.Sprintf("Topic (set by %s on %s): %s", who.Name, at.Local().Format("January 2 2006 at 15:04:05"), topic)
448 	}
449 	app.win.AddLine(netID, buffer, ui.Line{
450-		At:        time.Now(),
451-		Head:      "--",
452-		HeadColor: app.cfg.Colors.Status,
453+		At:   time.Now(),
454+		Head: ui.ColorString("--", app.cfg.Colors.Status),
455 		Body: ui.Styled(body, vaxis.Style{
456 			Foreground: app.cfg.Colors.Status,
457 		}),
+5, -6
 1@@ -429,7 +429,7 @@ func commandDoHelp(app *App, args []string) (err error) {
 2 	if len(args) == 0 {
 3 		app.win.AddLine(netID, buffer, ui.Line{
 4 			At:   t,
 5-			Head: "--",
 6+			Head: ui.PlainString("--"),
 7 			Body: ui.PlainString("Available commands:"),
 8 		})
 9 
10@@ -442,7 +442,7 @@ func commandDoHelp(app *App, args []string) (err error) {
11 		search := strings.ToUpper(args[0])
12 		app.win.AddLine(netID, buffer, ui.Line{
13 			At:   t,
14-			Head: "--",
15+			Head: ui.PlainString("--"),
16 			Body: ui.PlainSprintf("Commands that match \"%s\":", search),
17 		})
18 
19@@ -589,10 +589,9 @@ func commandDoNames(app *App, args []string) (err error) {
20 	body := sb.StyledString()
21 	// TODO remove last space
22 	app.win.AddLine(netID, buffer, ui.Line{
23-		At:        time.Now(),
24-		Head:      "--",
25-		HeadColor: app.cfg.Colors.Status,
26-		Body:      body,
27+		At:   time.Now(),
28+		Head: ui.ColorString("--", app.cfg.Colors.Status),
29+		Body: body,
30 	})
31 	return nil
32 }
+1, -0
1@@ -87,6 +87,7 @@ type MessageEvent struct {
2 	User            string
3 	Target          string
4 	TargetIsChannel bool
5+	TargetPrefix    string
6 	Command         string
7 	Content         string
8 	Time            time.Time
+23, -8
 1@@ -145,6 +145,7 @@ type Session struct {
 2 
 3 	serverName    string
 4 	defaultPrefix *Prefix
 5+
 6 	// ISUPPORT features
 7 	casemap       func(string) string
 8 	chanmodes     [4]string
 9@@ -153,6 +154,7 @@ type Session struct {
10 	historyLimit  int
11 	prefixSymbols string
12 	prefixModes   string
13+	statusmsg     string
14 	monitor       bool
15 	whox          bool
16 	listMask      bool
17@@ -1378,7 +1380,7 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
18 			return nil, err
19 		}
20 
21-		targetCf := s.casemap(target)
22+		targetCf := strings.TrimLeft(s.casemap(target), s.statusmsg)
23 		nickCf := s.casemap(msg.Prefix.Name)
24 		if !playback {
25 			s.typings.Done(targetCf, nickCf)
26@@ -2123,22 +2125,33 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
27 }
28 
29 func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
30-	var target, content string
31-	if err := msg.ParseParams(&target, &content); err != nil {
32+	var rawTarget, content string
33+	if err := msg.ParseParams(&rawTarget, &content); err != nil {
34 		return ev, err
35 	}
36 
37+	i := strings.IndexFunc(rawTarget, func(r rune) bool {
38+		return !strings.ContainsRune(s.statusmsg, r)
39+	})
40+	if i < 0 {
41+		i = len(rawTarget)
42+	}
43+	prefix := rawTarget[:i]
44+	target := rawTarget[i:]
45+
46 	ev = MessageEvent{
47-		User:    msg.Prefix.Name, // TODO correctly casemap
48-		Target:  target,          // TODO correctly casemap
49-		Command: msg.Command,
50-		Content: content,
51-		Time:    msg.TimeOrNow(),
52+		User:         msg.Prefix.Name, // TODO correctly casemap
53+		Target:       target,          // TODO correctly casemap
54+		TargetPrefix: prefix,
55+		Command:      msg.Command,
56+		Content:      content,
57+		Time:         msg.TimeOrNow(),
58 	}
59 
60 	if s.IsMe(target) {
61 		if context := msg.Tags["+draft/channel-context"]; context != "" {
62 			target = context
63+			ev.TargetPrefix = "*"
64 		}
65 	}
66 	targetCf := s.Casemap(target)
67@@ -2246,6 +2259,8 @@ func (s *Session) updateFeatures(features []string) {
68 			numPrefixes := len(value)/2 - 1
69 			s.prefixModes = value[1 : numPrefixes+1]
70 			s.prefixSymbols = value[numPrefixes+2:]
71+		case "STATUSMSG":
72+			s.statusmsg = value
73 		case "WHOX":
74 			s.whox = true
75 		case "SOJU.IM/FILEHOST":
+18, -9
 1@@ -42,9 +42,8 @@ const (
 2 
 3 type Line struct {
 4 	At        time.Time
 5-	Head      string
 6+	Head      StyledString
 7 	Body      StyledString
 8-	HeadColor vaxis.Color
 9 	Notify    NotifyType
10 	Highlight bool
11 	Readable  bool
12@@ -1239,15 +1238,25 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
13 		}
14 
15 		if yi >= y0 {
16-			identSt := vaxis.Style{
17-				Foreground: line.HeadColor,
18+			head := line.Head
19+			if line.Highlight && len(line.Head.styles) > 0 {
20+				var sb StyledStringBuilder
21+				sb.WriteString(head.string)
22+				for _, st := range line.Head.styles {
23+					s := st.Style
24+					s.Attribute |= vaxis.AttrReverse
25+					sb.AddStyle(st.Start, s)
26+				}
27+				head = sb.StyledString()
28 			}
29-			if line.Highlight {
30-				identSt.Attribute |= vaxis.AttrReverse
31+			xb, xe := printIdent(vx, x0+7, yi, nickColWidth, head)
32+
33+			lastHead := line.Head.string
34+			if len(line.Head.styles) > 0 {
35+				lastHead = lastHead[line.Head.styles[len(line.Head.styles)-1].Start:]
36 			}
37-			xb, xe := printIdent(vx, x0+7, yi, nickColWidth, Styled(line.Head, identSt))
38 
39-			if !strings.HasSuffix(line.Head, "--") && !strings.HasSuffix(line.Head, "!!") {
40+			if !strings.HasSuffix(lastHead, "--") && !strings.HasSuffix(lastHead, "!!") {
41 				ui.clickEvents = append(ui.clickEvents, clickEvent{
42 					xb: xb,
43 					xe: xe,
44@@ -1257,7 +1266,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
45 							NetID:  b.netID,
46 							Buffer: b.title,
47 						},
48-						Nick: line.Head,
49+						Nick: lastHead,
50 					},
51 				})
52 			}
+4, -0
 1@@ -74,6 +74,10 @@ func PlainSprintf(format string, a ...interface{}) StyledString {
 2 	return PlainString(fmt.Sprintf(format, a...))
 3 }
 4 
 5+func ColorString(s string, fg vaxis.Color) StyledString {
 6+	return Styled(s, vaxis.Style{Foreground: fg})
 7+}
 8+
 9 func Styled(s string, style vaxis.Style) StyledString {
10 	rStyle := rangedStyle{
11 		Start: 0,
+3, -3
 1@@ -491,10 +491,10 @@ func (ui *UI) AddLine(netID, buffer string, line Line) {
 2 	focused := ui.bs.focused && curNetID == netID && curBuffer == buffer
 3 	if b != nil && line.Notify == NotifyHighlight && !focused {
 4 		var header string
 5-		if buffer != line.Head {
 6-			header = fmt.Sprintf("%s — %s", buffer, line.Head)
 7+		if buffer != line.Head.String() {
 8+			header = fmt.Sprintf("%s — %s", buffer, line.Head.String())
 9 		} else {
10-			header = line.Head
11+			header = line.Head.String()
12 		}
13 		id := ui.notify(NotifyEvent{
14 			NetID:  netID,
+1, -1
1@@ -16,7 +16,7 @@ func (app *App) initWindow() {
2 	}
3 	app.win.AddBuffer("", "(home)", "")
4 	app.win.AddLine("", "", ui.Line{
5-		Head: "--",
6+		Head: ui.PlainString("--"),
7 		Body: ui.PlainString(fmt.Sprintf("Welcome to senpai %v! To get started, use the Help buttons, or enter /help for a list of commands.", version)),
8 		At:   time.Now(),
9 	})