M app.go => app.go +158 -118
@@ 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
}
M cmd/senpai/main.go => cmd/senpai/main.go +14 -7
@@ 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]
}
M commands.go => commands.go +108 -96
@@ 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)
M completions.go => completions.go +12 -7
@@ 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 "))
M doc/senpai.1.scd => doc/senpai.1.scd +1 -0
@@ 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
M irc/events.go => irc/events.go +5 -0
@@ 75,3 75,8 @@ type HistoryEvent struct {
Target string
Messages []Event
}
+
+type BouncerNetworkEvent struct {
+ ID string
+ Name string
+}
M irc/session.go => irc/session.go +42 -6
@@ 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")
+ }
+}
M irc/tokens.go => irc/tokens.go +1 -2
@@ 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, " ")
M ui/buffers.go => ui/buffers.go +61 -18
@@ 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)
M ui/ui.go => ui/ui.go +20 -7
@@ 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
}
M window.go => window.go +22 -14
@@ 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..."