summaryrefslogtreecommitdiff
path: root/app.go
diff options
context:
space:
mode:
Diffstat (limited to 'app.go')
-rw-r--r--app.go431
1 files changed, 241 insertions, 190 deletions
diff --git a/app.go b/app.go
index 8e8231f..ef93e51 100644
--- a/app.go
+++ b/app.go
@@ -79,7 +79,7 @@ func NewApp(cfg Config) (app *App, err error) {
func (app *App) Close() {
app.win.Close()
if app.s != nil {
- app.s.Stop()
+ app.s.Close()
}
}
@@ -109,21 +109,8 @@ func (app *App) eventLoop() {
app.handleEvents(evs)
if !app.pasting {
- app.draw()
- }
- }
-}
-
-// handleEvents handles a batch of events.
-func (app *App) handleEvents(evs []event) {
- for _, ev := range evs {
- switch ev.src {
- case uiEvent:
- app.handleUIEvent(ev.content.(tcell.Event))
- case ircEvent:
- app.handleIRCEvent(ev.content.(irc.Event))
- default:
- panic("unreachable")
+ app.setStatus()
+ app.win.Draw()
}
}
}
@@ -131,15 +118,48 @@ func (app *App) handleEvents(evs []event) {
// ircLoop maintains a connection to the IRC server by connecting and then
// forwarding IRC events to app.events repeatedly.
func (app *App) ircLoop() {
+ var auth irc.SASLClient
+ if app.cfg.Password != nil {
+ auth = &irc.SASLPlain{
+ Username: app.cfg.User,
+ Password: *app.cfg.Password,
+ }
+ }
+ params := irc.SessionParams{
+ Nickname: app.cfg.Nick,
+ Username: app.cfg.User,
+ RealName: app.cfg.Real,
+ Auth: auth,
+ }
for !app.win.ShouldExit() {
- app.connect()
- for ev := range app.s.Poll() {
+ conn := app.connect()
+ in, out := irc.ChanInOut(conn)
+ if app.cfg.Debug {
+ out = app.debugOutputMessages(out)
+ }
+ session := irc.NewSession(out, params)
+ app.events <- event{
+ src: ircEvent,
+ content: session,
+ }
+ for msg := range in {
+ if app.cfg.Debug {
+ app.queueStatusLine(ui.Line{
+ At: time.Now(),
+ Head: "IN --",
+ Body: msg.String(),
+ })
+ }
app.events <- event{
src: ircEvent,
- content: ev,
+ content: msg,
}
}
- app.addLineNow(Home, ui.Line{
+ app.events <- event{
+ src: ircEvent,
+ content: nil,
+ }
+ app.queueStatusLine(ui.Line{
Head: "!!",
HeadColor: ui.ColorRed,
Body: "Connection lost",
@@ -147,17 +167,17 @@ func (app *App) ircLoop() {
}
}
-func (app *App) connect() {
+func (app *App) connect() net.Conn {
for {
- app.addLineNow(Home, ui.Line{
+ app.queueStatusLine(ui.Line{
Head: "--",
Body: fmt.Sprintf("Connecting to %s...", app.cfg.Addr),
})
- err := app.tryConnect()
+ conn, err := app.tryConnect()
if err == nil {
- break
+ return conn
}
- app.addLineNow(Home, ui.Line{
+ app.queueStatusLine(ui.Line{
Head: "!!",
HeadColor: ui.ColorRed,
Body: fmt.Sprintf("Connection failed: %v", err),
@@ -166,7 +186,7 @@ func (app *App) connect() {
}
}
-func (app *App) tryConnect() (err error) {
+func (app *App) tryConnect() (conn net.Conn, err error) {
addr := app.cfg.Addr
colonIdx := strings.LastIndexByte(addr, ':')
bracketIdx := strings.LastIndexByte(addr, ']')
@@ -180,7 +200,7 @@ func (app *App) tryConnect() (err error) {
}
}
- conn, err := net.Dial("tcp", addr)
+ conn, err = net.Dial("tcp", addr)
if err != nil {
return
}
@@ -193,28 +213,24 @@ func (app *App) tryConnect() (err error) {
})
}
- var auth irc.SASLClient
- if app.cfg.Password != nil {
- auth = &irc.SASLPlain{
- Username: app.cfg.User,
- Password: *app.cfg.Password,
- }
- }
- app.s, err = irc.NewSession(conn, irc.SessionParams{
- Nickname: app.cfg.Nick,
- Username: app.cfg.User,
- RealName: app.cfg.Real,
- Auth: auth,
- Debug: app.cfg.Debug,
- })
- if err != nil {
- conn.Close()
- return
- }
-
return
}
+func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message {
+ debugOut := make(chan irc.Message, cap(out))
+ go func() {
+ for msg := range debugOut {
+ app.queueStatusLine(ui.Line{
+ At: time.Now(),
+ Head: "OUT --",
+ Body: msg.String(),
+ })
+ out <- msg
+ }
+ }()
+ return debugOut
+}
+
// uiLoop retrieves events from the UI and forwards them to app.events for
// handling in app.eventLoop().
func (app *App) uiLoop() {
@@ -230,118 +246,34 @@ func (app *App) uiLoop() {
}
}
-func (app *App) handleIRCEvent(ev irc.Event) {
- switch ev := ev.(type) {
- case irc.RawMessageEvent:
- head := "IN --"
- if ev.Outgoing {
- head = "OUT --"
- } else if !ev.IsValid {
- head = "IN ??"
- }
- app.win.AddLine(Home, false, ui.Line{
- At: time.Now(),
- Head: head,
- Body: ev.Message,
- })
- case irc.ErrorEvent:
- var severity string
- switch ev.Severity {
- case irc.SeverityNote:
- severity = "Note"
- case irc.SeverityWarn:
- severity = "Warning"
- case irc.SeverityFail:
- severity = "Error"
- }
- app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
- At: time.Now(),
- Head: "!!",
- HeadColor: ui.ColorRed,
- Body: fmt.Sprintf("%s (code %s): %s", severity, ev.Code, ev.Message),
- })
- case irc.RegisteredEvent:
- body := "Connected to the server"
- if app.s.Nick() != app.cfg.Nick {
- body += " as " + app.s.Nick()
- }
- app.win.AddLine(Home, false, ui.Line{
- At: time.Now(),
- Head: "--",
- Body: body,
- })
- case irc.SelfNickEvent:
- app.win.AddLine(app.win.CurrentBuffer(), true, ui.Line{
- At: ev.Time,
- Head: "--",
- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, app.s.Nick()),
- Highlight: true,
- })
- case irc.UserNickEvent:
- for _, c := range app.s.ChannelsSharedWith(ev.User.Name) {
- app.win.AddLine(c, false, ui.Line{
- At: ev.Time,
- Head: "--",
- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, ev.User.Name),
- Mergeable: true,
- })
- }
- case irc.SelfJoinEvent:
- app.win.AddBuffer(ev.Channel)
- app.s.RequestHistory(ev.Channel, time.Now())
- case irc.UserJoinEvent:
- app.win.AddLine(ev.Channel, false, ui.Line{
- At: time.Now(),
- Head: "--",
- Body: fmt.Sprintf("\x033+\x0314%s\x03", ev.User.Name),
- Mergeable: true,
- })
- case irc.SelfPartEvent:
- app.win.RemoveBuffer(ev.Channel)
- case irc.UserPartEvent:
- app.win.AddLine(ev.Channel, false, ui.Line{
- At: ev.Time,
- Head: "--",
- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User.Name),
- Mergeable: true,
- })
- case irc.UserQuitEvent:
- for _, c := range ev.Channels {
- app.win.AddLine(c, false, ui.Line{
- At: ev.Time,
- Head: "--",
- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User.Name),
- Mergeable: true,
- })
- }
- case irc.TopicChangeEvent:
- app.win.AddLine(ev.Channel, false, ui.Line{
- At: ev.Time,
- Head: "--",
- Body: fmt.Sprintf("\x0314Topic changed to: %s\x03", ev.Topic),
- })
- case irc.MessageEvent:
- buffer, line, hlNotification := app.formatMessage(ev)
- app.win.AddLine(buffer, hlNotification, line)
- if hlNotification {
- app.notifyHighlight(buffer, ev.User.Name, ev.Content)
- }
- if !ev.TargetIsChannel && app.s.NickCf() != app.s.Casemap(ev.User.Name) {
- app.lastQuery = ev.User.Name
- }
- case irc.HistoryEvent:
- var lines []ui.Line
- for _, m := range ev.Messages {
- switch m := m.(type) {
- case irc.MessageEvent:
- _, line, _ := app.formatMessage(m)
- lines = append(lines, line)
- default:
- }
+// handleEvents handles a batch of events.
+func (app *App) handleEvents(evs []event) {
+ for _, ev := range evs {
+ switch ev.src {
+ case uiEvent:
+ app.handleUIEvent(ev.content)
+ case ircEvent:
+ app.handleIRCEvent(ev.content)
+ default:
+ panic("unreachable")
}
- app.win.AddLines(ev.Target, lines)
- case error:
- panic(ev)
+ }
+}
+
+func (app *App) handleUIEvent(ev interface{}) {
+ switch ev := ev.(type) {
+ case *tcell.EventResize:
+ app.win.Resize()
+ case *tcell.EventPaste:
+ app.pasting = ev.Start()
+ case *tcell.EventMouse:
+ app.handleMouseEvent(ev)
+ case *tcell.EventKey:
+ app.handleKeyEvent(ev)
+ case ui.Line:
+ app.addStatusLine(ev)
+ default:
+ return
}
}
@@ -484,24 +416,6 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
}
}
-func (app *App) handleUIEvent(ev tcell.Event) {
- switch ev := ev.(type) {
- case *tcell.EventResize:
- app.win.Resize()
- case *tcell.EventPaste:
- app.pasting = ev.Start()
- case *tcell.EventMouse:
- app.handleMouseEvent(ev)
- case *tcell.EventKey:
- app.handleKeyEvent(ev)
- default:
- return
- }
- if !app.pasting {
- app.draw()
- }
-}
-
// requestHistory is a wrapper around irc.Session.RequestHistory to only request
// history when needed.
func (app *App) requestHistory() {
@@ -510,22 +424,159 @@ func (app *App) requestHistory() {
}
buffer := app.win.CurrentBuffer()
if app.win.IsAtTop() && buffer != Home {
- at := time.Now()
- if t := app.win.CurrentBufferOldestTime(); t != nil {
- at = *t
+ t := time.Now()
+ if oldest := app.win.CurrentBufferOldestTime(); oldest != nil {
+ t = *oldest
}
- app.s.RequestHistory(buffer, at)
+ app.s.NewHistoryRequest(buffer).
+ WithLimit(100).
+ Before(t)
}
}
+func (app *App) handleIRCEvent(ev interface{}) {
+ if ev == nil {
+ app.s.Close()
+ app.s = nil
+ return
+ }
+ if s, ok := ev.(*irc.Session); ok {
+ app.s = s
+ return
+ }
+
+ msg := ev.(irc.Message)
+
+ // Mutate IRC state
+ ev = app.s.HandleMessage(msg)
+
+ // Mutate UI state
+ switch ev := ev.(type) {
+ case irc.RegisteredEvent:
+ body := "Connected to the server"
+ if app.s.Nick() != app.cfg.Nick {
+ body += " as " + app.s.Nick()
+ }
+ app.win.AddLine(Home, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: body,
+ })
+ case irc.SelfNickEvent:
+ app.win.AddLine(app.win.CurrentBuffer(), true, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, app.s.Nick()),
+ Highlight: true,
+ })
+ case irc.UserNickEvent:
+ for _, c := range app.s.ChannelsSharedWith(ev.User) {
+ app.win.AddLine(c, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, ev.User),
+ Mergeable: true,
+ })
+ }
+ case irc.SelfJoinEvent:
+ app.win.AddBuffer(ev.Channel)
+ app.s.NewHistoryRequest(ev.Channel).
+ WithLimit(200).
+ Before(msg.TimeOrNow())
+ case irc.UserJoinEvent:
+ app.win.AddLine(ev.Channel, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x033+\x0314%s\x03", ev.User),
+ Mergeable: true,
+ })
+ case irc.SelfPartEvent:
+ app.win.RemoveBuffer(ev.Channel)
+ case irc.UserPartEvent:
+ app.win.AddLine(ev.Channel, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
+ Mergeable: true,
+ })
+ case irc.UserQuitEvent:
+ for _, c := range ev.Channels {
+ app.win.AddLine(c, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
+ Mergeable: true,
+ })
+ }
+ case irc.TopicChangeEvent:
+ app.win.AddLine(ev.Channel, false, ui.Line{
+ At: msg.TimeOrNow(),
+ Head: "--",
+ Body: fmt.Sprintf("\x0314Topic changed to: %s\x03", ev.Topic),
+ })
+ case irc.MessageEvent:
+ buffer, line, hlNotification := app.formatMessage(ev)
+ app.win.AddLine(buffer, hlNotification, line)
+ if hlNotification {
+ app.notifyHighlight(buffer, ev.User, ev.Content)
+ }
+ if !app.s.IsChannel(msg.Params[0]) && !app.s.IsMe(ev.User) {
+ app.lastQuery = msg.Prefix.Name
+ }
+ case irc.HistoryEvent:
+ var lines []ui.Line
+ for _, m := range ev.Messages {
+ switch ev := m.(type) {
+ case irc.MessageEvent:
+ _, line, _ := app.formatMessage(ev)
+ lines = append(lines, line)
+ }
+ }
+ app.win.AddLines(ev.Target, lines)
+ case irc.ErrorEvent:
+ if isBlackListed(msg.Command) {
+ break
+ }
+ var head string
+ var body string
+ switch ev.Severity {
+ case irc.SeverityFail:
+ head = "--"
+ body = fmt.Sprintf("Error (code %s): %s", ev.Code, ev.Message)
+ case irc.SeverityWarn:
+ head = "--"
+ body = fmt.Sprintf("Warning (code %s): %s", ev.Code, ev.Message)
+ case irc.SeverityNote:
+ head = ev.Code + " --"
+ body = ev.Message
+ default:
+ panic("unreachable")
+ }
+ app.addStatusLine(ui.Line{
+ At: msg.TimeOrNow(),
+ Head: head,
+ Body: body,
+ })
+ }
+}
+
+func isBlackListed(command string) bool {
+ switch command {
+ case "002", "003", "004", "422":
+ // useless connection messages
+ return true
+ }
+ return false
+}
+
// isHighlight reports whether the given message content is a highlight.
func (app *App) isHighlight(content string) bool {
- contentCf := strings.ToLower(content)
+ contentCf := app.s.Casemap(content)
if app.highlights == nil {
return strings.Contains(contentCf, app.s.NickCf())
}
for _, h := range app.highlights {
- if strings.Contains(contentCf, h) {
+ if strings.Contains(contentCf, app.s.Casemap(h)) {
return true
}
}
@@ -556,7 +607,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.win.AddLine(Home, false, ui.Line{
+ app.addStatusLine(ui.Line{
At: time.Now(),
Head: "!!",
HeadColor: ui.ColorRed,
@@ -615,7 +666,7 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
// - 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.NickCf() == app.s.Casemap(ev.User.Name)
+ isFromSelf := app.s.IsMe(ev.User)
isHighlight := app.isHighlight(ev.Content)
isAction := strings.HasPrefix(ev.Content, "\x01ACTION")
isQuery := !ev.TargetIsChannel && ev.Command == "PRIVMSG"
@@ -632,7 +683,7 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
hlLine := ev.TargetIsChannel && isHighlight && !isFromSelf
hlNotification = (isHighlight || isQuery) && !isFromSelf
- head := ev.User.Name
+ head := ev.User
headColor := ui.ColorWhite
if isFromSelf && isQuery {
head = "\u2192 " + ev.Target
@@ -645,14 +696,14 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
body := strings.TrimSuffix(ev.Content, "\x01")
if isNotice && isAction {
- c := ircColorSequence(ui.IdentColor(ev.User.Name))
- body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User.Name, body[7:])
+ c := ircColorSequence(ui.IdentColor(ev.User))
+ body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User, body[7:])
} else if isAction {
- c := ircColorSequence(ui.IdentColor(ev.User.Name))
- body = fmt.Sprintf("%s%s\x0F%s", c, ev.User.Name, body[7:])
+ c := ircColorSequence(ui.IdentColor(ev.User))
+ body = fmt.Sprintf("%s%s\x0F%s", c, ev.User, body[7:])
} else if isNotice {
- c := ircColorSequence(ui.IdentColor(ev.User.Name))
- body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User.Name, body)
+ c := ircColorSequence(ui.IdentColor(ev.User))
+ body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User, body)
}
line = ui.Line{