diff options
-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..." |