commit 9fb4378
Hubert Hirtz
·
2021-10-20 15:33:10 +0000 UTC
parent 40544a1
Support for soju.im/bouncer-networks This patch also disable the highlight on reconnect. This might be an issue (the user would want to know when senpai is online again?), but with multiple connections, it's bothersome to have to unread all of them on start (it wasn't a problem with only one connection since it was read instantly). Now, lastbuffer.txt also contains the network ID, otherwise the user might end up on another buffer with the same name. This patch does not extend /r to support multiple networks (it will send the message to the latest query, whatever the current displayed network is).
11 files changed,
+444,
-275
M
app.go
+158,
-118
1@@ -17,13 +17,6 @@ import (
2
3 const eventChanSize = 64
4
5-type source int
6-
7-const (
8- uiEvent source = iota
9- ircEvent
10-)
11-
12 func isCommand(input []rune) bool {
13 // Command can't start with two slashes because that's an escape for
14 // a literal slash in the message
15@@ -70,30 +63,32 @@ func (b *bound) Update(line *ui.Line) {
16 }
17
18 type event struct {
19- src source
20+ src string // "*" if UI, netID otherwise
21 content interface{}
22 }
23
24 type App struct {
25- win *ui.UI
26- s *irc.Session
27- pasting bool
28- events chan event
29+ win *ui.UI
30+ sessions map[string]*irc.Session
31+ pasting bool
32+ events chan event
33
34 cfg Config
35 highlights []string
36
37 lastQuery string
38+ lastQueryNet string
39 messageBounds map[string]bound
40+ lastNetID string
41 lastBuffer string
42 }
43
44-func NewApp(cfg Config, lastBuffer string) (app *App, err error) {
45+func NewApp(cfg Config) (app *App, err error) {
46 app = &App{
47- cfg: cfg,
48+ sessions: map[string]*irc.Session{},
49 events: make(chan event, eventChanSize),
50+ cfg: cfg,
51 messageBounds: map[string]bound{},
52- lastBuffer: lastBuffer,
53 }
54
55 if cfg.Highlights != nil {
56@@ -133,18 +128,28 @@ func NewApp(cfg Config, lastBuffer string) (app *App, err error) {
57
58 func (app *App) Close() {
59 app.win.Close()
60- if app.s != nil {
61- app.s.Close()
62+ for _, session := range app.sessions {
63+ session.Close()
64 }
65 }
66
67+func (app *App) SwitchToBuffer(netID, buffer string) {
68+ app.lastNetID = netID
69+ app.lastBuffer = buffer
70+}
71+
72 func (app *App) Run() {
73 go app.uiLoop()
74- go app.ircLoop()
75+ go app.ircLoop("")
76 app.eventLoop()
77 }
78
79-func (app *App) CurrentBuffer() string {
80+func (app *App) CurrentSession() *irc.Session {
81+ netID, _ := app.win.CurrentBuffer()
82+ return app.sessions[netID]
83+}
84+
85+func (app *App) CurrentBuffer() (netID, buffer string) {
86 return app.win.CurrentBuffer()
87 }
88
89@@ -172,8 +177,10 @@ func (app *App) eventLoop() {
90 app.updatePrompt()
91 app.setBufferNumbers()
92 var currentMembers []irc.Member
93- if app.s != nil {
94- currentMembers = app.s.Names(app.win.CurrentBuffer())
95+ netID, buffer := app.win.CurrentBuffer()
96+ s := app.sessions[netID]
97+ if s != nil && buffer != "" {
98+ currentMembers = s.Names(buffer)
99 }
100 app.win.Draw(currentMembers)
101 }
102@@ -182,7 +189,7 @@ func (app *App) eventLoop() {
103
104 // ircLoop maintains a connection to the IRC server by connecting and then
105 // forwarding IRC events to app.events repeatedly.
106-func (app *App) ircLoop() {
107+func (app *App) ircLoop(netID string) {
108 var auth irc.SASLClient
109 if app.cfg.Password != nil {
110 auth = &irc.SASLPlain{
111@@ -194,45 +201,46 @@ func (app *App) ircLoop() {
112 Nickname: app.cfg.Nick,
113 Username: app.cfg.User,
114 RealName: app.cfg.Real,
115+ NetID: netID,
116 Auth: auth,
117 }
118 for !app.win.ShouldExit() {
119- conn := app.connect()
120+ conn := app.connect(netID)
121 in, out := irc.ChanInOut(conn)
122 if app.cfg.Debug {
123- out = app.debugOutputMessages(out)
124+ out = app.debugOutputMessages(netID, out)
125 }
126 session := irc.NewSession(out, params)
127 app.events <- event{
128- src: ircEvent,
129+ src: netID,
130 content: session,
131 }
132 go func() {
133 for stop := range session.TypingStops() {
134 app.events <- event{
135- src: ircEvent,
136+ src: netID,
137 content: stop,
138 }
139 }
140 }()
141 for msg := range in {
142 if app.cfg.Debug {
143- app.queueStatusLine(ui.Line{
144+ app.queueStatusLine(netID, ui.Line{
145 At: time.Now(),
146 Head: "IN --",
147 Body: ui.PlainString(msg.String()),
148 })
149 }
150 app.events <- event{
151- src: ircEvent,
152+ src: netID,
153 content: msg,
154 }
155 }
156 app.events <- event{
157- src: ircEvent,
158+ src: netID,
159 content: nil,
160 }
161- app.queueStatusLine(ui.Line{
162+ app.queueStatusLine(netID, ui.Line{
163 Head: "!!",
164 HeadColor: tcell.ColorRed,
165 Body: ui.PlainString("Connection lost"),
166@@ -241,9 +249,9 @@ func (app *App) ircLoop() {
167 }
168 }
169
170-func (app *App) connect() net.Conn {
171+func (app *App) connect(netID string) net.Conn {
172 for {
173- app.queueStatusLine(ui.Line{
174+ app.queueStatusLine(netID, ui.Line{
175 Head: "--",
176 Body: ui.PlainSprintf("Connecting to %s...", app.cfg.Addr),
177 })
178@@ -251,7 +259,7 @@ func (app *App) connect() net.Conn {
179 if err == nil {
180 return conn
181 }
182- app.queueStatusLine(ui.Line{
183+ app.queueStatusLine(netID, ui.Line{
184 Head: "!!",
185 HeadColor: tcell.ColorRed,
186 Body: ui.PlainSprintf("Connection failed: %v", err),
187@@ -295,11 +303,11 @@ func (app *App) tryConnect() (conn net.Conn, err error) {
188 return
189 }
190
191-func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message {
192+func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<- irc.Message {
193 debugOut := make(chan irc.Message, cap(out))
194 go func() {
195 for msg := range debugOut {
196- app.queueStatusLine(ui.Line{
197+ app.queueStatusLine(netID, ui.Line{
198 At: time.Now(),
199 Head: "OUT --",
200 Body: ui.PlainString(msg.String()),
201@@ -316,7 +324,7 @@ func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message {
202 func (app *App) uiLoop() {
203 for ev := range app.win.Events {
204 app.events <- event{
205- src: uiEvent,
206+ src: "*",
207 content: ev,
208 }
209 }
210@@ -325,13 +333,10 @@ func (app *App) uiLoop() {
211 // handleEvents handles a batch of events.
212 func (app *App) handleEvents(evs []event) {
213 for _, ev := range evs {
214- switch ev.src {
215- case uiEvent:
216+ if ev.src == "*" {
217 app.handleUIEvent(ev.content)
218- case ircEvent:
219- app.handleIRCEvent(ev.content)
220- default:
221- panic("unreachable")
222+ } else {
223+ app.handleIRCEvent(ev.src, ev.content)
224 }
225 }
226 }
227@@ -346,10 +351,10 @@ func (app *App) handleUIEvent(ev interface{}) {
228 app.handleMouseEvent(ev)
229 case *tcell.EventKey:
230 app.handleKeyEvent(ev)
231- case ui.Line:
232- app.addStatusLine(ev)
233+ case statusLine:
234+ app.addStatusLine(ev.netID, ev.line)
235 default:
236- return
237+ panic("unreachable")
238 }
239 }
240
241@@ -472,11 +477,11 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
242 app.typing()
243 }
244 case tcell.KeyCR, tcell.KeyLF:
245- buffer := app.win.CurrentBuffer()
246+ netID, buffer := app.win.CurrentBuffer()
247 input := app.win.InputEnter()
248 err := app.handleInput(buffer, input)
249 if err != nil {
250- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyUnread, ui.Line{
251+ app.win.AddLine(netID, buffer, ui.NotifyUnread, ui.Line{
252 At: time.Now(),
253 Head: "!!",
254 HeadColor: tcell.ColorRed,
255@@ -494,29 +499,35 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
256 // requestHistory is a wrapper around irc.Session.RequestHistory to only request
257 // history when needed.
258 func (app *App) requestHistory() {
259- if app.s == nil {
260+ netID, buffer := app.win.CurrentBuffer()
261+ s := app.sessions[netID]
262+ if s == nil {
263 return
264 }
265- buffer := app.win.CurrentBuffer()
266- if app.win.IsAtTop() && buffer != Home {
267+ if app.win.IsAtTop() && buffer != "" {
268 t := time.Now()
269 if bound, ok := app.messageBounds[buffer]; ok {
270 t = bound.first
271 }
272- app.s.NewHistoryRequest(buffer).
273+ s.NewHistoryRequest(buffer).
274 WithLimit(100).
275 Before(t)
276 }
277 }
278
279-func (app *App) handleIRCEvent(ev interface{}) {
280+func (app *App) handleIRCEvent(netID string, ev interface{}) {
281 if ev == nil {
282- app.s.Close()
283- app.s = nil
284+ if s, ok := app.sessions[netID]; ok {
285+ s.Close()
286+ delete(app.sessions, netID)
287+ }
288 return
289 }
290 if s, ok := ev.(*irc.Session); ok {
291- app.s = s
292+ if s, ok := app.sessions[netID]; ok {
293+ s.Close()
294+ }
295+ app.sessions[netID] = s
296 return
297 }
298 if _, ok := ev.(irc.Typing); ok {
299@@ -524,12 +535,19 @@ func (app *App) handleIRCEvent(ev interface{}) {
300 return
301 }
302
303- msg := ev.(irc.Message)
304+ msg, ok := ev.(irc.Message)
305+ if !ok {
306+ panic("unreachable")
307+ }
308+ s, ok := app.sessions[netID]
309+ if !ok {
310+ panic("unreachable")
311+ }
312
313 // Mutate IRC state
314- ev, err := app.s.HandleMessage(msg)
315+ ev, err := s.HandleMessage(msg)
316 if err != nil {
317- app.win.AddLine(Home, ui.NotifyUnread, ui.Line{
318+ app.win.AddLine(netID, "", ui.NotifyUnread, ui.Line{
319 Head: "!!",
320 HeadColor: tcell.ColorRed,
321 Body: ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
322@@ -543,26 +561,26 @@ func (app *App) handleIRCEvent(ev interface{}) {
323 for _, channel := range app.cfg.Channels {
324 // TODO: group JOIN messages
325 // TODO: support autojoining channels with keys
326- app.s.Join(channel, "")
327+ s.Join(channel, "")
328 }
329 body := "Connected to the server"
330- if app.s.Nick() != app.cfg.Nick {
331- body = fmt.Sprintf("Connected to the server as %s", app.s.Nick())
332+ if s.Nick() != app.cfg.Nick {
333+ body = fmt.Sprintf("Connected to the server as %s", s.Nick())
334 }
335- app.win.AddLine(Home, ui.NotifyUnread, ui.Line{
336+ app.win.AddLine(netID, "", ui.NotifyNone, ui.Line{
337 At: msg.TimeOrNow(),
338 Head: "--",
339 Body: ui.PlainString(body),
340 })
341 case irc.SelfNickEvent:
342 var body ui.StyledStringBuilder
343- body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, app.s.Nick()))
344+ body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, s.Nick()))
345 textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray)
346 arrowStyle := tcell.StyleDefault
347 body.AddStyle(0, textStyle)
348 body.AddStyle(len(ev.FormerNick), arrowStyle)
349- body.AddStyle(body.Len()-len(app.s.Nick()), textStyle)
350- app.addStatusLine(ui.Line{
351+ body.AddStyle(body.Len()-len(s.Nick()), textStyle)
352+ app.addStatusLine(netID, ui.Line{
353 At: msg.TimeOrNow(),
354 Head: "--",
355 HeadColor: tcell.ColorGray,
356@@ -577,8 +595,8 @@ func (app *App) handleIRCEvent(ev interface{}) {
357 body.AddStyle(0, textStyle)
358 body.AddStyle(len(ev.FormerNick), arrowStyle)
359 body.AddStyle(body.Len()-len(ev.User), textStyle)
360- for _, c := range app.s.ChannelsSharedWith(ev.User) {
361- app.win.AddLine(c, ui.NotifyNone, ui.Line{
362+ for _, c := range s.ChannelsSharedWith(ev.User) {
363+ app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
364 At: msg.TimeOrNow(),
365 Head: "--",
366 HeadColor: tcell.ColorGray,
367@@ -587,14 +605,14 @@ func (app *App) handleIRCEvent(ev interface{}) {
368 })
369 }
370 case irc.SelfJoinEvent:
371- i, added := app.win.AddBuffer(ev.Channel)
372+ i, added := app.win.AddBuffer(netID, "", ev.Channel)
373 bounds, ok := app.messageBounds[ev.Channel]
374 if added || !ok {
375- app.s.NewHistoryRequest(ev.Channel).
376+ s.NewHistoryRequest(ev.Channel).
377 WithLimit(200).
378 Before(msg.TimeOrNow())
379 } else {
380- app.s.NewHistoryRequest(ev.Channel).
381+ s.NewHistoryRequest(ev.Channel).
382 WithLimit(200).
383 After(bounds.last)
384 }
385@@ -602,13 +620,14 @@ func (app *App) handleIRCEvent(ev interface{}) {
386 app.win.JumpBufferIndex(i)
387 }
388 if ev.Topic != "" {
389- app.printTopic(ev.Channel)
390+ app.printTopic(netID, ev.Channel)
391 }
392
393 // Restore last buffer
394- lastBuffer := app.lastBuffer
395- if ev.Channel == lastBuffer {
396- app.win.JumpBuffer(lastBuffer)
397+ if netID == app.lastNetID && ev.Channel == app.lastBuffer {
398+ app.win.JumpBufferNetwork(app.lastNetID, app.lastBuffer)
399+ app.lastNetID = ""
400+ app.lastBuffer = ""
401 }
402 case irc.UserJoinEvent:
403 var body ui.StyledStringBuilder
404@@ -617,7 +636,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
405 body.WriteByte('+')
406 body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
407 body.WriteString(ev.User)
408- app.win.AddLine(ev.Channel, ui.NotifyNone, ui.Line{
409+ app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
410 At: msg.TimeOrNow(),
411 Head: "--",
412 HeadColor: tcell.ColorGray,
413@@ -634,7 +653,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
414 body.WriteByte('-')
415 body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
416 body.WriteString(ev.User)
417- app.win.AddLine(ev.Channel, ui.NotifyNone, ui.Line{
418+ app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
419 At: msg.TimeOrNow(),
420 Head: "--",
421 HeadColor: tcell.ColorGray,
422@@ -649,7 +668,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
423 body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
424 body.WriteString(ev.User)
425 for _, c := range ev.Channels {
426- app.win.AddLine(c, ui.NotifyNone, ui.Line{
427+ app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
428 At: msg.TimeOrNow(),
429 Head: "--",
430 HeadColor: tcell.ColorGray,
431@@ -659,7 +678,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
432 }
433 case irc.TopicChangeEvent:
434 body := fmt.Sprintf("Topic changed to: %s", ev.Topic)
435- app.win.AddLine(ev.Channel, ui.NotifyUnread, ui.Line{
436+ app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
437 At: msg.TimeOrNow(),
438 Head: "--",
439 HeadColor: tcell.ColorGray,
440@@ -667,7 +686,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
441 })
442 case irc.ModeChangeEvent:
443 body := fmt.Sprintf("Mode change: %s", ev.Mode)
444- app.win.AddLine(ev.Channel, ui.NotifyUnread, ui.Line{
445+ app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
446 At: msg.TimeOrNow(),
447 Head: "--",
448 HeadColor: tcell.ColorGray,
449@@ -677,11 +696,11 @@ func (app *App) handleIRCEvent(ev interface{}) {
450 var buffer string
451 var notify ui.NotifyType
452 var body string
453- if app.s.IsMe(ev.Invitee) {
454- buffer = Home
455+ if s.IsMe(ev.Invitee) {
456+ buffer = ""
457 notify = ui.NotifyHighlight
458 body = fmt.Sprintf("%s invited you to join %s", ev.Inviter, ev.Channel)
459- } else if app.s.IsMe(ev.Inviter) {
460+ } else if s.IsMe(ev.Inviter) {
461 buffer = ev.Channel
462 notify = ui.NotifyNone
463 body = fmt.Sprintf("You invited %s to join this channel", ev.Invitee)
464@@ -690,7 +709,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
465 notify = ui.NotifyUnread
466 body = fmt.Sprintf("%s invited %s to join this channel", ev.Inviter, ev.Invitee)
467 }
468- app.win.AddLine(buffer, notify, ui.Line{
469+ app.win.AddLine(netID, buffer, notify, ui.Line{
470 At: msg.TimeOrNow(),
471 Head: "--",
472 HeadColor: tcell.ColorGray,
473@@ -698,19 +717,20 @@ func (app *App) handleIRCEvent(ev interface{}) {
474 Highlight: notify == ui.NotifyHighlight,
475 })
476 case irc.MessageEvent:
477- buffer, line, hlNotification := app.formatMessage(ev)
478+ buffer, line, hlNotification := app.formatMessage(s, ev)
479 var notify ui.NotifyType
480 if hlNotification {
481 notify = ui.NotifyHighlight
482 } else {
483 notify = ui.NotifyUnread
484 }
485- app.win.AddLine(buffer, notify, line)
486+ app.win.AddLine(netID, buffer, notify, line)
487 if hlNotification {
488 app.notifyHighlight(buffer, ev.User, line.Body.String())
489 }
490- if !app.s.IsChannel(msg.Params[0]) && !app.s.IsMe(ev.User) {
491+ if !s.IsChannel(msg.Params[0]) && !s.IsMe(ev.User) {
492 app.lastQuery = msg.Prefix.Name
493+ app.lastQueryNet = netID
494 }
495 bounds := app.messageBounds[ev.Target]
496 bounds.Update(&line)
497@@ -722,7 +742,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
498 for _, m := range ev.Messages {
499 switch ev := m.(type) {
500 case irc.MessageEvent:
501- _, line, _ := app.formatMessage(ev)
502+ _, line, _ := app.formatMessage(s, ev)
503 if hasBounds {
504 c := bounds.Compare(&line)
505 if c < 0 {
506@@ -735,7 +755,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
507 }
508 }
509 }
510- app.win.AddLines(ev.Target, linesBefore, linesAfter)
511+ app.win.AddLines(netID, ev.Target, linesBefore, linesAfter)
512 if len(linesBefore) != 0 {
513 bounds.Update(&linesBefore[0])
514 bounds.Update(&linesBefore[len(linesBefore)-1])
515@@ -745,6 +765,11 @@ func (app *App) handleIRCEvent(ev interface{}) {
516 bounds.Update(&linesAfter[len(linesAfter)-1])
517 }
518 app.messageBounds[ev.Target] = bounds
519+ case irc.BouncerNetworkEvent:
520+ _, added := app.win.AddBuffer(ev.ID, ev.Name, "")
521+ if added {
522+ go app.ircLoop(ev.ID)
523+ }
524 case irc.ErrorEvent:
525 if isBlackListed(msg.Command) {
526 break
527@@ -764,7 +789,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
528 default:
529 panic("unreachable")
530 }
531- app.addStatusLine(ui.Line{
532+ app.addStatusLine(netID, ui.Line{
533 At: msg.TimeOrNow(),
534 Head: head,
535 Body: ui.PlainString(body),
536@@ -782,13 +807,13 @@ func isBlackListed(command string) bool {
537 }
538
539 // isHighlight reports whether the given message content is a highlight.
540-func (app *App) isHighlight(content string) bool {
541- contentCf := app.s.Casemap(content)
542+func (app *App) isHighlight(s *irc.Session, content string) bool {
543+ contentCf := s.Casemap(content)
544 if app.highlights == nil {
545- return strings.Contains(contentCf, app.s.NickCf())
546+ return strings.Contains(contentCf, s.NickCf())
547 }
548 for _, h := range app.highlights {
549- if strings.Contains(contentCf, app.s.Casemap(h)) {
550+ if strings.Contains(contentCf, s.Casemap(h)) {
551 return true
552 }
553 }
554@@ -805,8 +830,9 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
555 if err != nil {
556 return
557 }
558+ netID, curBuffer := app.win.CurrentBuffer()
559 here := "0"
560- if buffer == app.win.CurrentBuffer() {
561+ if buffer == curBuffer { // TODO also check netID
562 here = "1"
563 }
564 cmd := exec.Command(sh, "-c", app.cfg.OnHighlight)
565@@ -819,7 +845,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
566 output, err := cmd.CombinedOutput()
567 if err != nil {
568 body := fmt.Sprintf("Failed to invoke on-highlight command: %v. Output: %q", err, string(output))
569- app.addStatusLine(ui.Line{
570+ app.addStatusLine(netID, ui.Line{
571 At: time.Now(),
572 Head: "!!",
573 HeadColor: tcell.ColorRed,
574@@ -831,32 +857,36 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
575 // typing sends typing notifications to the IRC server according to the user
576 // input.
577 func (app *App) typing() {
578- if app.s == nil || app.cfg.NoTypings {
579+ netID, buffer := app.win.CurrentBuffer()
580+ s := app.sessions[netID]
581+ if s == nil || app.cfg.NoTypings {
582 return
583 }
584- buffer := app.win.CurrentBuffer()
585- if buffer == Home {
586+ if buffer == "" {
587 return
588 }
589 input := app.win.InputContent()
590 if len(input) == 0 {
591- app.s.TypingStop(buffer)
592+ s.TypingStop(buffer)
593 } else if !isCommand(input) {
594- app.s.Typing(app.win.CurrentBuffer())
595+ s.Typing(buffer)
596 }
597 }
598
599 // completions computes the list of completions given the input text and the
600 // cursor position.
601 func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
602- var cs []ui.Completion
603-
604 if len(text) == 0 {
605- return cs
606+ return nil
607+ }
608+ netID, buffer := app.win.CurrentBuffer()
609+ s := app.sessions[netID]
610+ if s == nil {
611+ return nil
612 }
613
614- buffer := app.win.CurrentBuffer()
615- if app.s.IsChannel(buffer) {
616+ var cs []ui.Completion
617+ if buffer != "" {
618 cs = app.completionsChannelTopic(cs, cursorIdx, text)
619 cs = app.completionsChannelMembers(cs, cursorIdx, text)
620 }
621@@ -878,17 +908,22 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
622 // - which buffer the message must be added to,
623 // - the UI line,
624 // - whether senpai must trigger the "on-highlight" command.
625-func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line, hlNotification bool) {
626- isFromSelf := app.s.IsMe(ev.User)
627- isHighlight := app.isHighlight(ev.Content)
628+func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer string, line ui.Line, hlNotification bool) {
629+ isFromSelf := s.IsMe(ev.User)
630+ isHighlight := app.isHighlight(s, ev.Content)
631 isAction := strings.HasPrefix(ev.Content, "\x01ACTION")
632 isQuery := !ev.TargetIsChannel && ev.Command == "PRIVMSG"
633 isNotice := ev.Command == "NOTICE"
634
635 if !ev.TargetIsChannel && isNotice {
636- buffer = app.win.CurrentBuffer()
637+ curNetID, curBuffer := app.win.CurrentBuffer()
638+ if curNetID == s.NetID() {
639+ buffer = curBuffer
640+ } else {
641+ isHighlight = true
642+ }
643 } else if !ev.TargetIsChannel {
644- buffer = Home
645+ buffer = ""
646 } else {
647 buffer = ev.Target
648 }
649@@ -942,40 +977,45 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
650
651 // updatePrompt changes the prompt text according to the application context.
652 func (app *App) updatePrompt() {
653- buffer := app.win.CurrentBuffer()
654+ netID, buffer := app.win.CurrentBuffer()
655+ s := app.sessions[netID]
656 command := isCommand(app.win.InputContent())
657 var prompt ui.StyledString
658- if buffer == Home || command {
659+ if buffer == "" || command {
660 prompt = ui.Styled(">",
661 tcell.
662 StyleDefault.
663 Foreground(tcell.Color(app.cfg.Colors.Prompt)),
664 )
665- } else if app.s == nil {
666+ } else if s == nil {
667 prompt = ui.Styled("<offline>",
668 tcell.
669 StyleDefault.
670 Foreground(tcell.ColorRed),
671 )
672 } else {
673- prompt = identString(app.s.Nick())
674+ prompt = identString(s.Nick())
675 }
676 app.win.SetPrompt(prompt)
677 }
678
679-func (app *App) printTopic(buffer string) {
680+func (app *App) printTopic(netID, buffer string) (ok bool) {
681 var body string
682-
683- topic, who, at := app.s.Topic(buffer)
684+ s := app.sessions[netID]
685+ if s == nil {
686+ return false
687+ }
688+ topic, who, at := s.Topic(buffer)
689 if who == nil {
690 body = fmt.Sprintf("Topic: %s", topic)
691 } else {
692 body = fmt.Sprintf("Topic (by %s, %s): %s", who, at.Local().Format("Mon Jan 2 15:04:05"), topic)
693 }
694- app.win.AddLine(buffer, ui.NotifyNone, ui.Line{
695+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
696 At: time.Now(),
697 Head: "--",
698 HeadColor: tcell.ColorGray,
699 Body: ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
700 })
701+ return true
702 }
+14,
-7
1@@ -43,19 +43,21 @@ func main() {
2
3 cfg.Debug = cfg.Debug || debug
4
5- lastBuffer := getLastBuffer()
6-
7- app, err := senpai.NewApp(cfg, lastBuffer)
8+ app, err := senpai.NewApp(cfg)
9 if err != nil {
10 panic(err)
11 }
12
13+ lastNetID, lastBuffer := getLastBuffer()
14+ app.SwitchToBuffer(lastNetID, lastBuffer)
15+
16 app.Run()
17 app.Close()
18
19 // Write last buffer on close
20 lastBufferPath := getLastBufferPath()
21- err = os.WriteFile(lastBufferPath, []byte(app.CurrentBuffer()), 0666)
22+ lastNetID, lastBuffer = app.CurrentBuffer()
23+ err = os.WriteFile(lastBufferPath, []byte(fmt.Sprintf("%s %s", lastNetID, lastBuffer)), 0666)
24 if err != nil {
25 fmt.Fprintf(os.Stderr, "failed to write last buffer at %q: %s\n", lastBufferPath, err)
26 }
27@@ -76,11 +78,16 @@ func getLastBufferPath() string {
28 return lastBufferPath
29 }
30
31-func getLastBuffer() string {
32+func getLastBuffer() (netID, buffer string) {
33 buf, err := ioutil.ReadFile(getLastBufferPath())
34 if err != nil {
35- return ""
36+ return "", ""
37+ }
38+
39+ fields := strings.SplitN(string(buf), " ", 2)
40+ if len(fields) < 2 {
41+ return "", ""
42 }
43
44- return strings.TrimSpace(string(buf))
45+ return fields[0], fields[1]
46 }
+108,
-96
1@@ -136,28 +136,27 @@ func init() {
2 }
3 }
4
5-func noCommand(app *App, buffer, content string) error {
6- // You can't send messages to home buffer, and it might get
7- // delivered to a user "home" without a bouncer, which will be bad.
8- if buffer == Home {
9- return fmt.Errorf("can't send message to home")
10+func noCommand(app *App, content string) error {
11+ netID, buffer := app.win.CurrentBuffer()
12+ if buffer == "" {
13+ return fmt.Errorf("can't send message to this buffer")
14 }
15-
16- if app.s == nil {
17+ s := app.sessions[netID]
18+ if s == nil {
19 return errOffline
20 }
21
22- app.s.PrivMsg(buffer, content)
23- if !app.s.HasCapability("echo-message") {
24- buffer, line, _ := app.formatMessage(irc.MessageEvent{
25- User: app.s.Nick(),
26+ s.PrivMsg(buffer, content)
27+ if !s.HasCapability("echo-message") {
28+ buffer, line, _ := app.formatMessage(s, irc.MessageEvent{
29+ User: s.Nick(),
30 Target: buffer,
31 TargetIsChannel: true,
32 Command: "PRIVMSG",
33 Content: content,
34 Time: time.Now(),
35 })
36- app.win.AddLine(buffer, ui.NotifyNone, line)
37+ app.win.AddLine(netID, buffer, ui.NotifyNone, line)
38 }
39
40 return nil
41@@ -180,8 +179,9 @@ func commandDoBuffer(app *App, args []string) error {
42
43 func commandDoHelp(app *App, args []string) (err error) {
44 t := time.Now()
45+ netID, buffer := app.win.CurrentBuffer()
46 if len(args) == 0 {
47- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
48+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
49 At: t,
50 Head: "--",
51 Body: ui.PlainString("Available commands:"),
52@@ -190,22 +190,22 @@ func commandDoHelp(app *App, args []string) (err error) {
53 if cmd.Desc == "" {
54 continue
55 }
56- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
57+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
58 At: t,
59 Body: ui.PlainSprintf(" \x02%s\x02 %s", cmdName, cmd.Usage),
60 })
61- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
62+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
63 At: t,
64 Body: ui.PlainSprintf(" %s", cmd.Desc),
65 })
66- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
67+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
68 At: t,
69 })
70 }
71 } else {
72 search := strings.ToUpper(args[0])
73 found := false
74- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
75+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
76 At: t,
77 Head: "--",
78 Body: ui.PlainSprintf("Commands that match \"%s\":", search),
79@@ -221,21 +221,21 @@ func commandDoHelp(app *App, args []string) (err error) {
80 usage.SetStyle(tcell.StyleDefault)
81 usage.WriteByte(' ')
82 usage.WriteString(cmd.Usage)
83- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
84+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
85 At: t,
86 Body: usage.StyledString(),
87 })
88- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
89+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
90 At: t,
91 Body: ui.PlainSprintf(" %s", cmd.Desc),
92 })
93- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
94+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
95 At: t,
96 })
97 found = true
98 }
99 if !found {
100- app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{
101+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
102 At: t,
103 Body: ui.PlainSprintf(" no command matches %q", args[0]),
104 })
105@@ -245,75 +245,81 @@ func commandDoHelp(app *App, args []string) (err error) {
106 }
107
108 func commandDoJoin(app *App, args []string) (err error) {
109- if app.s == nil {
110+ s := app.CurrentSession()
111+ if s == nil {
112 return errOffline
113 }
114-
115+ channel := args[0]
116 key := ""
117 if len(args) == 2 {
118 key = args[1]
119 }
120- app.s.Join(args[0], key)
121+ s.Join(channel, key)
122 return nil
123 }
124
125 func commandDoMe(app *App, args []string) (err error) {
126- if app.s == nil {
127- return errOffline
128- }
129-
130- buffer := app.win.CurrentBuffer()
131- if buffer == Home {
132+ netID, buffer := app.win.CurrentBuffer()
133+ if buffer == "" {
134+ netID = app.lastQueryNet
135 buffer = app.lastQuery
136 }
137+ s := app.sessions[netID]
138+ if s == nil {
139+ return errOffline
140+ }
141 content := fmt.Sprintf("\x01ACTION %s\x01", args[0])
142- app.s.PrivMsg(buffer, content)
143- if !app.s.HasCapability("echo-message") {
144- buffer, line, _ := app.formatMessage(irc.MessageEvent{
145- User: app.s.Nick(),
146+ s.PrivMsg(buffer, content)
147+ if !s.HasCapability("echo-message") {
148+ buffer, line, _ := app.formatMessage(s, irc.MessageEvent{
149+ User: s.Nick(),
150 Target: buffer,
151 TargetIsChannel: true,
152 Command: "PRIVMSG",
153 Content: content,
154 Time: time.Now(),
155 })
156- app.win.AddLine(buffer, ui.NotifyNone, line)
157+ app.win.AddLine(netID, buffer, ui.NotifyNone, line)
158 }
159 return nil
160 }
161
162 func commandDoMsg(app *App, args []string) (err error) {
163- if app.s == nil {
164- return errOffline
165- }
166-
167 target := args[0]
168 content := args[1]
169- app.s.PrivMsg(target, content)
170- if !app.s.HasCapability("echo-message") {
171- buffer, line, _ := app.formatMessage(irc.MessageEvent{
172- User: app.s.Nick(),
173+ netID, _ := app.win.CurrentBuffer()
174+ s := app.sessions[netID]
175+ if s == nil {
176+ return errOffline
177+ }
178+ s.PrivMsg(target, content)
179+ if !s.HasCapability("echo-message") {
180+ buffer, line, _ := app.formatMessage(s, irc.MessageEvent{
181+ User: s.Nick(),
182 Target: target,
183 TargetIsChannel: true,
184 Command: "PRIVMSG",
185 Content: content,
186 Time: time.Now(),
187 })
188- app.win.AddLine(buffer, ui.NotifyNone, line)
189+ app.win.AddLine(netID, buffer, ui.NotifyNone, line)
190 }
191 return nil
192 }
193
194 func commandDoNames(app *App, args []string) (err error) {
195- if app.s == nil {
196+ netID, buffer := app.win.CurrentBuffer()
197+ s := app.sessions[netID]
198+ if s == nil {
199 return errOffline
200 }
201-
202- buffer := app.win.CurrentBuffer()
203+ if !s.IsChannel(buffer) {
204+ return fmt.Errorf("this is not a channel")
205+ }
206 var sb ui.StyledStringBuilder
207 sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGrey))
208 sb.WriteString("Names: ")
209- for _, name := range app.s.Names(buffer) {
210+ for _, name := range s.Names(buffer) {
211 if name.PowerLevel != "" {
212 sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
213 sb.WriteString(name.PowerLevel)
214@@ -324,7 +330,7 @@ func commandDoNames(app *App, args []string) (err error) {
215 }
216 body := sb.StyledString()
217 // TODO remove last space
218- app.win.AddLine(buffer, ui.NotifyNone, ui.Line{
219+ app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{
220 At: time.Now(),
221 Head: "--",
222 HeadColor: tcell.ColorGray,
223@@ -334,40 +340,40 @@ func commandDoNames(app *App, args []string) (err error) {
224 }
225
226 func commandDoNick(app *App, args []string) (err error) {
227- if app.s == nil {
228- return errOffline
229- }
230-
231 nick := args[0]
232- if i := strings.IndexAny(nick, " :@!*?"); i >= 0 {
233+ if i := strings.IndexAny(nick, " :"); i >= 0 {
234 return fmt.Errorf("illegal char %q in nickname", nick[i])
235 }
236- app.s.ChangeNick(nick)
237- return nil
238-}
239-
240-func commandDoMode(app *App, args []string) (err error) {
241- if app.s == nil {
242+ s := app.CurrentSession()
243+ if s == nil {
244 return errOffline
245 }
246+ s.ChangeNick(nick)
247+ return
248+}
249
250+func commandDoMode(app *App, args []string) (err error) {
251 channel := args[0]
252 flags := args[1]
253 modeArgs := args[2:]
254
255- app.s.ChangeMode(channel, flags, modeArgs)
256+ s := app.CurrentSession()
257+ if s == nil {
258+ return errOffline
259+ }
260+ s.ChangeMode(channel, flags, modeArgs)
261 return nil
262 }
263
264 func commandDoPart(app *App, args []string) (err error) {
265- if app.s == nil {
266+ netID, channel := app.win.CurrentBuffer()
267+ s := app.sessions[netID]
268+ if s == nil {
269 return errOffline
270 }
271-
272- channel := app.win.CurrentBuffer()
273 reason := ""
274 if 0 < len(args) {
275- if app.s.IsChannel(args[0]) {
276+ if s.IsChannel(args[0]) {
277 channel = args[0]
278 if 1 < len(args) {
279 reason = args[1]
280@@ -376,10 +382,11 @@ func commandDoPart(app *App, args []string) (err error) {
281 reason = args[0]
282 }
283 }
284- if channel == Home {
285- return fmt.Errorf("cannot part home")
286+
287+ if channel == "" {
288+ return fmt.Errorf("cannot part this buffer")
289 }
290- app.s.Part(channel, reason)
291+ s.Part(channel, reason)
292 return nil
293 }
294
295@@ -388,68 +395,73 @@ func commandDoQuit(app *App, args []string) (err error) {
296 if 0 < len(args) {
297 reason = args[0]
298 }
299- if app.s != nil {
300- app.s.Quit(reason)
301+ for _, session := range app.sessions {
302+ session.Quit(reason)
303 }
304 app.win.Exit()
305 return nil
306 }
307
308 func commandDoQuote(app *App, args []string) (err error) {
309- if app.s == nil {
310+ s := app.CurrentSession()
311+ if s == nil {
312 return errOffline
313 }
314-
315- app.s.SendRaw(args[0])
316+ s.SendRaw(args[0])
317 return nil
318 }
319
320 func commandDoR(app *App, args []string) (err error) {
321- if app.s == nil {
322+ s := app.sessions[app.lastQueryNet]
323+ if s == nil {
324 return errOffline
325 }
326-
327- app.s.PrivMsg(app.lastQuery, args[0])
328- if !app.s.HasCapability("echo-message") {
329- buffer, line, _ := app.formatMessage(irc.MessageEvent{
330- User: app.s.Nick(),
331+ s.PrivMsg(app.lastQuery, args[0])
332+ if !s.HasCapability("echo-message") {
333+ buffer, line, _ := app.formatMessage(s, irc.MessageEvent{
334+ User: s.Nick(),
335 Target: app.lastQuery,
336 TargetIsChannel: true,
337 Command: "PRIVMSG",
338 Content: args[0],
339 Time: time.Now(),
340 })
341- app.win.AddLine(buffer, ui.NotifyNone, line)
342+ app.win.AddLine(app.lastQueryNet, buffer, ui.NotifyNone, line)
343 }
344 return nil
345 }
346
347 func commandDoTopic(app *App, args []string) (err error) {
348- if app.s == nil {
349- return errOffline
350- }
351-
352+ netID, buffer := app.win.CurrentBuffer()
353+ var ok bool
354 if len(args) == 0 {
355- app.printTopic(app.win.CurrentBuffer())
356+ ok = app.printTopic(netID, buffer)
357 } else {
358- app.s.ChangeTopic(app.win.CurrentBuffer(), args[0])
359+ s := app.sessions[netID]
360+ if s != nil {
361+ s.ChangeTopic(buffer, args[0])
362+ ok = true
363+ }
364+ }
365+ if !ok {
366+ return errOffline
367 }
368 return nil
369 }
370
371 func commandDoInvite(app *App, args []string) (err error) {
372- if app.s == nil {
373+ nick := args[0]
374+ netID, channel := app.win.CurrentBuffer()
375+ s := app.sessions[netID]
376+ if s == nil {
377 return errOffline
378 }
379-
380- nick := args[0]
381- channel := app.win.CurrentBuffer()
382 if len(args) == 2 {
383 channel = args[1]
384- } else if channel == Home {
385- return fmt.Errorf("cannot invite to home")
386+ } else if channel == "" {
387+ return fmt.Errorf("either send this command from a channel, or specify the channel")
388 }
389- app.s.Invite(nick, channel)
390+ s.Invite(nick, channel)
391 return nil
392 }
393
394@@ -522,7 +534,7 @@ func (app *App) handleInput(buffer, content string) error {
395
396 cmdName, rawArgs, isCommand := parseCommand(content)
397 if !isCommand {
398- return noCommand(app, buffer, rawArgs)
399+ return noCommand(app, rawArgs)
400 }
401 if cmdName == "" {
402 return fmt.Errorf("lone slash at the beginning")
403@@ -554,8 +566,8 @@ func (app *App) handleInput(buffer, content string) error {
404 if len(args) < cmd.MinArgs {
405 return fmt.Errorf("usage: %s %s", cmdName, cmd.Usage)
406 }
407- if buffer == Home && !cmd.AllowHome {
408- return fmt.Errorf("command %q cannot be executed from home", cmdName)
409+ if buffer == "" && !cmd.AllowHome {
410+ return fmt.Errorf("command %q cannot be executed from a server buffer", cmdName)
411 }
412
413 return cmd.Handle(app, args)
+12,
-7
1@@ -18,9 +18,11 @@ func (app *App) completionsChannelMembers(cs []ui.Completion, cursorIdx int, tex
2 if len(word) == 0 {
3 return cs
4 }
5- wordCf := app.s.Casemap(string(word))
6- for _, name := range app.s.Names(app.win.CurrentBuffer()) {
7- if strings.HasPrefix(app.s.Casemap(name.Name.Name), wordCf) {
8+ netID, buffer := app.win.CurrentBuffer()
9+ s := app.sessions[netID] // is not nil
10+ wordCf := s.Casemap(string(word))
11+ for _, name := range s.Names(buffer) {
12+ if strings.HasPrefix(s.Casemap(name.Name.Name), wordCf) {
13 nickComp := []rune(name.Name.Name)
14 if start == 0 {
15 nickComp = append(nickComp, ':')
16@@ -45,7 +47,9 @@ func (app *App) completionsChannelTopic(cs []ui.Completion, cursorIdx int, text
17 if !hasPrefix(text, []rune("/topic ")) {
18 return cs
19 }
20- topic, _, _ := app.s.Topic(app.win.CurrentBuffer())
21+ netID, buffer := app.win.CurrentBuffer()
22+ s := app.sessions[netID] // is not nil
23+ topic, _, _ := s.Topic(buffer)
24 if cursorIdx == len(text) {
25 compText := append(text, []rune(topic)...)
26 cs = append(cs, ui.Completion{
27@@ -60,6 +64,7 @@ func (app *App) completionsMsg(cs []ui.Completion, cursorIdx int, text []rune) [
28 if !hasPrefix(text, []rune("/msg ")) {
29 return cs
30 }
31+ s := app.CurrentSession() // is not nil
32 // Check if the first word (target) is already written and complete (in
33 // which case we don't have completions to provide).
34 var word string
35@@ -69,15 +74,15 @@ func (app *App) completionsMsg(cs []ui.Completion, cursorIdx int, text []rune) [
36 return cs
37 }
38 if !hasMetALetter && text[i] != ' ' {
39- word = app.s.Casemap(string(text[i:cursorIdx]))
40+ word = s.Casemap(string(text[i:cursorIdx]))
41 hasMetALetter = true
42 }
43 }
44 if word == "" {
45 return cs
46 }
47- for _, user := range app.s.Users() {
48- if strings.HasPrefix(app.s.Casemap(user), word) {
49+ for _, user := range s.Users() {
50+ if strings.HasPrefix(s.Casemap(user), word) {
51 nickComp := append([]rune(user), ' ')
52 c := make([]rune, len(text)+5+len(nickComp)-cursorIdx)
53 copy(c[:5], []rune("/msg "))
+1,
-0
1@@ -23,6 +23,7 @@ extensions, such as:
2
3 - _CHATHISTORY_, senpai fetches history from the server instead of keeping logs,
4 - _@+typing_, senpai shows when others are typing a message,
5+- _BOUNCER_, senpai connects to all your networks at once automatically,
6 - and more to come!
7
8 # CONFIGURATION
+5,
-0
1@@ -75,3 +75,8 @@ type HistoryEvent struct {
2 Target string
3 Messages []Event
4 }
5+
6+type BouncerNetworkEvent struct {
7+ ID string
8+ Name string
9+}
+42,
-6
1@@ -57,7 +57,8 @@ var SupportedCapabilities = map[string]struct{}{
2 "sasl": {},
3 "setname": {},
4
5- "draft/chathistory": {},
6+ "draft/chathistory": {},
7+ "soju.im/bouncer-networks": {},
8 }
9
10 // Values taken by the "@+typing=" client tag. TypingUnspec means the value or
11@@ -91,8 +92,8 @@ type SessionParams struct {
12 Nickname string
13 Username string
14 RealName string
15-
16- Auth SASLClient
17+ NetID string
18+ Auth SASLClient
19 }
20
21 type Session struct {
22@@ -108,6 +109,7 @@ type Session struct {
23 real string
24 acct string
25 host string
26+ netID string
27 auth SASLClient
28
29 availableCaps map[string]string
30@@ -138,6 +140,7 @@ func NewSession(out chan<- Message, params SessionParams) *Session {
31 nickCf: CasemapASCII(params.Nickname),
32 user: params.Username,
33 real: params.RealName,
34+ netID: params.NetID,
35 auth: params.Auth,
36 availableCaps: map[string]string{},
37 enabledCaps: map[string]struct{}{},
38@@ -180,6 +183,10 @@ func (s *Session) Nick() string {
39 return s.nick
40 }
41
42+func (s *Session) NetID() string {
43+ return s.netID
44+}
45+
46 // NickCf is our casemapped nickname.
47 func (s *Session) NickCf() string {
48 return s.nickCf
49@@ -486,10 +493,10 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) {
50 return nil, err
51 }
52
53- s.out <- NewMessage("CAP", "END")
54+ s.endRegistration()
55 s.host = ParsePrefix(userhost).Host
56 case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
57- s.out <- NewMessage("CAP", "END")
58+ s.endRegistration()
59 case "CAP":
60 var subcommand string
61 if err := msg.ParseParams(nil, &subcommand); err != nil {
62@@ -525,7 +532,7 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) {
63
64 _, ok := s.availableCaps["sasl"]
65 if s.auth == nil || !ok {
66- s.out <- NewMessage("CAP", "END")
67+ s.endRegistration()
68 }
69 }
70 default:
71@@ -1024,6 +1031,19 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
72 FormerNick: msg.Prefix.Name,
73 }, nil
74 }
75+ case "BOUNCER":
76+ if len(msg.Params) < 3 {
77+ break
78+ }
79+ if msg.Params[0] != "NETWORK" || s.netID != "" {
80+ break
81+ }
82+ id := msg.Params[1]
83+ attrs := parseTags(msg.Params[2])
84+ return BouncerNetworkEvent{
85+ ID: id,
86+ Name: attrs["name"],
87+ }, nil
88 case "PING":
89 var payload string
90 if err := msg.ParseParams(&payload); err != nil {
91@@ -1137,6 +1157,8 @@ func (s *Session) updateFeatures(features []string) {
92
93 Switch:
94 switch key {
95+ case "BOUNCER_NETID":
96+ s.netID = value
97 case "CASEMAPPING":
98 switch value {
99 case "ascii":
100@@ -1175,3 +1197,17 @@ func (s *Session) updateFeatures(features []string) {
101 }
102 }
103 }
104+
105+func (s *Session) endRegistration() {
106+ if _, ok := s.enabledCaps["soju.im/bouncer-networks"]; !ok {
107+ s.out <- NewMessage("CAP", "END")
108+ return
109+ }
110+ if s.netID == "" {
111+ s.out <- NewMessage("CAP", "END")
112+ s.out <- NewMessage("BOUNCER", "LISTNETWORKS")
113+ } else {
114+ s.out <- NewMessage("BOUNCER", "BIND", s.netID)
115+ s.out <- NewMessage("CAP", "END")
116+ }
117+}
+1,
-2
1@@ -132,7 +132,6 @@ func escapeTagValue(unescaped string) string {
2 }
3
4 func parseTags(s string) (tags map[string]string) {
5- s = s[1:]
6 tags = map[string]string{}
7
8 for _, item := range strings.Split(s, ";") {
9@@ -239,7 +238,7 @@ func ParseMessage(line string) (msg Message, err error) {
10 var tags string
11
12 tags, line = word(line)
13- msg.Tags = parseTags(tags)
14+ msg.Tags = parseTags(tags[1:])
15 }
16
17 line = strings.TrimLeft(line, " ")
+61,
-18
1@@ -172,6 +172,8 @@ func (l *Line) NewLines(width int) []int {
2 }
3
4 type buffer struct {
5+ netID string
6+ netName string
7 title string
8 highlights int
9 unread bool
10@@ -239,15 +241,39 @@ func (bs *BufferList) Previous() {
11 bs.list[bs.current].unread = false
12 }
13
14-func (bs *BufferList) Add(title string) (i int, added bool) {
15+func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) {
16 lTitle := strings.ToLower(title)
17+ gotNetID := false
18 for i, b := range bs.list {
19- if strings.ToLower(b.title) == lTitle {
20- return i, false
21+ lbTitle := strings.ToLower(b.title)
22+ if b.netID == netID {
23+ gotNetID = true
24+ if lbTitle == lTitle {
25+ return i, false
26+ }
27+ } else if gotNetID || (b.netID == netID && lbTitle < lTitle) {
28+ b := buffer{
29+ netID: netID,
30+ netName: netName,
31+ title: title,
32+ }
33+ bs.list = append(bs.list[:i+1], bs.list[i:]...)
34+ bs.list[i] = b
35+ if i <= bs.current && bs.current < len(bs.list) {
36+ bs.current++
37+ }
38+ return i, true
39 }
40 }
41
42- bs.list = append(bs.list, buffer{title: title})
43+ if netName == "" {
44+ netName = netID
45+ }
46+ bs.list = append(bs.list, buffer{
47+ netID: netID,
48+ netName: netName,
49+ title: title,
50+ })
51 return len(bs.list) - 1, true
52 }
53
54@@ -266,8 +292,8 @@ func (bs *BufferList) Remove(title string) (ok bool) {
55 return
56 }
57
58-func (bs *BufferList) AddLine(title string, notify NotifyType, line Line) {
59- idx := bs.idx(title)
60+func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line) {
61+ idx := bs.idx(netID, title)
62 if idx < 0 {
63 return
64 }
65@@ -303,8 +329,8 @@ func (bs *BufferList) AddLine(title string, notify NotifyType, line Line) {
66 }
67 }
68
69-func (bs *BufferList) AddLines(title string, before, after []Line) {
70- idx := bs.idx(title)
71+func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
72+ idx := bs.idx(netID, title)
73 if idx < 0 {
74 return
75 }
76@@ -326,8 +352,9 @@ func (bs *BufferList) AddLines(title string, before, after []Line) {
77 }
78 }
79
80-func (bs *BufferList) Current() (title string) {
81- return bs.list[bs.current].title
82+func (bs *BufferList) Current() (netID, title string) {
83+ b := &bs.list[bs.current]
84+ return b.netID, b.title
85 }
86
87 func (bs *BufferList) ScrollUp(n int) {
88@@ -352,14 +379,10 @@ func (bs *BufferList) IsAtTop() bool {
89 return b.isAtTop
90 }
91
92-func (bs *BufferList) idx(title string) int {
93- if title == "" {
94- return bs.current
95- }
96-
97+func (bs *BufferList) idx(netID, title string) int {
98 lTitle := strings.ToLower(title)
99 for i, b := range bs.list {
100- if strings.ToLower(b.title) == lTitle {
101+ if b.netID == netID && strings.ToLower(b.title) == lTitle {
102 return i
103 }
104 }
105@@ -391,7 +414,18 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
106 x = x0 + indexPadding
107 }
108
109- title := truncate(b.title, width-(x-x0), "\u2026")
110+ var title string
111+ if b.title == "" {
112+ title = b.netName
113+ } else {
114+ if i == bs.clicked {
115+ screen.SetContent(x, y, ' ', nil, tcell.StyleDefault.Reverse(true))
116+ screen.SetContent(x+1, y, ' ', nil, tcell.StyleDefault.Reverse(true))
117+ }
118+ x += 2
119+ title = b.title
120+ }
121+ title = truncate(title, width-(x-x0), "\u2026")
122 printString(screen, &x, y, Styled(title, st))
123
124 if i == bs.clicked {
125@@ -427,8 +461,17 @@ func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, widt
126 if i == bs.clicked {
127 st = st.Reverse(true)
128 }
129- title := truncate(b.title, width-x, "\u2026")
130+
131+ var title string
132+ if b.title == "" {
133+ st = st.Dim(true)
134+ title = b.netName
135+ } else {
136+ title = b.title
137+ }
138+ title = truncate(title, width-x, "\u2026")
139 printString(screen, &x, y0, Styled(title, st))
140+
141 if 0 < b.highlights {
142 st = st.Foreground(tcell.ColorRed).Reverse(true)
143 screen.SetContent(x, y0, ' ', nil, st)
M
ui/ui.go
+20,
-7
1@@ -82,7 +82,7 @@ func (ui *UI) Close() {
2 ui.screen.Fini()
3 }
4
5-func (ui *UI) CurrentBuffer() string {
6+func (ui *UI) CurrentBuffer() (netID, title string) {
7 return ui.bs.Current()
8 }
9
10@@ -147,8 +147,8 @@ func (ui *UI) IsAtTop() bool {
11 return ui.bs.IsAtTop()
12 }
13
14-func (ui *UI) AddBuffer(title string) (i int, added bool) {
15- return ui.bs.Add(title)
16+func (ui *UI) AddBuffer(netID, netName, title string) (i int, added bool) {
17+ return ui.bs.Add(netID, netName, title)
18 }
19
20 func (ui *UI) RemoveBuffer(title string) {
21@@ -156,12 +156,12 @@ func (ui *UI) RemoveBuffer(title string) {
22 ui.memberOffset = 0
23 }
24
25-func (ui *UI) AddLine(buffer string, notify NotifyType, line Line) {
26- ui.bs.AddLine(buffer, notify, line)
27+func (ui *UI) AddLine(netID, buffer string, notify NotifyType, line Line) {
28+ ui.bs.AddLine(netID, buffer, notify, line)
29 }
30
31-func (ui *UI) AddLines(buffer string, before, after []Line) {
32- ui.bs.AddLines(buffer, before, after)
33+func (ui *UI) AddLines(netID, buffer string, before, after []Line) {
34+ ui.bs.AddLines(netID, buffer, before, after)
35 }
36
37 func (ui *UI) JumpBuffer(sub string) bool {
38@@ -188,6 +188,19 @@ func (ui *UI) JumpBufferIndex(i int) bool {
39 return false
40 }
41
42+func (ui *UI) JumpBufferNetwork(netID, sub string) bool {
43+ subLower := strings.ToLower(sub)
44+ for i, b := range ui.bs.list {
45+ if b.netID == netID && strings.Contains(strings.ToLower(b.title), subLower) {
46+ if ui.bs.To(i) {
47+ ui.memberOffset = 0
48+ }
49+ return true
50+ }
51+ }
52+ return false
53+}
54+
55 func (ui *UI) SetStatus(status string) {
56 ui.status = status
57 }
+22,
-14
1@@ -9,42 +9,50 @@ import (
2 "github.com/gdamore/tcell/v2"
3 )
4
5-var Home = "home"
6-
7 const welcomeMessage = "senpai dev build. See senpai(1) for a list of keybindings and commands. Private messages and status notices go here."
8
9 func (app *App) initWindow() {
10- app.win.AddBuffer(Home)
11- app.win.AddLine(Home, ui.NotifyNone, ui.Line{
12+ app.win.AddBuffer("", "(home)", "")
13+ app.win.AddLine("", "", ui.NotifyNone, ui.Line{
14 Head: "--",
15 Body: ui.PlainString(welcomeMessage),
16 At: time.Now(),
17 })
18 }
19
20-func (app *App) queueStatusLine(line ui.Line) {
21+type statusLine struct {
22+ netID string
23+ line ui.Line
24+}
25+
26+func (app *App) queueStatusLine(netID string, line ui.Line) {
27 if line.At.IsZero() {
28 line.At = time.Now()
29 }
30 app.events <- event{
31- src: uiEvent,
32- content: line,
33+ src: "*",
34+ content: statusLine{
35+ netID: netID,
36+ line: line,
37+ },
38 }
39 }
40
41-func (app *App) addStatusLine(line ui.Line) {
42- buffer := app.win.CurrentBuffer()
43- if buffer != Home {
44- app.win.AddLine(Home, ui.NotifyNone, line)
45+func (app *App) addStatusLine(netID string, line ui.Line) {
46+ currentNetID, buffer := app.win.CurrentBuffer()
47+ if currentNetID == netID && buffer != "" {
48+ app.win.AddLine(netID, buffer, ui.NotifyNone, line)
49 }
50- app.win.AddLine(buffer, ui.NotifyNone, line)
51+ app.win.AddLine(netID, "", ui.NotifyNone, line)
52 }
53
54 func (app *App) setStatus() {
55- if app.s == nil {
56+ netID, buffer := app.win.CurrentBuffer()
57+ s := app.sessions[netID]
58+ if s == nil {
59 return
60 }
61- ts := app.s.Typings(app.win.CurrentBuffer())
62+ ts := s.Typings(buffer)
63 status := ""
64 if 3 < len(ts) {
65 status = "several people are typing..."