summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtz.pm>2021-10-20 17:33:10 +0200
committerHubert Hirtz <hubert@hirtz.pm>2021-10-24 12:57:24 +0200
commit9fb4378753ddec61a504a0dec403f40d6def7e90 (patch)
tree32955930be2d97614f3ec6290f67d0556769a16e
parentRemove 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.go276
-rw-r--r--cmd/senpai/main.go21
-rw-r--r--commands.go204
-rw-r--r--completions.go19
-rw-r--r--doc/senpai.1.scd1
-rw-r--r--irc/events.go5
-rw-r--r--irc/session.go48
-rw-r--r--irc/tokens.go3
-rw-r--r--ui/buffers.go79
-rw-r--r--ui/ui.go27
-rw-r--r--window.go36
11 files changed, 444 insertions, 275 deletions
diff --git a/app.go b/app.go
index 2a68573..bc96f0f 100644
--- a/app.go
+++ b/app.go
@@ -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)
diff --git a/ui/ui.go b/ui/ui.go
index d1007cb..5f87fe3 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -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
}
diff --git a/window.go b/window.go
index ce416ed..82941a1 100644
--- a/window.go
+++ b/window.go
@@ -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..."