diff options
author | Hubert Hirtz <hubert@hirtz.pm> | 2021-10-20 17:33:10 +0200 |
---|---|---|
committer | Hubert Hirtz <hubert@hirtz.pm> | 2021-10-24 12:57:24 +0200 |
commit | 9fb4378753ddec61a504a0dec403f40d6def7e90 (patch) | |
tree | 32955930be2d97614f3ec6290f67d0556769a16e | |
parent | Remove draft file (diff) |
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).
-rw-r--r-- | app.go | 276 | ||||
-rw-r--r-- | cmd/senpai/main.go | 21 | ||||
-rw-r--r-- | commands.go | 204 | ||||
-rw-r--r-- | completions.go | 19 | ||||
-rw-r--r-- | doc/senpai.1.scd | 1 | ||||
-rw-r--r-- | irc/events.go | 5 | ||||
-rw-r--r-- | irc/session.go | 48 | ||||
-rw-r--r-- | irc/tokens.go | 3 | ||||
-rw-r--r-- | ui/buffers.go | 79 | ||||
-rw-r--r-- | ui/ui.go | 27 | ||||
-rw-r--r-- | window.go | 36 |
11 files changed, 444 insertions, 275 deletions
@@ -17,13 +17,6 @@ import ( const eventChanSize = 64 -type source int - -const ( - uiEvent source = iota - ircEvent -) - func isCommand(input []rune) bool { // Command can't start with two slashes because that's an escape for // a literal slash in the message @@ -70,30 +63,32 @@ func (b *bound) Update(line *ui.Line) { } type event struct { - src source + src string // "*" if UI, netID otherwise content interface{} } type App struct { - win *ui.UI - s *irc.Session - pasting bool - events chan event + win *ui.UI + sessions map[string]*irc.Session + pasting bool + events chan event cfg Config highlights []string lastQuery string + lastQueryNet string messageBounds map[string]bound + lastNetID string lastBuffer string } -func NewApp(cfg Config, lastBuffer string) (app *App, err error) { +func NewApp(cfg Config) (app *App, err error) { app = &App{ - cfg: cfg, + sessions: map[string]*irc.Session{}, events: make(chan event, eventChanSize), + cfg: cfg, messageBounds: map[string]bound{}, - lastBuffer: lastBuffer, } if cfg.Highlights != nil { @@ -133,18 +128,28 @@ func NewApp(cfg Config, lastBuffer string) (app *App, err error) { func (app *App) Close() { app.win.Close() - if app.s != nil { - app.s.Close() + for _, session := range app.sessions { + session.Close() } } +func (app *App) SwitchToBuffer(netID, buffer string) { + app.lastNetID = netID + app.lastBuffer = buffer +} + func (app *App) Run() { go app.uiLoop() - go app.ircLoop() + go app.ircLoop("") app.eventLoop() } -func (app *App) CurrentBuffer() string { +func (app *App) CurrentSession() *irc.Session { + netID, _ := app.win.CurrentBuffer() + return app.sessions[netID] +} + +func (app *App) CurrentBuffer() (netID, buffer string) { return app.win.CurrentBuffer() } @@ -172,8 +177,10 @@ func (app *App) eventLoop() { app.updatePrompt() app.setBufferNumbers() var currentMembers []irc.Member - if app.s != nil { - currentMembers = app.s.Names(app.win.CurrentBuffer()) + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s != nil && buffer != "" { + currentMembers = s.Names(buffer) } app.win.Draw(currentMembers) } @@ -182,7 +189,7 @@ func (app *App) eventLoop() { // ircLoop maintains a connection to the IRC server by connecting and then // forwarding IRC events to app.events repeatedly. -func (app *App) ircLoop() { +func (app *App) ircLoop(netID string) { var auth irc.SASLClient if app.cfg.Password != nil { auth = &irc.SASLPlain{ @@ -194,45 +201,46 @@ func (app *App) ircLoop() { Nickname: app.cfg.Nick, Username: app.cfg.User, RealName: app.cfg.Real, + NetID: netID, Auth: auth, } for !app.win.ShouldExit() { - conn := app.connect() + conn := app.connect(netID) in, out := irc.ChanInOut(conn) if app.cfg.Debug { - out = app.debugOutputMessages(out) + out = app.debugOutputMessages(netID, out) } session := irc.NewSession(out, params) app.events <- event{ - src: ircEvent, + src: netID, content: session, } go func() { for stop := range session.TypingStops() { app.events <- event{ - src: ircEvent, + src: netID, content: stop, } } }() for msg := range in { if app.cfg.Debug { - app.queueStatusLine(ui.Line{ + app.queueStatusLine(netID, ui.Line{ At: time.Now(), Head: "IN --", Body: ui.PlainString(msg.String()), }) } app.events <- event{ - src: ircEvent, + src: netID, content: msg, } } app.events <- event{ - src: ircEvent, + src: netID, content: nil, } - app.queueStatusLine(ui.Line{ + app.queueStatusLine(netID, ui.Line{ Head: "!!", HeadColor: tcell.ColorRed, Body: ui.PlainString("Connection lost"), @@ -241,9 +249,9 @@ func (app *App) ircLoop() { } } -func (app *App) connect() net.Conn { +func (app *App) connect(netID string) net.Conn { for { - app.queueStatusLine(ui.Line{ + app.queueStatusLine(netID, ui.Line{ Head: "--", Body: ui.PlainSprintf("Connecting to %s...", app.cfg.Addr), }) @@ -251,7 +259,7 @@ func (app *App) connect() net.Conn { if err == nil { return conn } - app.queueStatusLine(ui.Line{ + app.queueStatusLine(netID, ui.Line{ Head: "!!", HeadColor: tcell.ColorRed, Body: ui.PlainSprintf("Connection failed: %v", err), @@ -295,11 +303,11 @@ func (app *App) tryConnect() (conn net.Conn, err error) { return } -func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message { +func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<- irc.Message { debugOut := make(chan irc.Message, cap(out)) go func() { for msg := range debugOut { - app.queueStatusLine(ui.Line{ + app.queueStatusLine(netID, ui.Line{ At: time.Now(), Head: "OUT --", Body: ui.PlainString(msg.String()), @@ -316,7 +324,7 @@ func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message { func (app *App) uiLoop() { for ev := range app.win.Events { app.events <- event{ - src: uiEvent, + src: "*", content: ev, } } @@ -325,13 +333,10 @@ func (app *App) uiLoop() { // handleEvents handles a batch of events. func (app *App) handleEvents(evs []event) { for _, ev := range evs { - switch ev.src { - case uiEvent: + if ev.src == "*" { app.handleUIEvent(ev.content) - case ircEvent: - app.handleIRCEvent(ev.content) - default: - panic("unreachable") + } else { + app.handleIRCEvent(ev.src, ev.content) } } } @@ -346,10 +351,10 @@ func (app *App) handleUIEvent(ev interface{}) { app.handleMouseEvent(ev) case *tcell.EventKey: app.handleKeyEvent(ev) - case ui.Line: - app.addStatusLine(ev) + case statusLine: + app.addStatusLine(ev.netID, ev.line) default: - return + panic("unreachable") } } @@ -472,11 +477,11 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) { app.typing() } case tcell.KeyCR, tcell.KeyLF: - buffer := app.win.CurrentBuffer() + netID, buffer := app.win.CurrentBuffer() input := app.win.InputEnter() err := app.handleInput(buffer, input) if err != nil { - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyUnread, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyUnread, ui.Line{ At: time.Now(), Head: "!!", HeadColor: tcell.ColorRed, @@ -494,29 +499,35 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) { // requestHistory is a wrapper around irc.Session.RequestHistory to only request // history when needed. func (app *App) requestHistory() { - if app.s == nil { + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { return } - buffer := app.win.CurrentBuffer() - if app.win.IsAtTop() && buffer != Home { + if app.win.IsAtTop() && buffer != "" { t := time.Now() if bound, ok := app.messageBounds[buffer]; ok { t = bound.first } - app.s.NewHistoryRequest(buffer). + s.NewHistoryRequest(buffer). WithLimit(100). Before(t) } } -func (app *App) handleIRCEvent(ev interface{}) { +func (app *App) handleIRCEvent(netID string, ev interface{}) { if ev == nil { - app.s.Close() - app.s = nil + if s, ok := app.sessions[netID]; ok { + s.Close() + delete(app.sessions, netID) + } return } if s, ok := ev.(*irc.Session); ok { - app.s = s + if s, ok := app.sessions[netID]; ok { + s.Close() + } + app.sessions[netID] = s return } if _, ok := ev.(irc.Typing); ok { @@ -524,12 +535,19 @@ func (app *App) handleIRCEvent(ev interface{}) { return } - msg := ev.(irc.Message) + msg, ok := ev.(irc.Message) + if !ok { + panic("unreachable") + } + s, ok := app.sessions[netID] + if !ok { + panic("unreachable") + } // Mutate IRC state - ev, err := app.s.HandleMessage(msg) + ev, err := s.HandleMessage(msg) if err != nil { - app.win.AddLine(Home, ui.NotifyUnread, ui.Line{ + app.win.AddLine(netID, "", ui.NotifyUnread, ui.Line{ Head: "!!", HeadColor: tcell.ColorRed, Body: ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err), @@ -543,26 +561,26 @@ func (app *App) handleIRCEvent(ev interface{}) { for _, channel := range app.cfg.Channels { // TODO: group JOIN messages // TODO: support autojoining channels with keys - app.s.Join(channel, "") + s.Join(channel, "") } body := "Connected to the server" - if app.s.Nick() != app.cfg.Nick { - body = fmt.Sprintf("Connected to the server as %s", app.s.Nick()) + if s.Nick() != app.cfg.Nick { + body = fmt.Sprintf("Connected to the server as %s", s.Nick()) } - app.win.AddLine(Home, ui.NotifyUnread, ui.Line{ + app.win.AddLine(netID, "", ui.NotifyNone, ui.Line{ At: msg.TimeOrNow(), Head: "--", Body: ui.PlainString(body), }) case irc.SelfNickEvent: var body ui.StyledStringBuilder - body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, app.s.Nick())) + body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, s.Nick())) textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray) arrowStyle := tcell.StyleDefault body.AddStyle(0, textStyle) body.AddStyle(len(ev.FormerNick), arrowStyle) - body.AddStyle(body.Len()-len(app.s.Nick()), textStyle) - app.addStatusLine(ui.Line{ + body.AddStyle(body.Len()-len(s.Nick()), textStyle) + app.addStatusLine(netID, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -577,8 +595,8 @@ func (app *App) handleIRCEvent(ev interface{}) { body.AddStyle(0, textStyle) body.AddStyle(len(ev.FormerNick), arrowStyle) body.AddStyle(body.Len()-len(ev.User), textStyle) - for _, c := range app.s.ChannelsSharedWith(ev.User) { - app.win.AddLine(c, ui.NotifyNone, ui.Line{ + for _, c := range s.ChannelsSharedWith(ev.User) { + app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -587,14 +605,14 @@ func (app *App) handleIRCEvent(ev interface{}) { }) } case irc.SelfJoinEvent: - i, added := app.win.AddBuffer(ev.Channel) + i, added := app.win.AddBuffer(netID, "", ev.Channel) bounds, ok := app.messageBounds[ev.Channel] if added || !ok { - app.s.NewHistoryRequest(ev.Channel). + s.NewHistoryRequest(ev.Channel). WithLimit(200). Before(msg.TimeOrNow()) } else { - app.s.NewHistoryRequest(ev.Channel). + s.NewHistoryRequest(ev.Channel). WithLimit(200). After(bounds.last) } @@ -602,13 +620,14 @@ func (app *App) handleIRCEvent(ev interface{}) { app.win.JumpBufferIndex(i) } if ev.Topic != "" { - app.printTopic(ev.Channel) + app.printTopic(netID, ev.Channel) } // Restore last buffer - lastBuffer := app.lastBuffer - if ev.Channel == lastBuffer { - app.win.JumpBuffer(lastBuffer) + if netID == app.lastNetID && ev.Channel == app.lastBuffer { + app.win.JumpBufferNetwork(app.lastNetID, app.lastBuffer) + app.lastNetID = "" + app.lastBuffer = "" } case irc.UserJoinEvent: var body ui.StyledStringBuilder @@ -617,7 +636,7 @@ func (app *App) handleIRCEvent(ev interface{}) { body.WriteByte('+') body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray)) body.WriteString(ev.User) - app.win.AddLine(ev.Channel, ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -634,7 +653,7 @@ func (app *App) handleIRCEvent(ev interface{}) { body.WriteByte('-') body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray)) body.WriteString(ev.User) - app.win.AddLine(ev.Channel, ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -649,7 +668,7 @@ func (app *App) handleIRCEvent(ev interface{}) { body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray)) body.WriteString(ev.User) for _, c := range ev.Channels { - app.win.AddLine(c, ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -659,7 +678,7 @@ func (app *App) handleIRCEvent(ev interface{}) { } case irc.TopicChangeEvent: body := fmt.Sprintf("Topic changed to: %s", ev.Topic) - app.win.AddLine(ev.Channel, ui.NotifyUnread, ui.Line{ + app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -667,7 +686,7 @@ func (app *App) handleIRCEvent(ev interface{}) { }) case irc.ModeChangeEvent: body := fmt.Sprintf("Mode change: %s", ev.Mode) - app.win.AddLine(ev.Channel, ui.NotifyUnread, ui.Line{ + app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -677,11 +696,11 @@ func (app *App) handleIRCEvent(ev interface{}) { var buffer string var notify ui.NotifyType var body string - if app.s.IsMe(ev.Invitee) { - buffer = Home + if s.IsMe(ev.Invitee) { + buffer = "" notify = ui.NotifyHighlight body = fmt.Sprintf("%s invited you to join %s", ev.Inviter, ev.Channel) - } else if app.s.IsMe(ev.Inviter) { + } else if s.IsMe(ev.Inviter) { buffer = ev.Channel notify = ui.NotifyNone body = fmt.Sprintf("You invited %s to join this channel", ev.Invitee) @@ -690,7 +709,7 @@ func (app *App) handleIRCEvent(ev interface{}) { notify = ui.NotifyUnread body = fmt.Sprintf("%s invited %s to join this channel", ev.Inviter, ev.Invitee) } - app.win.AddLine(buffer, notify, ui.Line{ + app.win.AddLine(netID, buffer, notify, ui.Line{ At: msg.TimeOrNow(), Head: "--", HeadColor: tcell.ColorGray, @@ -698,19 +717,20 @@ func (app *App) handleIRCEvent(ev interface{}) { Highlight: notify == ui.NotifyHighlight, }) case irc.MessageEvent: - buffer, line, hlNotification := app.formatMessage(ev) + buffer, line, hlNotification := app.formatMessage(s, ev) var notify ui.NotifyType if hlNotification { notify = ui.NotifyHighlight } else { notify = ui.NotifyUnread } - app.win.AddLine(buffer, notify, line) + app.win.AddLine(netID, buffer, notify, line) if hlNotification { app.notifyHighlight(buffer, ev.User, line.Body.String()) } - if !app.s.IsChannel(msg.Params[0]) && !app.s.IsMe(ev.User) { + if !s.IsChannel(msg.Params[0]) && !s.IsMe(ev.User) { app.lastQuery = msg.Prefix.Name + app.lastQueryNet = netID } bounds := app.messageBounds[ev.Target] bounds.Update(&line) @@ -722,7 +742,7 @@ func (app *App) handleIRCEvent(ev interface{}) { for _, m := range ev.Messages { switch ev := m.(type) { case irc.MessageEvent: - _, line, _ := app.formatMessage(ev) + _, line, _ := app.formatMessage(s, ev) if hasBounds { c := bounds.Compare(&line) if c < 0 { @@ -735,7 +755,7 @@ func (app *App) handleIRCEvent(ev interface{}) { } } } - app.win.AddLines(ev.Target, linesBefore, linesAfter) + app.win.AddLines(netID, ev.Target, linesBefore, linesAfter) if len(linesBefore) != 0 { bounds.Update(&linesBefore[0]) bounds.Update(&linesBefore[len(linesBefore)-1]) @@ -745,6 +765,11 @@ func (app *App) handleIRCEvent(ev interface{}) { bounds.Update(&linesAfter[len(linesAfter)-1]) } app.messageBounds[ev.Target] = bounds + case irc.BouncerNetworkEvent: + _, added := app.win.AddBuffer(ev.ID, ev.Name, "") + if added { + go app.ircLoop(ev.ID) + } case irc.ErrorEvent: if isBlackListed(msg.Command) { break @@ -764,7 +789,7 @@ func (app *App) handleIRCEvent(ev interface{}) { default: panic("unreachable") } - app.addStatusLine(ui.Line{ + app.addStatusLine(netID, ui.Line{ At: msg.TimeOrNow(), Head: head, Body: ui.PlainString(body), @@ -782,13 +807,13 @@ func isBlackListed(command string) bool { } // isHighlight reports whether the given message content is a highlight. -func (app *App) isHighlight(content string) bool { - contentCf := app.s.Casemap(content) +func (app *App) isHighlight(s *irc.Session, content string) bool { + contentCf := s.Casemap(content) if app.highlights == nil { - return strings.Contains(contentCf, app.s.NickCf()) + return strings.Contains(contentCf, s.NickCf()) } for _, h := range app.highlights { - if strings.Contains(contentCf, app.s.Casemap(h)) { + if strings.Contains(contentCf, s.Casemap(h)) { return true } } @@ -805,8 +830,9 @@ func (app *App) notifyHighlight(buffer, nick, content string) { if err != nil { return } + netID, curBuffer := app.win.CurrentBuffer() here := "0" - if buffer == app.win.CurrentBuffer() { + if buffer == curBuffer { // TODO also check netID here = "1" } cmd := exec.Command(sh, "-c", app.cfg.OnHighlight) @@ -819,7 +845,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) { output, err := cmd.CombinedOutput() if err != nil { body := fmt.Sprintf("Failed to invoke on-highlight command: %v. Output: %q", err, string(output)) - app.addStatusLine(ui.Line{ + app.addStatusLine(netID, ui.Line{ At: time.Now(), Head: "!!", HeadColor: tcell.ColorRed, @@ -831,32 +857,36 @@ func (app *App) notifyHighlight(buffer, nick, content string) { // typing sends typing notifications to the IRC server according to the user // input. func (app *App) typing() { - if app.s == nil || app.cfg.NoTypings { + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil || app.cfg.NoTypings { return } - buffer := app.win.CurrentBuffer() - if buffer == Home { + if buffer == "" { return } input := app.win.InputContent() if len(input) == 0 { - app.s.TypingStop(buffer) + s.TypingStop(buffer) } else if !isCommand(input) { - app.s.Typing(app.win.CurrentBuffer()) + s.Typing(buffer) } } // completions computes the list of completions given the input text and the // cursor position. func (app *App) completions(cursorIdx int, text []rune) []ui.Completion { - var cs []ui.Completion - if len(text) == 0 { - return cs + return nil + } + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { + return nil } - buffer := app.win.CurrentBuffer() - if app.s.IsChannel(buffer) { + var cs []ui.Completion + if buffer != "" { cs = app.completionsChannelTopic(cs, cursorIdx, text) cs = app.completionsChannelMembers(cs, cursorIdx, text) } @@ -878,17 +908,22 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion { // - which buffer the message must be added to, // - the UI line, // - whether senpai must trigger the "on-highlight" command. -func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line, hlNotification bool) { - isFromSelf := app.s.IsMe(ev.User) - isHighlight := app.isHighlight(ev.Content) +func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer string, line ui.Line, hlNotification bool) { + isFromSelf := s.IsMe(ev.User) + isHighlight := app.isHighlight(s, ev.Content) isAction := strings.HasPrefix(ev.Content, "\x01ACTION") isQuery := !ev.TargetIsChannel && ev.Command == "PRIVMSG" isNotice := ev.Command == "NOTICE" if !ev.TargetIsChannel && isNotice { - buffer = app.win.CurrentBuffer() + curNetID, curBuffer := app.win.CurrentBuffer() + if curNetID == s.NetID() { + buffer = curBuffer + } else { + isHighlight = true + } } else if !ev.TargetIsChannel { - buffer = Home + buffer = "" } else { buffer = ev.Target } @@ -942,40 +977,45 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line, // updatePrompt changes the prompt text according to the application context. func (app *App) updatePrompt() { - buffer := app.win.CurrentBuffer() + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] command := isCommand(app.win.InputContent()) var prompt ui.StyledString - if buffer == Home || command { + if buffer == "" || command { prompt = ui.Styled(">", tcell. StyleDefault. Foreground(tcell.Color(app.cfg.Colors.Prompt)), ) - } else if app.s == nil { + } else if s == nil { prompt = ui.Styled("<offline>", tcell. StyleDefault. Foreground(tcell.ColorRed), ) } else { - prompt = identString(app.s.Nick()) + prompt = identString(s.Nick()) } app.win.SetPrompt(prompt) } -func (app *App) printTopic(buffer string) { +func (app *App) printTopic(netID, buffer string) (ok bool) { var body string - - topic, who, at := app.s.Topic(buffer) + s := app.sessions[netID] + if s == nil { + return false + } + topic, who, at := s.Topic(buffer) if who == nil { body = fmt.Sprintf("Topic: %s", topic) } else { body = fmt.Sprintf("Topic (by %s, %s): %s", who, at.Local().Format("Mon Jan 2 15:04:05"), topic) } - app.win.AddLine(buffer, ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: time.Now(), Head: "--", HeadColor: tcell.ColorGray, Body: ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)), }) + return true } diff --git a/cmd/senpai/main.go b/cmd/senpai/main.go index e41d8db..dc2493e 100644 --- a/cmd/senpai/main.go +++ b/cmd/senpai/main.go @@ -43,19 +43,21 @@ func main() { cfg.Debug = cfg.Debug || debug - lastBuffer := getLastBuffer() - - app, err := senpai.NewApp(cfg, lastBuffer) + app, err := senpai.NewApp(cfg) if err != nil { panic(err) } + lastNetID, lastBuffer := getLastBuffer() + app.SwitchToBuffer(lastNetID, lastBuffer) + app.Run() app.Close() // Write last buffer on close lastBufferPath := getLastBufferPath() - err = os.WriteFile(lastBufferPath, []byte(app.CurrentBuffer()), 0666) + lastNetID, lastBuffer = app.CurrentBuffer() + err = os.WriteFile(lastBufferPath, []byte(fmt.Sprintf("%s %s", lastNetID, lastBuffer)), 0666) if err != nil { fmt.Fprintf(os.Stderr, "failed to write last buffer at %q: %s\n", lastBufferPath, err) } @@ -76,11 +78,16 @@ func getLastBufferPath() string { return lastBufferPath } -func getLastBuffer() string { +func getLastBuffer() (netID, buffer string) { buf, err := ioutil.ReadFile(getLastBufferPath()) if err != nil { - return "" + return "", "" + } + + fields := strings.SplitN(string(buf), " ", 2) + if len(fields) < 2 { + return "", "" } - return strings.TrimSpace(string(buf)) + return fields[0], fields[1] } diff --git a/commands.go b/commands.go index 9734436..fcf04f1 100644 --- a/commands.go +++ b/commands.go @@ -136,28 +136,27 @@ func init() { } } -func noCommand(app *App, buffer, content string) error { - // You can't send messages to home buffer, and it might get - // delivered to a user "home" without a bouncer, which will be bad. - if buffer == Home { - return fmt.Errorf("can't send message to home") +func noCommand(app *App, content string) error { + netID, buffer := app.win.CurrentBuffer() + if buffer == "" { + return fmt.Errorf("can't send message to this buffer") } - - if app.s == nil { + s := app.sessions[netID] + if s == nil { return errOffline } - app.s.PrivMsg(buffer, content) - if !app.s.HasCapability("echo-message") { - buffer, line, _ := app.formatMessage(irc.MessageEvent{ - User: app.s.Nick(), + s.PrivMsg(buffer, content) + if !s.HasCapability("echo-message") { + buffer, line, _ := app.formatMessage(s, irc.MessageEvent{ + User: s.Nick(), Target: buffer, TargetIsChannel: true, Command: "PRIVMSG", Content: content, Time: time.Now(), }) - app.win.AddLine(buffer, ui.NotifyNone, line) + app.win.AddLine(netID, buffer, ui.NotifyNone, line) } return nil @@ -180,8 +179,9 @@ func commandDoBuffer(app *App, args []string) error { func commandDoHelp(app *App, args []string) (err error) { t := time.Now() + netID, buffer := app.win.CurrentBuffer() if len(args) == 0 { - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Head: "--", Body: ui.PlainString("Available commands:"), @@ -190,22 +190,22 @@ func commandDoHelp(app *App, args []string) (err error) { if cmd.Desc == "" { continue } - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Body: ui.PlainSprintf(" \x02%s\x02 %s", cmdName, cmd.Usage), }) - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Body: ui.PlainSprintf(" %s", cmd.Desc), }) - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, }) } } else { search := strings.ToUpper(args[0]) found := false - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Head: "--", Body: ui.PlainSprintf("Commands that match \"%s\":", search), @@ -221,21 +221,21 @@ func commandDoHelp(app *App, args []string) (err error) { usage.SetStyle(tcell.StyleDefault) usage.WriteByte(' ') usage.WriteString(cmd.Usage) - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Body: usage.StyledString(), }) - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Body: ui.PlainSprintf(" %s", cmd.Desc), }) - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, }) found = true } if !found { - app.win.AddLine(app.win.CurrentBuffer(), ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: t, Body: ui.PlainSprintf(" no command matches %q", args[0]), }) @@ -245,75 +245,81 @@ func commandDoHelp(app *App, args []string) (err error) { } func commandDoJoin(app *App, args []string) (err error) { - if app.s == nil { + s := app.CurrentSession() + if s == nil { return errOffline } - + channel := args[0] key := "" if len(args) == 2 { key = args[1] } - app.s.Join(args[0], key) + s.Join(channel, key) return nil } func commandDoMe(app *App, args []string) (err error) { - if app.s == nil { - return errOffline - } - - buffer := app.win.CurrentBuffer() - if buffer == Home { + netID, buffer := app.win.CurrentBuffer() + if buffer == "" { + netID = app.lastQueryNet buffer = app.lastQuery } + s := app.sessions[netID] + if s == nil { + return errOffline + } content := fmt.Sprintf("\x01ACTION %s\x01", args[0]) - app.s.PrivMsg(buffer, content) - if !app.s.HasCapability("echo-message") { - buffer, line, _ := app.formatMessage(irc.MessageEvent{ - User: app.s.Nick(), + s.PrivMsg(buffer, content) + if !s.HasCapability("echo-message") { + buffer, line, _ := app.formatMessage(s, irc.MessageEvent{ + User: s.Nick(), Target: buffer, TargetIsChannel: true, Command: "PRIVMSG", Content: content, Time: time.Now(), }) - app.win.AddLine(buffer, ui.NotifyNone, line) + app.win.AddLine(netID, buffer, ui.NotifyNone, line) } return nil } func commandDoMsg(app *App, args []string) (err error) { - if app.s == nil { - return errOffline - } - target := args[0] content := args[1] - app.s.PrivMsg(target, content) - if !app.s.HasCapability("echo-message") { - buffer, line, _ := app.formatMessage(irc.MessageEvent{ - User: app.s.Nick(), + netID, _ := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { + return errOffline + } + s.PrivMsg(target, content) + if !s.HasCapability("echo-message") { + buffer, line, _ := app.formatMessage(s, irc.MessageEvent{ + User: s.Nick(), Target: target, TargetIsChannel: true, Command: "PRIVMSG", Content: content, Time: time.Now(), }) - app.win.AddLine(buffer, ui.NotifyNone, line) + app.win.AddLine(netID, buffer, ui.NotifyNone, line) } return nil } func commandDoNames(app *App, args []string) (err error) { - if app.s == nil { + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { return errOffline } - - buffer := app.win.CurrentBuffer() + if !s.IsChannel(buffer) { + return fmt.Errorf("this is not a channel") + } var sb ui.StyledStringBuilder sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGrey)) sb.WriteString("Names: ") - for _, name := range app.s.Names(buffer) { + for _, name := range s.Names(buffer) { if name.PowerLevel != "" { sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen)) sb.WriteString(name.PowerLevel) @@ -324,7 +330,7 @@ func commandDoNames(app *App, args []string) (err error) { } body := sb.StyledString() // TODO remove last space - app.win.AddLine(buffer, ui.NotifyNone, ui.Line{ + app.win.AddLine(netID, buffer, ui.NotifyNone, ui.Line{ At: time.Now(), Head: "--", HeadColor: tcell.ColorGray, @@ -334,40 +340,40 @@ func commandDoNames(app *App, args []string) (err error) { } func commandDoNick(app *App, args []string) (err error) { - if app.s == nil { - return errOffline - } - nick := args[0] - if i := strings.IndexAny(nick, " :@!*?"); i >= 0 { + if i := strings.IndexAny(nick, " :"); i >= 0 { return fmt.Errorf("illegal char %q in nickname", nick[i]) } - app.s.ChangeNick(nick) - return nil -} - -func commandDoMode(app *App, args []string) (err error) { - if app.s == nil { + s := app.CurrentSession() + if s == nil { return errOffline } + s.ChangeNick(nick) + return +} +func commandDoMode(app *App, args []string) (err error) { channel := args[0] flags := args[1] modeArgs := args[2:] - app.s.ChangeMode(channel, flags, modeArgs) + s := app.CurrentSession() + if s == nil { + return errOffline + } + s.ChangeMode(channel, flags, modeArgs) return nil } func commandDoPart(app *App, args []string) (err error) { - if app.s == nil { + netID, channel := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { return errOffline } - - channel := app.win.CurrentBuffer() reason := "" if 0 < len(args) { - if app.s.IsChannel(args[0]) { + if s.IsChannel(args[0]) { channel = args[0] if 1 < len(args) { reason = args[1] @@ -376,10 +382,11 @@ func commandDoPart(app *App, args []string) (err error) { reason = args[0] } } - if channel == Home { - return fmt.Errorf("cannot part home") + + if channel == "" { + return fmt.Errorf("cannot part this buffer") } - app.s.Part(channel, reason) + s.Part(channel, reason) return nil } @@ -388,68 +395,73 @@ func commandDoQuit(app *App, args []string) (err error) { if 0 < len(args) { reason = args[0] } - if app.s != nil { - app.s.Quit(reason) + for _, session := range app.sessions { + session.Quit(reason) } app.win.Exit() return nil } func commandDoQuote(app *App, args []string) (err error) { - if app.s == nil { + s := app.CurrentSession() + if s == nil { return errOffline } - - app.s.SendRaw(args[0]) + s.SendRaw(args[0]) return nil } func commandDoR(app *App, args []string) (err error) { - if app.s == nil { + s := app.sessions[app.lastQueryNet] + if s == nil { return errOffline } - - app.s.PrivMsg(app.lastQuery, args[0]) - if !app.s.HasCapability("echo-message") { - buffer, line, _ := app.formatMessage(irc.MessageEvent{ - User: app.s.Nick(), + s.PrivMsg(app.lastQuery, args[0]) + if !s.HasCapability("echo-message") { + buffer, line, _ := app.formatMessage(s, irc.MessageEvent{ + User: s.Nick(), Target: app.lastQuery, TargetIsChannel: true, Command: "PRIVMSG", Content: args[0], Time: time.Now(), }) - app.win.AddLine(buffer, ui.NotifyNone, line) + app.win.AddLine(app.lastQueryNet, buffer, ui.NotifyNone, line) } return nil } func commandDoTopic(app *App, args []string) (err error) { - if app.s == nil { - return errOffline - } - + netID, buffer := app.win.CurrentBuffer() + var ok bool if len(args) == 0 { - app.printTopic(app.win.CurrentBuffer()) + ok = app.printTopic(netID, buffer) } else { - app.s.ChangeTopic(app.win.CurrentBuffer(), args[0]) + s := app.sessions[netID] + if s != nil { + s.ChangeTopic(buffer, args[0]) + ok = true + } + } + if !ok { + return errOffline } return nil } func commandDoInvite(app *App, args []string) (err error) { - if app.s == nil { + nick := args[0] + netID, channel := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { return errOffline } - - nick := args[0] - channel := app.win.CurrentBuffer() if len(args) == 2 { channel = args[1] - } else if channel == Home { - return fmt.Errorf("cannot invite to home") + } else if channel == "" { + return fmt.Errorf("either send this command from a channel, or specify the channel") } - app.s.Invite(nick, channel) + s.Invite(nick, channel) return nil } @@ -522,7 +534,7 @@ func (app *App) handleInput(buffer, content string) error { cmdName, rawArgs, isCommand := parseCommand(content) if !isCommand { - return noCommand(app, buffer, rawArgs) + return noCommand(app, rawArgs) } if cmdName == "" { return fmt.Errorf("lone slash at the beginning") @@ -554,8 +566,8 @@ func (app *App) handleInput(buffer, content string) error { if len(args) < cmd.MinArgs { return fmt.Errorf("usage: %s %s", cmdName, cmd.Usage) } - if buffer == Home && !cmd.AllowHome { - return fmt.Errorf("command %q cannot be executed from home", cmdName) + if buffer == "" && !cmd.AllowHome { + return fmt.Errorf("command %q cannot be executed from a server buffer", cmdName) } return cmd.Handle(app, args) diff --git a/completions.go b/completions.go index db8a7f7..3e4a516 100644 --- a/completions.go +++ b/completions.go @@ -18,9 +18,11 @@ func (app *App) completionsChannelMembers(cs []ui.Completion, cursorIdx int, tex if len(word) == 0 { return cs } - wordCf := app.s.Casemap(string(word)) - for _, name := range app.s.Names(app.win.CurrentBuffer()) { - if strings.HasPrefix(app.s.Casemap(name.Name.Name), wordCf) { + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] // is not nil + wordCf := s.Casemap(string(word)) + for _, name := range s.Names(buffer) { + if strings.HasPrefix(s.Casemap(name.Name.Name), wordCf) { nickComp := []rune(name.Name.Name) if start == 0 { nickComp = append(nickComp, ':') @@ -45,7 +47,9 @@ func (app *App) completionsChannelTopic(cs []ui.Completion, cursorIdx int, text if !hasPrefix(text, []rune("/topic ")) { return cs } - topic, _, _ := app.s.Topic(app.win.CurrentBuffer()) + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] // is not nil + topic, _, _ := s.Topic(buffer) if cursorIdx == len(text) { compText := append(text, []rune(topic)...) cs = append(cs, ui.Completion{ @@ -60,6 +64,7 @@ func (app *App) completionsMsg(cs []ui.Completion, cursorIdx int, text []rune) [ if !hasPrefix(text, []rune("/msg ")) { return cs } + s := app.CurrentSession() // is not nil // Check if the first word (target) is already written and complete (in // which case we don't have completions to provide). var word string @@ -69,15 +74,15 @@ func (app *App) completionsMsg(cs []ui.Completion, cursorIdx int, text []rune) [ return cs } if !hasMetALetter && text[i] != ' ' { - word = app.s.Casemap(string(text[i:cursorIdx])) + word = s.Casemap(string(text[i:cursorIdx])) hasMetALetter = true } } if word == "" { return cs } - for _, user := range app.s.Users() { - if strings.HasPrefix(app.s.Casemap(user), word) { + for _, user := range s.Users() { + if strings.HasPrefix(s.Casemap(user), word) { nickComp := append([]rune(user), ' ') c := make([]rune, len(text)+5+len(nickComp)-cursorIdx) copy(c[:5], []rune("/msg ")) diff --git a/doc/senpai.1.scd b/doc/senpai.1.scd index 7ecc50b..6c2ac3a 100644 --- a/doc/senpai.1.scd +++ b/doc/senpai.1.scd @@ -23,6 +23,7 @@ extensions, such as: - _CHATHISTORY_, senpai fetches history from the server instead of keeping logs, - _@+typing_, senpai shows when others are typing a message, +- _BOUNCER_, senpai connects to all your networks at once automatically, - and more to come! # CONFIGURATION diff --git a/irc/events.go b/irc/events.go index 4898c26..c90f518 100644 --- a/irc/events.go +++ b/irc/events.go @@ -75,3 +75,8 @@ type HistoryEvent struct { Target string Messages []Event } + +type BouncerNetworkEvent struct { + ID string + Name string +} diff --git a/irc/session.go b/irc/session.go index e0421a7..201f50c 100644 --- a/irc/session.go +++ b/irc/session.go @@ -57,7 +57,8 @@ var SupportedCapabilities = map[string]struct{}{ "sasl": {}, "setname": {}, - "draft/chathistory": {}, + "draft/chathistory": {}, + "soju.im/bouncer-networks": {}, } // Values taken by the "@+typing=" client tag. TypingUnspec means the value or @@ -91,8 +92,8 @@ type SessionParams struct { Nickname string Username string RealName string - - Auth SASLClient + NetID string + Auth SASLClient } type Session struct { @@ -108,6 +109,7 @@ type Session struct { real string acct string host string + netID string auth SASLClient availableCaps map[string]string @@ -138,6 +140,7 @@ func NewSession(out chan<- Message, params SessionParams) *Session { nickCf: CasemapASCII(params.Nickname), user: params.Username, real: params.RealName, + netID: params.NetID, auth: params.Auth, availableCaps: map[string]string{}, enabledCaps: map[string]struct{}{}, @@ -180,6 +183,10 @@ func (s *Session) Nick() string { return s.nick } +func (s *Session) NetID() string { + return s.netID +} + // NickCf is our casemapped nickname. func (s *Session) NickCf() string { return s.nickCf @@ -486,10 +493,10 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) { return nil, err } - s.out <- NewMessage("CAP", "END") + s.endRegistration() s.host = ParsePrefix(userhost).Host case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs: - s.out <- NewMessage("CAP", "END") + s.endRegistration() case "CAP": var subcommand string if err := msg.ParseParams(nil, &subcommand); err != nil { @@ -525,7 +532,7 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) { _, ok := s.availableCaps["sasl"] if s.auth == nil || !ok { - s.out <- NewMessage("CAP", "END") + s.endRegistration() } } default: @@ -1024,6 +1031,19 @@ func (s *Session) handleRegistered(msg Message) (Event, error) { FormerNick: msg.Prefix.Name, }, nil } + case "BOUNCER": + if len(msg.Params) < 3 { + break + } + if msg.Params[0] != "NETWORK" || s.netID != "" { + break + } + id := msg.Params[1] + attrs := parseTags(msg.Params[2]) + return BouncerNetworkEvent{ + ID: id, + Name: attrs["name"], + }, nil case "PING": var payload string if err := msg.ParseParams(&payload); err != nil { @@ -1137,6 +1157,8 @@ func (s *Session) updateFeatures(features []string) { Switch: switch key { + case "BOUNCER_NETID": + s.netID = value case "CASEMAPPING": switch value { case "ascii": @@ -1175,3 +1197,17 @@ func (s *Session) updateFeatures(features []string) { } } } + +func (s *Session) endRegistration() { + if _, ok := s.enabledCaps["soju.im/bouncer-networks"]; !ok { + s.out <- NewMessage("CAP", "END") + return + } + if s.netID == "" { + s.out <- NewMessage("CAP", "END") + s.out <- NewMessage("BOUNCER", "LISTNETWORKS") + } else { + s.out <- NewMessage("BOUNCER", "BIND", s.netID) + s.out <- NewMessage("CAP", "END") + } +} diff --git a/irc/tokens.go b/irc/tokens.go index bb5f399..a52a764 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -132,7 +132,6 @@ func escapeTagValue(unescaped string) string { } func parseTags(s string) (tags map[string]string) { - s = s[1:] tags = map[string]string{} for _, item := range strings.Split(s, ";") { @@ -239,7 +238,7 @@ func ParseMessage(line string) (msg Message, err error) { var tags string tags, line = word(line) - msg.Tags = parseTags(tags) + msg.Tags = parseTags(tags[1:]) } line = strings.TrimLeft(line, " ") diff --git a/ui/buffers.go b/ui/buffers.go index 8fdd4b6..06bd7e9 100644 --- a/ui/buffers.go +++ b/ui/buffers.go @@ -172,6 +172,8 @@ func (l *Line) NewLines(width int) []int { } type buffer struct { + netID string + netName string title string highlights int unread bool @@ -239,15 +241,39 @@ func (bs *BufferList) Previous() { bs.list[bs.current].unread = false } -func (bs *BufferList) Add(title string) (i int, added bool) { +func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) { lTitle := strings.ToLower(title) + gotNetID := false for i, b := range bs.list { - if strings.ToLower(b.title) == lTitle { - return i, false + lbTitle := strings.ToLower(b.title) + if b.netID == netID { + gotNetID = true + if lbTitle == lTitle { + return i, false + } + } else if gotNetID || (b.netID == netID && lbTitle < lTitle) { + b := buffer{ + netID: netID, + netName: netName, + title: title, + } + bs.list = append(bs.list[:i+1], bs.list[i:]...) + bs.list[i] = b + if i <= bs.current && bs.current < len(bs.list) { + bs.current++ + } + return i, true } } - bs.list = append(bs.list, buffer{title: title}) + if netName == "" { + netName = netID + } + bs.list = append(bs.list, buffer{ + netID: netID, + netName: netName, + title: title, + }) return len(bs.list) - 1, true } @@ -266,8 +292,8 @@ func (bs *BufferList) Remove(title string) (ok bool) { return } -func (bs *BufferList) AddLine(title string, notify NotifyType, line Line) { - idx := bs.idx(title) +func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line) { + idx := bs.idx(netID, title) if idx < 0 { return } @@ -303,8 +329,8 @@ func (bs *BufferList) AddLine(title string, notify NotifyType, line Line) { } } -func (bs *BufferList) AddLines(title string, before, after []Line) { - idx := bs.idx(title) +func (bs *BufferList) AddLines(netID, title string, before, after []Line) { + idx := bs.idx(netID, title) if idx < 0 { return } @@ -326,8 +352,9 @@ func (bs *BufferList) AddLines(title string, before, after []Line) { } } -func (bs *BufferList) Current() (title string) { - return bs.list[bs.current].title +func (bs *BufferList) Current() (netID, title string) { + b := &bs.list[bs.current] + return b.netID, b.title } func (bs *BufferList) ScrollUp(n int) { @@ -352,14 +379,10 @@ func (bs *BufferList) IsAtTop() bool { return b.isAtTop } -func (bs *BufferList) idx(title string) int { - if title == "" { - return bs.current - } - +func (bs *BufferList) idx(netID, title string) int { lTitle := strings.ToLower(title) for i, b := range bs.list { - if strings.ToLower(b.title) == lTitle { + if b.netID == netID && strings.ToLower(b.title) == lTitle { return i } } @@ -391,7 +414,18 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, x = x0 + indexPadding } - title := truncate(b.title, width-(x-x0), "\u2026") + var title string + if b.title == "" { + title = b.netName + } else { + if i == bs.clicked { + screen.SetContent(x, y, ' ', nil, tcell.StyleDefault.Reverse(true)) + screen.SetContent(x+1, y, ' ', nil, tcell.StyleDefault.Reverse(true)) + } + x += 2 + title = b.title + } + title = truncate(title, width-(x-x0), "\u2026") printString(screen, &x, y, Styled(title, st)) if i == bs.clicked { @@ -427,8 +461,17 @@ func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, widt if i == bs.clicked { st = st.Reverse(true) } - title := truncate(b.title, width-x, "\u2026") + + var title string + if b.title == "" { + st = st.Dim(true) + title = b.netName + } else { + title = b.title + } + title = truncate(title, width-x, "\u2026") printString(screen, &x, y0, Styled(title, st)) + if 0 < b.highlights { st = st.Foreground(tcell.ColorRed).Reverse(true) screen.SetContent(x, y0, ' ', nil, st) @@ -82,7 +82,7 @@ func (ui *UI) Close() { ui.screen.Fini() } -func (ui *UI) CurrentBuffer() string { +func (ui *UI) CurrentBuffer() (netID, title string) { return ui.bs.Current() } @@ -147,8 +147,8 @@ func (ui *UI) IsAtTop() bool { return ui.bs.IsAtTop() } -func (ui *UI) AddBuffer(title string) (i int, added bool) { - return ui.bs.Add(title) +func (ui *UI) AddBuffer(netID, netName, title string) (i int, added bool) { + return ui.bs.Add(netID, netName, title) } func (ui *UI) RemoveBuffer(title string) { @@ -156,12 +156,12 @@ func (ui *UI) RemoveBuffer(title string) { ui.memberOffset = 0 } -func (ui *UI) AddLine(buffer string, notify NotifyType, line Line) { - ui.bs.AddLine(buffer, notify, line) +func (ui *UI) AddLine(netID, buffer string, notify NotifyType, line Line) { + ui.bs.AddLine(netID, buffer, notify, line) } -func (ui *UI) AddLines(buffer string, before, after []Line) { - ui.bs.AddLines(buffer, before, after) +func (ui *UI) AddLines(netID, buffer string, before, after []Line) { + ui.bs.AddLines(netID, buffer, before, after) } func (ui *UI) JumpBuffer(sub string) bool { @@ -188,6 +188,19 @@ func (ui *UI) JumpBufferIndex(i int) bool { return false } +func (ui *UI) JumpBufferNetwork(netID, sub string) bool { + subLower := strings.ToLower(sub) + for i, b := range ui.bs.list { + if b.netID == netID && strings.Contains(strings.ToLower(b.title), subLower) { + if ui.bs.To(i) { + ui.memberOffset = 0 + } + return true + } + } + return false +} + func (ui *UI) SetStatus(status string) { ui.status = status } @@ -9,42 +9,50 @@ import ( "github.com/gdamore/tcell/v2" ) -var Home = "home" - const welcomeMessage = "senpai dev build. See senpai(1) for a list of keybindings and commands. Private messages and status notices go here." func (app *App) initWindow() { - app.win.AddBuffer(Home) - app.win.AddLine(Home, ui.NotifyNone, ui.Line{ + app.win.AddBuffer("", "(home)", "") + app.win.AddLine("", "", ui.NotifyNone, ui.Line{ Head: "--", Body: ui.PlainString(welcomeMessage), At: time.Now(), }) } -func (app *App) queueStatusLine(line ui.Line) { +type statusLine struct { + netID string + line ui.Line +} + +func (app *App) queueStatusLine(netID string, line ui.Line) { if line.At.IsZero() { line.At = time.Now() } app.events <- event{ - src: uiEvent, - content: line, + src: "*", + content: statusLine{ + netID: netID, + line: line, + }, } } -func (app *App) addStatusLine(line ui.Line) { - buffer := app.win.CurrentBuffer() - if buffer != Home { - app.win.AddLine(Home, ui.NotifyNone, line) +func (app *App) addStatusLine(netID string, line ui.Line) { + currentNetID, buffer := app.win.CurrentBuffer() + if currentNetID == netID && buffer != "" { + app.win.AddLine(netID, buffer, ui.NotifyNone, line) } - app.win.AddLine(buffer, ui.NotifyNone, line) + app.win.AddLine(netID, "", ui.NotifyNone, line) } func (app *App) setStatus() { - if app.s == nil { + netID, buffer := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { return } - ts := app.s.Typings(app.win.CurrentBuffer()) + ts := s.Typings(buffer) status := "" if 3 < len(ts) { status = "several people are typing..." |