check / check (push) Waiting to run
A second /connect started a new poll loop without stopping the first. The first loop then polled with the new client too, so each message could be shown twice. It also read stopPoll without a.mu while cmdConnect wrote it. cmdConnect now closes the earlier loop's channel in the same locked step that replaces the client and the cursor, and hands the new channel to the new loop. The loop checks its channel under a.mu before each poll and again before it moves the cursor, so a stopped loop never polls with the new client or moves the new cursor. Model: opus-5-5
955 lines
16 KiB
Go
955 lines
16 KiB
Go
// Package cli implements the neoirc-cli terminal client.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
api "sneak.berlin/go/neoirc/internal/cli/api"
|
|
"sneak.berlin/go/neoirc/pkg/irc"
|
|
)
|
|
|
|
const (
|
|
splitParts = 2
|
|
pollTimeout = 15
|
|
pollRetry = 2 * time.Second
|
|
timeFormat = "15:04"
|
|
)
|
|
|
|
// App holds the application state.
|
|
type App struct {
|
|
ui *UI
|
|
client *api.Client
|
|
|
|
mu sync.Mutex
|
|
nick string
|
|
target string
|
|
connected bool
|
|
lastQID int64
|
|
stopPoll chan struct{}
|
|
}
|
|
|
|
// Run creates and runs the CLI application.
|
|
func Run() {
|
|
err := run(NewUI())
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run starts the client on ui and returns when ui stops. Tests
|
|
// call it with ui on a simulation screen.
|
|
func run(ui *UI) error {
|
|
app := &App{ //nolint:exhaustruct
|
|
ui: ui,
|
|
nick: "guest",
|
|
}
|
|
|
|
app.ui.OnInput(app.handleInput)
|
|
|
|
// SetStatus and AddStatus wait for the event loop, which
|
|
// app.ui.Run starts below, so they run on a goroutine. The
|
|
// nick is read first, as a typed /nick may change app.nick.
|
|
nick := app.nick
|
|
|
|
go func() {
|
|
app.ui.SetStatus(nick, "", "disconnected")
|
|
|
|
app.ui.AddStatus(
|
|
"Welcome to neoirc-cli — an IRC-style client",
|
|
)
|
|
app.ui.AddStatus(
|
|
"Type [yellow]/connect <server-url>" +
|
|
"[white] to begin, " +
|
|
"or [yellow]/help[white] for commands",
|
|
)
|
|
}()
|
|
|
|
return app.ui.Run()
|
|
}
|
|
|
|
func (a *App) handleInput(text string) {
|
|
if strings.HasPrefix(text, "/") {
|
|
a.handleCommand(text)
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
target := a.target
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus(
|
|
"[red]Not connected. Use /connect <url>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if target == "" {
|
|
a.ui.AddStatus(
|
|
"[red]No target. " +
|
|
"Use /join #channel or /query nick",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdPrivmsg,
|
|
To: target,
|
|
Body: []string{text},
|
|
})
|
|
if err != nil {
|
|
a.ui.AddStatus(
|
|
"[red]Send error: " + err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleCommand(text string) {
|
|
parts := strings.SplitN(text, " ", splitParts)
|
|
cmd := strings.ToLower(parts[0])
|
|
|
|
args := ""
|
|
if len(parts) > 1 {
|
|
args = parts[1]
|
|
}
|
|
|
|
a.dispatchCommand(cmd, args)
|
|
}
|
|
|
|
func (a *App) dispatchCommand(cmd, args string) {
|
|
switch cmd {
|
|
case "/connect":
|
|
a.cmdConnect(args)
|
|
case "/nick":
|
|
a.cmdNick(args)
|
|
case "/join":
|
|
a.cmdJoin(args)
|
|
case "/part":
|
|
a.cmdPart(args)
|
|
case "/msg":
|
|
a.cmdMsg(args)
|
|
case "/query":
|
|
a.cmdQuery(args)
|
|
case "/topic":
|
|
a.cmdTopic(args)
|
|
case "/window", "/w":
|
|
a.cmdWindow(args)
|
|
case "/quit":
|
|
a.cmdQuit()
|
|
case "/help":
|
|
a.cmdHelp()
|
|
default:
|
|
a.dispatchInfoCommand(cmd, args)
|
|
}
|
|
}
|
|
|
|
func (a *App) dispatchInfoCommand(cmd, args string) {
|
|
switch cmd {
|
|
case "/names":
|
|
a.cmdNames()
|
|
case "/list":
|
|
a.cmdList()
|
|
case "/motd":
|
|
a.cmdMotd()
|
|
case "/who":
|
|
a.cmdWho(args)
|
|
case "/whois":
|
|
a.cmdWhois(args)
|
|
default:
|
|
a.ui.AddStatus(
|
|
"[red]Unknown command: " + cmd,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdConnect(serverURL string) {
|
|
if serverURL == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /connect <server-url>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
serverURL = strings.TrimRight(serverURL, "/")
|
|
|
|
a.ui.AddStatus("Connecting to " + serverURL + "...")
|
|
|
|
a.mu.Lock()
|
|
nick := a.nick
|
|
a.mu.Unlock()
|
|
|
|
client := api.NewClient(serverURL)
|
|
|
|
resp, err := client.CreateSession(nick)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Connection failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
// The poll loop of an earlier connection is stopped in the same
|
|
// step that replaces the client, so it never polls with the new
|
|
// one.
|
|
a.mu.Lock()
|
|
a.client = client
|
|
a.nick = resp.Nick
|
|
a.connected = true
|
|
a.lastQID = 0
|
|
|
|
if a.stopPoll != nil {
|
|
close(a.stopPoll)
|
|
}
|
|
|
|
stop := make(chan struct{})
|
|
a.stopPoll = stop
|
|
a.mu.Unlock()
|
|
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[green]Connected! Nick: %s, Session: %d",
|
|
resp.Nick, resp.ID,
|
|
))
|
|
a.ui.SetStatus(resp.Nick, "", "connected")
|
|
|
|
go a.pollLoop(stop)
|
|
}
|
|
|
|
func (a *App) cmdNick(nick string) {
|
|
if nick == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /nick <name>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.mu.Lock()
|
|
a.nick = nick
|
|
a.mu.Unlock()
|
|
|
|
a.ui.AddStatus(
|
|
"Nick set to " + nick +
|
|
" (will be used on connect)",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdNick,
|
|
Body: []string{nick},
|
|
})
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Nick change failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.nick = nick
|
|
target := a.target
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SetStatus(nick, target, "connected")
|
|
a.ui.AddStatus("Nick changed to " + nick)
|
|
}
|
|
|
|
func (a *App) cmdJoin(channel string) {
|
|
if channel == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /join #channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.JoinChannel(channel)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Join failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.target = channel
|
|
nick := a.nick
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SwitchToBuffer(channel)
|
|
a.ui.SetStatus(nick, channel, "connected")
|
|
}
|
|
|
|
func (a *App) cmdPart(channel string) {
|
|
a.mu.Lock()
|
|
if channel == "" {
|
|
channel = a.target
|
|
}
|
|
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if channel == "" ||
|
|
!strings.HasPrefix(channel, "#") {
|
|
a.ui.AddStatus("[red]No channel to part")
|
|
|
|
return
|
|
}
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.PartChannel(channel)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Part failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
if a.target == channel {
|
|
a.target = ""
|
|
}
|
|
|
|
nick := a.nick
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SwitchBuffer(0)
|
|
a.ui.SetStatus(nick, "", "connected")
|
|
}
|
|
|
|
func (a *App) cmdMsg(args string) {
|
|
parts := strings.SplitN(args, " ", splitParts)
|
|
if len(parts) < splitParts {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /msg <nick> <text>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
target, text := parts[0], parts[1]
|
|
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdPrivmsg,
|
|
To: target,
|
|
Body: []string{text},
|
|
})
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Send failed: %v", err,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdQuery(nick string) {
|
|
if nick == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /query <nick>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.target = nick
|
|
myNick := a.nick
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SwitchToBuffer(nick)
|
|
a.ui.SetStatus(myNick, nick, "connected")
|
|
}
|
|
|
|
func (a *App) cmdTopic(args string) {
|
|
a.mu.Lock()
|
|
target := a.target
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(target, "#") {
|
|
a.ui.AddStatus("[red]Not in a channel")
|
|
|
|
return
|
|
}
|
|
|
|
// The HTTP API has no topic query: a TOPIC needs text.
|
|
if args == "" {
|
|
a.ui.AddStatus("[red]Usage: /topic <text>")
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdTopic,
|
|
To: target,
|
|
Body: []string{args},
|
|
})
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Topic set failed: %v", err,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdNames() {
|
|
a.mu.Lock()
|
|
target := a.target
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(target, "#") {
|
|
a.ui.AddStatus("[red]Not in a channel")
|
|
|
|
return
|
|
}
|
|
|
|
members, err := a.client.GetMembers(target)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]Names failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
nicks := make([]string, 0, len(members))
|
|
|
|
for _, member := range members {
|
|
nicks = append(nicks, memberPrefix(member)+member.Nick)
|
|
}
|
|
|
|
a.ui.AddLine(target, fmt.Sprintf(
|
|
"[cyan]*** Members of %s: %s",
|
|
target, strings.Join(nicks, " "),
|
|
))
|
|
}
|
|
|
|
// memberPrefix returns "@" for an operator, "+" for a voiced
|
|
// member, and "" for anyone else.
|
|
func memberPrefix(member api.Member) string {
|
|
if member.IsOperator {
|
|
return "@"
|
|
}
|
|
|
|
if member.IsVoiced {
|
|
return "+"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (a *App) cmdList() {
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
channels, err := a.client.ListChannels()
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]List failed: %v", err,
|
|
))
|
|
|
|
return
|
|
}
|
|
|
|
a.ui.AddStatus("[cyan]*** Channel list:")
|
|
|
|
for _, ch := range channels {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
" %s %s", ch.Name, ch.Topic,
|
|
))
|
|
}
|
|
|
|
a.ui.AddStatus("[cyan]*** End of channel list")
|
|
}
|
|
|
|
func (a *App) cmdMotd() {
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(
|
|
&api.Message{Command: irc.CmdMotd}, //nolint:exhaustruct
|
|
)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]MOTD failed: %v", err,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdWho(args string) {
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
target := a.target
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
channel := args
|
|
if channel == "" {
|
|
channel = target
|
|
}
|
|
|
|
if channel == "" ||
|
|
!strings.HasPrefix(channel, "#") {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /who #channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(
|
|
&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdWho, To: channel,
|
|
},
|
|
)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]WHO failed: %v", err,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdWhois(args string) {
|
|
a.mu.Lock()
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
if !connected {
|
|
a.ui.AddStatus("[red]Not connected")
|
|
|
|
return
|
|
}
|
|
|
|
if args == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /whois <nick>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := a.client.SendMessage(
|
|
&api.Message{ //nolint:exhaustruct
|
|
Command: irc.CmdWhois, To: args,
|
|
},
|
|
)
|
|
if err != nil {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[red]WHOIS failed: %v", err,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdWindow(args string) {
|
|
if args == "" {
|
|
a.ui.AddStatus(
|
|
"[red]Usage: /window <number>",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
var bufIndex int
|
|
|
|
_, _ = fmt.Sscanf(args, "%d", &bufIndex)
|
|
|
|
a.mu.Lock()
|
|
nick := a.nick
|
|
connected := a.connected
|
|
a.mu.Unlock()
|
|
|
|
connStatus := "disconnected"
|
|
if connected {
|
|
connStatus = "connected"
|
|
}
|
|
|
|
// Read the name before switching. A message from the server can
|
|
// add a buffer at any time, but only at the end, so bufIndex
|
|
// still names this buffer when SwitchBuffer runs.
|
|
name, found := a.ui.BufferName(bufIndex)
|
|
if found {
|
|
a.ui.SwitchBuffer(bufIndex)
|
|
|
|
if name != "(status)" {
|
|
a.mu.Lock()
|
|
a.target = name
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SetStatus(
|
|
nick, name, connStatus,
|
|
)
|
|
} else {
|
|
a.mu.Lock()
|
|
a.target = ""
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SetStatus(nick, "", connStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) cmdQuit() {
|
|
a.mu.Lock()
|
|
|
|
if a.connected && a.client != nil {
|
|
_ = a.client.SendMessage(
|
|
&api.Message{Command: irc.CmdQuit}, //nolint:exhaustruct
|
|
)
|
|
}
|
|
|
|
if a.stopPoll != nil {
|
|
close(a.stopPoll)
|
|
}
|
|
|
|
a.mu.Unlock()
|
|
a.ui.Stop()
|
|
}
|
|
|
|
func (a *App) cmdHelp() {
|
|
help := []string{
|
|
"[cyan]*** neoirc-cli commands:",
|
|
" /connect <url> — Connect to server",
|
|
" /nick <name> — Change nickname",
|
|
" /join #channel — Join channel",
|
|
" /part [#chan] — Leave channel",
|
|
" /msg <nick> <text> — Send DM",
|
|
" /query <nick> — Open DM window",
|
|
" /topic <text> — Set topic",
|
|
" /names — List channel members",
|
|
" /list — List channels",
|
|
" /who [#channel] — List users in channel",
|
|
" /whois <nick> — Show user info",
|
|
" /motd — Show message of the day",
|
|
" /window <n> — Switch buffer",
|
|
" /quit — Disconnect and exit",
|
|
" /help — This help",
|
|
" Plain text sends to current target.",
|
|
}
|
|
|
|
for _, line := range help {
|
|
a.ui.AddStatus(line)
|
|
}
|
|
}
|
|
|
|
// pollLoop long-polls for messages in the background until stop is
|
|
// closed. A later /connect closes stop under a.mu as it replaces the
|
|
// client and the cursor, so the loop checks stop under a.mu too. A
|
|
// stopped loop then neither polls with the new client nor moves the
|
|
// new cursor.
|
|
func (a *App) pollLoop(stop <-chan struct{}) {
|
|
for {
|
|
a.mu.Lock()
|
|
|
|
select {
|
|
case <-stop:
|
|
a.mu.Unlock()
|
|
|
|
return
|
|
default:
|
|
}
|
|
|
|
client := a.client
|
|
lastQID := a.lastQID
|
|
a.mu.Unlock()
|
|
|
|
if client == nil {
|
|
return
|
|
}
|
|
|
|
result, err := client.PollMessages(
|
|
lastQID, pollTimeout,
|
|
)
|
|
if err != nil {
|
|
time.Sleep(pollRetry)
|
|
|
|
continue
|
|
}
|
|
|
|
// A poll sent before stop was closed answers for the earlier
|
|
// connection.
|
|
a.mu.Lock()
|
|
|
|
select {
|
|
case <-stop:
|
|
a.mu.Unlock()
|
|
|
|
return
|
|
default:
|
|
}
|
|
|
|
if result.LastID > 0 {
|
|
a.lastQID = result.LastID
|
|
}
|
|
|
|
a.mu.Unlock()
|
|
|
|
for i := range result.Messages {
|
|
a.handleServerMessage(&result.Messages[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleServerMessage(msg *api.Message) {
|
|
timestamp := a.formatTS(msg)
|
|
|
|
a.mu.Lock()
|
|
myNick := a.nick
|
|
a.mu.Unlock()
|
|
|
|
switch msg.Command {
|
|
case irc.CmdPrivmsg:
|
|
a.handlePrivmsgEvent(msg, timestamp, myNick)
|
|
case irc.CmdJoin:
|
|
a.handleJoinEvent(msg, timestamp)
|
|
case irc.CmdPart:
|
|
a.handlePartEvent(msg, timestamp, myNick)
|
|
case irc.CmdQuit:
|
|
a.handleQuitEvent(msg, timestamp)
|
|
case irc.CmdNick:
|
|
a.handleNickEvent(msg, timestamp, myNick)
|
|
case irc.CmdNotice:
|
|
a.handleNoticeEvent(msg, timestamp)
|
|
case irc.CmdTopic:
|
|
a.handleTopicEvent(msg, timestamp)
|
|
default:
|
|
a.handleDefaultEvent(msg, timestamp)
|
|
}
|
|
}
|
|
|
|
func (a *App) formatTS(msg *api.Message) string {
|
|
if msg.TS != "" {
|
|
return msg.ParseTS().UTC().Format(timeFormat)
|
|
}
|
|
|
|
return time.Now().Format(timeFormat)
|
|
}
|
|
|
|
func (a *App) handlePrivmsgEvent(
|
|
msg *api.Message, timestamp, myNick string,
|
|
) {
|
|
lines := msg.BodyLines()
|
|
text := strings.Join(lines, " ")
|
|
|
|
// A direct message shows in the buffer of the other user:
|
|
// the recipient of one the user sent, else the sender.
|
|
target := msg.To
|
|
if !strings.HasPrefix(target, "#") && msg.From != myNick {
|
|
target = msg.From
|
|
}
|
|
|
|
a.ui.AddLine(target, fmt.Sprintf(
|
|
"[gray]%s [green]<%s>[white] %s",
|
|
timestamp, msg.From, text,
|
|
))
|
|
}
|
|
|
|
func (a *App) handleJoinEvent(
|
|
msg *api.Message, timestamp string,
|
|
) {
|
|
if msg.To == "" {
|
|
return
|
|
}
|
|
|
|
a.ui.AddLine(msg.To, fmt.Sprintf(
|
|
"[gray]%s [yellow]*** %s has joined %s",
|
|
timestamp, msg.From, msg.To,
|
|
))
|
|
}
|
|
|
|
// handlePartEvent shows a part in the channel's buffer. The
|
|
// user's own part does not mark the channel unread: the user
|
|
// has just left it.
|
|
func (a *App) handlePartEvent(
|
|
msg *api.Message, timestamp, myNick string,
|
|
) {
|
|
if msg.To == "" {
|
|
return
|
|
}
|
|
|
|
lines := msg.BodyLines()
|
|
reason := strings.Join(lines, " ")
|
|
|
|
line := fmt.Sprintf(
|
|
"[gray]%s [yellow]*** %s has left %s",
|
|
timestamp, msg.From, msg.To,
|
|
)
|
|
|
|
if reason != "" {
|
|
line += " (" + reason + ")"
|
|
}
|
|
|
|
if msg.From == myNick {
|
|
a.ui.AddLineAsRead(msg.To, line)
|
|
|
|
return
|
|
}
|
|
|
|
a.ui.AddLine(msg.To, line)
|
|
}
|
|
|
|
func (a *App) handleQuitEvent(
|
|
msg *api.Message, timestamp string,
|
|
) {
|
|
lines := msg.BodyLines()
|
|
reason := strings.Join(lines, " ")
|
|
|
|
if reason != "" {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[gray]%s [yellow]*** %s has quit (%s)",
|
|
timestamp, msg.From, reason,
|
|
))
|
|
} else {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[gray]%s [yellow]*** %s has quit",
|
|
timestamp, msg.From,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (a *App) handleNickEvent(
|
|
msg *api.Message, timestamp, myNick string,
|
|
) {
|
|
lines := msg.BodyLines()
|
|
|
|
newNick := ""
|
|
if len(lines) > 0 {
|
|
newNick = lines[0]
|
|
}
|
|
|
|
if msg.From == myNick && newNick != "" {
|
|
a.mu.Lock()
|
|
a.nick = newNick
|
|
|
|
target := a.target
|
|
a.mu.Unlock()
|
|
|
|
a.ui.SetStatus(newNick, target, "connected")
|
|
}
|
|
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[gray]%s [yellow]*** %s is now known as %s",
|
|
timestamp, msg.From, newNick,
|
|
))
|
|
}
|
|
|
|
func (a *App) handleNoticeEvent(
|
|
msg *api.Message, timestamp string,
|
|
) {
|
|
lines := msg.BodyLines()
|
|
text := strings.Join(lines, " ")
|
|
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[gray]%s [magenta]--%s-- %s",
|
|
timestamp, msg.From, text,
|
|
))
|
|
}
|
|
|
|
func (a *App) handleTopicEvent(
|
|
msg *api.Message, timestamp string,
|
|
) {
|
|
if msg.To == "" {
|
|
return
|
|
}
|
|
|
|
lines := msg.BodyLines()
|
|
text := strings.Join(lines, " ")
|
|
|
|
a.ui.AddLine(msg.To, fmt.Sprintf(
|
|
"[gray]%s [cyan]*** %s set topic: %s",
|
|
timestamp, msg.From, text,
|
|
))
|
|
}
|
|
|
|
func (a *App) handleDefaultEvent(
|
|
msg *api.Message, timestamp string,
|
|
) {
|
|
lines := msg.BodyLines()
|
|
text := strings.Join(lines, " ")
|
|
|
|
if text != "" {
|
|
a.ui.AddStatus(fmt.Sprintf(
|
|
"[gray]%s [white][%s] %s",
|
|
timestamp, msg.Command, text,
|
|
))
|
|
}
|
|
}
|