summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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..."