Browse Source

Add notice, ctcp support, improve consistency, basic functionality

master
Tyler Sommer 7 months ago
parent
commit
1384397182
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
15 changed files with 848 additions and 360 deletions
  1. +217
    -52
      builtin_cmds.go
  2. +12
    -1
      cmd/squirssi/main.go
  3. +1
    -1
      colors/irc.go
  4. +1
    -2
      go.mod
  5. +2
    -2
      go.sum
  6. +19
    -19
      handlers.go
  7. +123
    -76
      handlers_irc.go
  8. +129
    -0
      logging.go
  9. +3
    -0
      plugin.go
  10. +41
    -177
      server.go
  11. +1
    -1
      widget/chatpane.go
  12. +11
    -0
      window.go
  13. +75
    -0
      window_history.go
  14. +73
    -0
      window_tabber.go
  15. +140
    -29
      window_writer.go

+ 217
- 52
builtin_cmds.go View File

@ -11,43 +11,148 @@ import (
type Command func(*Server, []string)
func modeHandler(mode string) Command {
return func(srv *Server, args []string) {
args = append(append(append([]string{}, args[:1]...), mode), args[1:]...)
modeChange(srv, args)
}
}
var builtInsOrdered = []string{
"exit",
"connect",
"disconnect",
"w",
"wc",
"join",
"part",
"topic",
"whois",
"names",
"nick",
"me",
"msg",
"ctcp",
"notice",
"kick",
"mode",
"ban",
"unban",
"op",
"deop",
"voice",
"devoice",
"mute",
"unmute",
"echo",
"raw",
"help",
}
var builtIns = map[string]Command{
"exit": exitProgram,
"w": selectWindow,
"wc": closeWindow,
"join": joinChannel,
"part": partChannel,
"mode": modeChange,
"topic": topicChange,
"whois": whoisNick,
"names": namesChannel,
"nick": changeNick,
"me": actionMessage,
"msg": msgTarget,
"help": helpCmd,
"?": helpCmd,
"exit": exitProgram,
"w": selectWindow,
"wc": closeWindow,
"join": joinChannel,
"part": partChannel,
"topic": topicChange,
"whois": whoisNick,
"names": namesChannel,
"nick": changeNick,
"me": actionTarget,
"msg": msgTarget,
"ctcp": ctcpTarget,
"notice": noticeTarget,
"kick": kickTarget,
"mode": modeChange,
"ban": modeHandler("+b"),
"unban": modeHandler("-b"),
"op": modeHandler("+o"),
"deop": modeHandler("-o"),
"voice": modeHandler("+v"),
"devoice": modeHandler("-v"),
"mute": modeHandler("+q"),
"unmute": modeHandler("-q"),
"connect": connectServer,
"disconnect": disconnectServer,
"quit": disconnectServer,
"echo": func(srv *Server, args []string) {
win := srv.WindowManager.Active()
win := srv.wm.Active()
_, _ = win.WriteString(strings.Join(args[1:], " "))
},
"raw": func(srv *Server, args []string) {
srv.IRCDoAsync(func(conn *irc.Connection) error {
conn.SendRaw(strings.Join(args[1:], " "))
win := srv.WindowManager.Active()
win := srv.wm.Active()
if win != nil {
_, _ = win.WriteString("-> " + strings.Join(args[1:], " "))
WriteRaw(win, "-> " + strings.Join(args[1:], " "))
}
return nil
})
},
}
var builtInDescriptions = map[string]string{
"help": "Prints this help text.",
"exit": "Exits squirssi.",
"w": "Switches to the given window by number.",
"wc": "Closes the given window by number, or the currently active window.",
"join": "Attempts to join the given channel.",
"part": "Parts the given channel.",
"topic": "Sets the topic for the given channel, or the currently active window.",
"whois": "Runs a WHOIS query on the given nickname.",
"names": "Runs a NAMES query on the given channel.",
"nick": "Changes the current nickname.",
"me": "Performs an action message in the current window.",
"msg": "Sends a message to the given target.",
"ctcp": "Sends a CTCP query to the given target.",
"notice": "Sends a NOTICE to the given target.",
"kick": "Kicks a user from the given channel.",
"mode": "Sets mode on a channel or the current user.",
"ban": "Bans (+b) a user from the given channel.",
"unban": "Unbans (-b) a user from the given channel.",
"op": "Ops (+o) a user on the given channel.",
"deop": "Deops (-o) a user on the given channel.",
"voice": "Voices (+v) a user on the given channel.",
"devoice": "Devoices (-v) a user on the given channel.",
"mute": "Mutes (+q) a user on the given channel.",
"unmute": "Unmutes (-q) a user on the given channel.",
"connect": "Connects to the configured IRC server.",
"disconnect": "Disconnects from the connected IRC server.",
"echo": "Writes any arguments given to the currently active window.",
"raw": "Sends a raw IRC command.",
}
func helpCmd(srv *Server, args []string) {
win := srv.wm.Active()
if win == nil {
return
}
if len(args) > 1 {
if desc, ok := builtInDescriptions[args[1]]; ok {
WriteHelpGeneric(win, "Help information for "+args[1])
WriteHelp(win, args[1], desc)
} else {
WriteHelpGeneric(win, "Unknown command: "+args[1])
}
return
}
// print all help.
WriteHelpGeneric(win, "[Available commands:](mod:bold)")
for _, cmd := range builtInsOrdered {
WriteHelp(win, cmd, builtInDescriptions[cmd])
}
}
func connectServer(srv *Server, _ []string) {
go func() {
if err := srv.irc.Connect(); err != nil {
logrus.Errorln("unable to connect:", err)
logrus.Errorln("Unable to connect:", err)
}
}()
}
@ -55,7 +160,7 @@ func connectServer(srv *Server, _ []string) {
func disconnectServer(srv *Server, _ []string) {
go func() {
if err := srv.irc.Disconnect(); err != nil {
logrus.Errorln("unable to disconnect:", err)
logrus.Errorln("Unable to disconnect:", err)
}
}()
}
@ -70,9 +175,9 @@ func exitProgram(srv *Server, _ []string) {
func topicChange(srv *Server, args []string) {
if len(args) < 2 || !strings.HasPrefix("#", args[1]) {
win := srv.WindowManager.Active()
win := srv.wm.Active()
if win == nil || !strings.HasPrefix(win.Title(), "#") {
logrus.Warnln("topicChange: couldnt determine current channel")
logrus.Warnln("topic: unable to determine current channel")
return
}
args = append(append([]string{}, args[0], win.Title()), args[1:]...)
@ -92,14 +197,36 @@ func topicChange(srv *Server, args []string) {
})
}
func kickTarget(srv *Server, args []string) {
if len(args) < 2 || !strings.HasPrefix(args[1], "#") {
win := srv.wm.Active()
t := ""
if win == nil || win.Title() == "status" {
t = srv.CurrentNick()
} else {
t = win.Title()
}
args = append(append([]string{}, args[0], t), args[1:]...)
}
target := args[1]
if len(target) > 0 && target[0] != '#' {
logrus.Warnln("kick: unable to determine current channel")
return
}
nick := args[2]
msg := strings.Join(args[3:], " ")
srv.IRCDoAsync(func(conn *irc.Connection) error {
conn.Kick(nick, target, msg)
return nil
})
}
func modeChange(srv *Server, args []string) {
if len(args) < 2 || strings.HasPrefix(args[1], "+") || strings.HasPrefix(args[1], "-") {
win := srv.WindowManager.Active()
win := srv.wm.Active()
t := ""
if win == nil || win.Title() == "status" {
srv.mu.RLock()
t = srv.currentNick
srv.mu.RUnlock()
t = srv.CurrentNick()
} else {
t = win.Title()
}
@ -115,35 +242,33 @@ func modeChange(srv *Server, args []string) {
func selectWindow(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("selectWindow: expected one argument")
logrus.Warnln("window: expected one argument")
return
}
var err error
ch, err := strconv.Atoi(args[1])
if err != nil {
logrus.Warnln("selectWindow: expected first argument to be an integer")
logrus.Warnln("window: expected first argument to be an integer")
return
}
srv.WindowManager.SelectIndex(ch)
srv.wm.SelectIndex(ch)
}
func closeWindow(srv *Server, args []string) {
var ch int
if len(args) < 2 {
ch = srv.WindowManager.ActiveIndex()
ch = srv.wm.ActiveIndex()
} else {
var err error
ch, err = strconv.Atoi(args[1])
if err != nil {
logrus.Warnln("closeWindow: expected first argument to be an integer")
logrus.Warnln("window_close: expected first argument to be an integer")
return
}
}
win := srv.WindowManager.Index(ch)
win := srv.wm.Index(ch)
if ch, ok := win.(*Channel); ok {
srv.mu.RLock()
myNick := srv.currentNick
srv.mu.RUnlock()
myNick := srv.CurrentNick()
if ch.HasUser(myNick) {
srv.IRCDoAsync(func(conn *irc.Connection) error {
conn.Part(win.Title())
@ -151,12 +276,12 @@ func closeWindow(srv *Server, args []string) {
})
}
}
srv.WindowManager.CloseIndex(ch)
srv.wm.CloseIndex(ch)
}
func joinChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("joinChannel: expected one argument")
logrus.Warnln("join: expected one argument")
return
}
srv.IRCDoAsync(func(conn *irc.Connection) error {
@ -167,7 +292,7 @@ func joinChannel(srv *Server, args []string) {
func partChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("partChannel: expected one argument")
logrus.Warnln("part: expected one argument")
return
}
srv.IRCDoAsync(func(conn *irc.Connection) error {
@ -178,7 +303,7 @@ func partChannel(srv *Server, args []string) {
func whoisNick(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("whoisNick: expected one argument")
logrus.Warnln("whois: expected one argument")
return
}
srv.IRCDoAsync(func(conn *irc.Connection) error {
@ -189,13 +314,13 @@ func whoisNick(srv *Server, args []string) {
func namesChannel(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("namesChannel: expected one argument")
logrus.Warnln("names: expected one argument")
return
}
channel := args[1]
win := srv.WindowManager.Named(channel)
win := srv.wm.Named(channel)
if win == nil {
logrus.Warnln("namesChannel: no window named", channel)
logrus.Warnln("names: no window named", channel)
return
}
irc353Handler := event.HandlerFunc(func(ev *event.Event) {
@ -222,7 +347,7 @@ func namesChannel(srv *Server, args []string) {
func changeNick(srv *Server, args []string) {
if len(args) < 2 {
logrus.Warnln("changeNick: expected one argument")
logrus.Warnln("nick: expected one argument")
return
}
srv.IRCDoAsync(func(conn *irc.Connection) error {
@ -231,9 +356,9 @@ func changeNick(srv *Server, args []string) {
})
}
func actionMessage(srv *Server, args []string) {
func actionTarget(srv *Server, args []string) {
message := strings.Join(args[1:], " ")
window := srv.WindowManager.Active()
window := srv.wm.Active()
if window == nil || window.Title() == "status" {
return
}
@ -241,15 +366,13 @@ func actionMessage(srv *Server, args []string) {
conn.Action(window.Title(), message)
return nil
})
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
myNick := MyNick(srv.CurrentNick())
WriteAction(window, myNick, MyMessage(message))
}
func msgTarget(srv *Server, args []string) {
if len(args) < 3 {
logrus.Warnln("msgTarget: expects at least 2 arguments")
logrus.Warnln("msg: expected at least 2 arguments")
return
}
target := args[1]
@ -261,24 +384,66 @@ func msgTarget(srv *Server, args []string) {
conn.Privmsg(target, message)
return nil
})
window := srv.WindowManager.Named(target)
window := srv.wm.Named(target)
if !strings.HasPrefix(target, "#") {
// direct message!
if window == nil {
dm := &DirectMessage{
newBufferedWindow(target, srv.events),
}
srv.WindowManager.Append(dm)
srv.wm.Append(dm)
window = dm
}
}
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
myNick := MyNick(srv.CurrentNick())
if window == nil {
// no window for this but we might still have sent the message, so write it to the status window
window = srv.WindowManager.Index(0)
window = srv.wm.Index(0)
message = target + " -> " + message
}
WritePrivmsg(window, myNick, MyMessage(message))
}
func noticeTarget(srv *Server, args []string) {
if len(args) < 3 {
logrus.Warnln("notice: expected at least 2 arguments")
return
}
target := args[1]
if target == "status" {
return
}
message := strings.Join(args[2:], " ")
srv.IRCDoAsync(func(conn *irc.Connection) error {
conn.Notice(target, message)
return nil
})
window := srv.wm.Named(target)
if window == nil {
// no window for this but we might still have sent the message, so write it to the status window
window = srv.wm.Index(0)
}
WriteNotice(window, SomeTarget(target, srv.CurrentNick()), true, message)
}
func ctcpTarget(srv *Server, args []string) {
if len(args) < 3 {
logrus.Warnln("ctcp: expected at least 2 arguments")
return
}
target := args[1]
if target == "status" {
return
}
message := strings.Join(args[2:], " ")
srv.IRCDoAsync(func(conn *irc.Connection) error {
conn.SendRawf("PRIVMSG %s :\x01%s\x01", target, message)
return nil
})
window := srv.wm.Named(target)
if window == nil {
// no window for this but we might still have sent the message, so write it to the status window
window = srv.wm.Index(0)
}
WriteCTCP(window, SomeTarget(target, srv.CurrentNick()), true, message)
}

+ 12
- 1
cmd/squirssi/main.go View File

@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"os"
"sort"
"strings"
"code.dopame.me/veonik/squircy3/cli"
@ -42,6 +43,8 @@ var rootDir string
var extraPlugins stringsFlag
var logLevel = stringLevel(logrus.InfoLevel)
var Squircy3Version = "SNAPSHOT"
func init() {
flag.StringVar(&rootDir, "root", "~/.squirssi", "path to folder containing squirssi data")
flag.Var(&logLevel, "log-level", "controls verbosity of logging output")
@ -67,15 +70,23 @@ func init() {
rootDir = bp
}
func printPluginsLoaded(plugins *plugin.Manager) {
pls := plugins.Loaded()
sort.Strings(pls)
logrus.Infoln("Loaded plugins:", strings.Join(pls, ", "))
}
type Manager struct {
*cli.Manager
}
func (m *Manager) Start() error {
func (m *Manager) Start() (err error) {
if err := m.Manager.Start(); err != nil {
return err
}
logrus.Infof("Starting squirssi (version %s, built with squircy3-%s)", squirssi.Version, Squircy3Version)
plugins := m.Plugins()
printPluginsLoaded(plugins)
srv, err := squirssi.FromPlugins(plugins)
if err != nil {
return err


+ 1
- 1
colors/irc.go View File

@ -35,7 +35,7 @@ var ircToUIMap = map[IRC]ui.Color{
IRCBlue: Blue,
IRCGreen: Green,
IRCRed: Red,
IRCBrown: Maroon,
IRCBrown: Red4,
IRCMagenta: Magenta1,
IRCOrange: Orange1,
IRCYellow: Yellow,


+ 1
- 2
go.mod View File

@ -3,8 +3,7 @@ module code.dopame.me/veonik/squirssi
go 1.14
require (
code.dopame.me/veonik/squircy3 v0.0.0-20200926012754-a6ef0aa7a541
github.com/dop251/goja v0.0.0-20200526165454-f1752421c432
code.dopame.me/veonik/squircy3 v0.9.0
github.com/gizak/termui/v3 v3.1.0
github.com/mattn/go-runewidth v0.0.9
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7


+ 2
- 2
go.sum View File

@ -1,5 +1,5 @@
code.dopame.me/veonik/squircy3 v0.0.0-20200926012754-a6ef0aa7a541 h1:hTCz0TCr2kvSCKiUYkIxGFmsOondPRu2YF+H8rrgFdk=
code.dopame.me/veonik/squircy3 v0.0.0-20200926012754-a6ef0aa7a541/go.mod h1:D0PZ58ANI0zuFIgCLrhmBSZQmLeKFd0vL+6YP2AamYc=
code.dopame.me/veonik/squircy3 v0.9.0 h1:Ggtq0TaIhkW0N+XcfA+7YaQ4E0B0RemkjsK2mBCob80=
code.dopame.me/veonik/squircy3 v0.9.0/go.mod h1:D0PZ58ANI0zuFIgCLrhmBSZQmLeKFd0vL+6YP2AamYc=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=


+ 19
- 19
handlers.go View File

@ -47,18 +47,18 @@ func onUIKeyPress(srv *Server, key string) {
srv.RenderOnly(InputTextBox)
case "<PageUp>":
srv.mu.RLock()
h := srv.pageHeight
h := srv.pageHeight - 2
srv.mu.RUnlock()
srv.WindowManager.ScrollOffset(-h)
srv.wm.ScrollOffset(-h)
case "<PageDown>":
srv.mu.RLock()
h := srv.pageHeight
h := srv.pageHeight - 2
srv.mu.RUnlock()
srv.WindowManager.ScrollOffset(h)
srv.wm.ScrollOffset(h)
case "<Home>":
srv.WindowManager.ScrollTo(0)
srv.wm.ScrollTo(0)
case "<End>":
srv.WindowManager.ScrollTo(math.MaxInt32)
srv.wm.ScrollTo(math.MaxInt32)
case "<Space>":
srv.inputTextBox.Append(" ")
srv.RenderOnly(InputTextBox)
@ -69,31 +69,31 @@ func onUIKeyPress(srv *Server, key string) {
srv.inputTextBox.DeleteNext()
srv.RenderOnly(InputTextBox)
case "<C-5>":
srv.WindowManager.SelectNext()
srv.wm.SelectNext()
case "<Escape>":
srv.WindowManager.SelectPrev()
srv.wm.SelectPrev()
case "<Up>":
win := srv.WindowManager.Active()
win := srv.wm.Active()
if win == nil {
return
}
cur := srv.inputTextBox.Consume()
if cur.Text != "" {
srv.HistoryManager.Insert(win, cur)
srv.history.Insert(win, cur)
}
msg := srv.HistoryManager.Previous(win)
msg := srv.history.Previous(win)
srv.inputTextBox.Set(msg)
srv.RenderOnly(InputTextBox)
case "<Down>":
win := srv.WindowManager.Active()
win := srv.wm.Active()
if win == nil {
return
}
cur := srv.inputTextBox.Consume()
if cur.Text != "" {
srv.HistoryManager.Insert(win, cur)
srv.history.Insert(win, cur)
}
msg := srv.HistoryManager.Next(win)
msg := srv.history.Next(win)
srv.inputTextBox.Set(msg)
srv.RenderOnly(InputTextBox)
case "<Left>":
@ -103,7 +103,7 @@ func onUIKeyPress(srv *Server, key string) {
srv.inputTextBox.CursorNext()
srv.RenderOnly(InputTextBox)
case "<Tab>":
win := srv.WindowManager.Active()
win := srv.wm.Active()
if ch, ok := win.(*Channel); ok {
var tabbed string
if srv.tabber.Active() {
@ -116,8 +116,8 @@ func onUIKeyPress(srv *Server, key string) {
srv.RenderOnly(InputTextBox)
case "<Enter>":
in := srv.inputTextBox.Consume()
active := srv.WindowManager.ActiveIndex()
channel := srv.WindowManager.Active()
active := srv.wm.ActiveIndex()
channel := srv.wm.Active()
if channel == nil {
return
}
@ -126,7 +126,8 @@ func onUIKeyPress(srv *Server, key string) {
srv.RenderOnly(MainWindow, InputTextBox)
return
}
defer srv.HistoryManager.Append(channel, in)
defer srv.RenderOnly(InputTextBox)
defer srv.history.Append(channel, in)
switch in.Kind {
case widget.ModeCommand:
args := strings.Split(in.Text, " ")
@ -137,7 +138,6 @@ func onUIKeyPress(srv *Server, key string) {
logrus.Warnln("no command named:", c)
}
case widget.ModeMessage:
srv.RenderOnly(InputTextBox)
if active == 0 {
// status window doesn't accept messages
return


+ 123
- 76
handlers_irc.go View File

@ -15,6 +15,7 @@ func bindIRCHandlers(srv *Server, events *event.Dispatcher) {
events.Bind("irc.CONNECT", HandleIRCEvent(srv, onIRCConnect))
events.Bind("irc.DISCONNECT", HandleIRCEvent(srv, onIRCDisconnect))
events.Bind("irc.PRIVMSG", HandleIRCEvent(srv, onIRCPrivmsg))
events.Bind("irc.NOTICE", HandleIRCEvent(srv, onIRCNotice))
events.Bind("irc.CTCP_ACTION", HandleIRCEvent(srv, onIRCAction))
events.Bind("irc.JOIN", HandleIRCEvent(srv, onIRCJoin))
events.Bind("irc.PART", HandleIRCEvent(srv, onIRCPart))
@ -23,6 +24,7 @@ func bindIRCHandlers(srv *Server, events *event.Dispatcher) {
events.Bind("irc.PART", HandleIRCEvent(srv, onIRCNames))
events.Bind("irc.KICK", HandleIRCEvent(srv, onIRCNames))
events.Bind("irc.NICK", HandleIRCEvent(srv, onIRCNick))
events.Bind("irc.433", HandleIRCEvent(srv, onIRC433))
events.Bind("irc.353", HandleIRCEvent(srv, onIRC353))
events.Bind("irc.366", HandleIRCEvent(srv, onIRC366))
events.Bind("irc.QUIT", HandleIRCEvent(srv, onIRCQuit))
@ -39,6 +41,14 @@ func bindIRCHandlers(srv *Server, events *event.Dispatcher) {
for _, code := range whoisCodes {
events.Bind(code, HandleIRCEvent(srv, onIRCWhois))
}
miscCodes := []string{
"irc.001", "irc.002", "irc.003", "irc.250", "irc.251",
"irc.252", "irc.253", "irc.254", "irc.255", "irc.265",
"irc.266", "irc.375", "irc.372", "irc.376",
}
for _, code := range miscCodes {
events.Bind(code, HandleIRCEvent(srv, onIRCMessage))
}
events.Bind("debug.IRC", event.HandlerFunc(handleIRCDebugEvent))
}
@ -76,20 +86,43 @@ func normalizeDebugEvent(ev *event.Event) *IRCEvent {
}
var debugIgnore = map[string]struct{}{
"PRIVMSG": {},
"CTCP_ACTION": {},
"TOPIC": {},
"MODE": {},
"KICK": {},
"NICK": {},
"QUIT": {},
"JOIN": {},
"PART": {},
"366": {},
"353": {},
"324": {},
"331": {},
"332": {},
"CTCP_ACTION": {},
"CTCP_TIME": {},
"CTCP_VERSION": {},
"CTCP_USERINFO": {},
"CTCP_CLIENTINFO": {},
"CTCP_PING": {},
"PRIVMSG": {},
"NOTICE": {},
"TOPIC": {},
"MODE": {},
"KICK": {},
"NICK": {},
"QUIT": {},
"JOIN": {},
"PART": {},
"366": {},
"353": {},
"324": {},
"331": {},
"332": {},
"001": {},
"002": {},
"003": {},
"251": {},
"252": {},
"253": {},
"254": {},
"255": {},
"250": {},
"265": {},
"266": {},
"375": {},
"372": {},
"376": {},
"433": {},
}
func handleIRCDebugEvent(ev *event.Event) {
@ -97,7 +130,7 @@ func handleIRCDebugEvent(ev *event.Event) {
if _, ok := debugIgnore[nev.Code]; ok {
return
}
logrus.Debugf("irc.%s - %s => %s", nev.Code, nev.Target, strings.Join(nev.Args[1:], " "))
logrus.Debugf("irc.%s - T(%s) N(%s) => %s", nev.Code, nev.Target, nev.Nick, strings.Join(nev.Args[1:], " "))
}
func NormalizeIRCEvent(ev *event.Event) *IRCEvent {
@ -128,7 +161,7 @@ func HandleIRCEvent(srv *Server, h IRCEventHandler) event.Handler {
func onIRC324(srv *Server, ev *IRCEvent) {
modes := strings.Join(ev.Args[2:], " ")
win := srv.WindowManager.Named(ev.Args[1])
win := srv.wm.Named(ev.Args[1])
blankBefore := false
if ch, ok := win.(*Channel); ok {
ch.mu.Lock()
@ -150,7 +183,7 @@ func onIRC324(srv *Server, ev *IRCEvent) {
func onIRC331(srv *Server, ev *IRCEvent) {
target := ev.Args[1]
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if ch, ok := win.(*Channel); ok {
ch.mu.Lock()
ch.topic = ""
@ -161,7 +194,7 @@ func onIRC331(srv *Server, ev *IRCEvent) {
func onIRC332(srv *Server, ev *IRCEvent) {
target := ev.Args[1]
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if ch, ok := win.(*Channel); ok {
topic := strings.Join(ev.Args[2:], " ")
ch.mu.Lock()
@ -173,9 +206,7 @@ func onIRC332(srv *Server, ev *IRCEvent) {
func onIRCConnect(srv *Server, _ *IRCEvent) {
srv.IRCDoAsync(func(conn *irc.Connection) error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = conn.GetNick()
srv.setCurrentNick(conn.GetNick())
conn.AddCallback("*", func(ev *irc2.Event) {
srv.events.Emit("debug.IRC", map[string]interface{}{
"source": ev,
@ -187,25 +218,24 @@ func onIRCConnect(srv *Server, _ *IRCEvent) {
func onIRCDisconnect(srv *Server, _ *IRCEvent) {
logrus.Infoln("*** Disconnected")
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = ""
srv.setCurrentNick("")
}
func onIRCMode(srv *Server, ev *IRCEvent) {
target := ev.Target
nick := SomeNick(ev.Nick)
mode := strings.Join(ev.Args[1:], " ")
srv.mu.RLock()
if ev.Nick == srv.currentNick {
currNick := srv.CurrentNick()
if ev.Nick == currNick {
nick.me = true
} else if target == currNick {
nick = MyNick(target)
}
srv.mu.RUnlock()
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if win != nil {
WriteMode(win, nick, mode)
} else {
WriteMode(srv.WindowManager.Index(0), nick, mode)
WriteMode(srv.wm.Index(0), nick, mode)
}
}
@ -213,12 +243,10 @@ func onIRCTopic(srv *Server, ev *IRCEvent) {
target := ev.Target
nick := SomeNick(ev.Nick)
topic := strings.Join(ev.Args[1:], " ")
srv.mu.RLock()
if ev.Nick == srv.currentNick {
if ev.Nick == srv.CurrentNick() {
nick.me = true
}
srv.mu.RUnlock()
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if win != nil {
WriteTopic(win, nick, topic)
} else {
@ -226,26 +254,30 @@ func onIRCTopic(srv *Server, ev *IRCEvent) {
}
}
func onIRC433(srv *Server, _ *IRCEvent) {
srv.IRCDoAsync(func(conn *irc.Connection) error {
srv.setCurrentNick(conn.GetNick())
return nil
})
}
func onIRCNick(srv *Server, ev *IRCEvent) {
nick := SomeNick(ev.Nick)
newNick := ev.Message
srv.mu.Lock()
if ev.Nick == srv.currentNick {
newNick := SomeNick(ev.Message)
if ev.Nick == srv.CurrentNick() {
nick.me = true
srv.currentNick = newNick
newNick.me = true
srv.setCurrentNick(newNick.string)
}
srv.mu.Unlock()
WriteNick(srv.WindowManager, nick, newNick)
WriteNick(srv.wm, nick, newNick)
}
func onIRCKick(srv *Server, ev *IRCEvent) {
channel := ev.Target
kicked := SomeNick(ev.Args[1])
srv.mu.RLock()
if kicked.string == srv.currentNick {
if kicked.string == srv.CurrentNick() {
kicked.me = true
}
srv.mu.RUnlock()
if kicked.me {
go func() {
<-time.After(2 * time.Second)
@ -257,7 +289,7 @@ func onIRCKick(srv *Server, ev *IRCEvent) {
}
}()
}
win := srv.WindowManager.Named(channel)
win := srv.wm.Named(channel)
if win == nil {
logrus.Errorln("received kick with no Window:", channel, ev.Message, ev.Nick)
return
@ -277,7 +309,7 @@ func onIRC353(srv *Server, ev *IRCEvent) {
// NAMES
chanName := ev.Args[2]
nicks := strings.Split(ev.Args[3], " ")
win := srv.WindowManager.Named(chanName)
win := srv.wm.Named(chanName)
if win == nil {
logrus.Warnln("received NAMES for channel with no window:", chanName)
return
@ -290,7 +322,7 @@ func onIRC353(srv *Server, ev *IRCEvent) {
func onIRC366(srv *Server, ev *IRCEvent) {
// END NAMES
chanName := ev.Args[1]
win := srv.WindowManager.Named(chanName)
win := srv.wm.Named(chanName)
if win == nil {
logrus.Warnln("received END NAMES for channel with no window:", chanName)
return
@ -304,7 +336,7 @@ func onIRC366(srv *Server, ev *IRCEvent) {
defer namesCache.Unlock()
ch.SetUsers(namesCache.values[chanName])
delete(namesCache.values, chanName)
srv.WindowManager.events.Emit("ui.DIRTY", nil)
srv.wm.events.Emit("ui.DIRTY", nil)
}
func onIRCError(srv *Server, ev *IRCEvent) {
@ -314,22 +346,25 @@ func onIRCError(srv *Server, ev *IRCEvent) {
} else {
kind = ev.Target
}
win := srv.WindowManager.Named(kind)
win := srv.wm.Named(kind)
WriteError(win, kind, ev.Message)
}
func onIRCWhois(srv *Server, ev *IRCEvent) {
nick := ev.Args[1]
data := ev.Args[2:]
win := srv.WindowManager.Named(nick)
win := srv.wm.Named(nick)
WriteWhois(win, nick, data)
}
func onIRCMessage(srv *Server, ev *IRCEvent) {
win := srv.wm.Index(0)
WriteMessage(win, strings.Join(ev.Args[1:], " "))
}
func onIRCNames(srv *Server, ev *IRCEvent) {
if ev.Code == "PART" || ev.Code == "KICK" {
srv.mu.RLock()
myNick := srv.currentNick
srv.mu.RUnlock()
myNick := srv.CurrentNick()
if ev.Nick == myNick {
// dont bother trying to get names when we are the one leaving
return
@ -346,23 +381,21 @@ func onIRCNames(srv *Server, ev *IRCEvent) {
func onIRCJoin(srv *Server, ev *IRCEvent) {
target := ev.Target
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
nick := SomeNick(ev.Nick)
srv.mu.RLock()
if ev.Nick == srv.currentNick {
if ev.Nick == srv.CurrentNick() {
nick.me = true
}
srv.mu.RUnlock()
if win == nil {
ch := &Channel{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []User{},
usersIndexed: make(map[string]int),
}
srv.WindowManager.Append(ch)
srv.wm.Append(ch)
win = ch
if nick.me {
srv.WindowManager.SelectIndex(srv.WindowManager.Len() - 1)
srv.wm.SelectIndex(srv.wm.Len() - 1)
modeChange(srv, []string{"mode"})
}
}
@ -375,12 +408,10 @@ func onIRCJoin(srv *Server, ev *IRCEvent) {
func onIRCPart(srv *Server, ev *IRCEvent) {
target := ev.Target
nick := SomeNick(ev.Nick)
win := srv.WindowManager.Named(target)
srv.mu.RLock()
if ev.Nick == srv.currentNick {
win := srv.wm.Named(target)
if ev.Nick == srv.CurrentNick() {
nick.me = true
}
srv.mu.RUnlock()
if win == nil {
if !nick.me {
// dont bother logging if we are the ones leaving
@ -398,22 +429,20 @@ func onIRCAction(srv *Server, ev *IRCEvent) {
direct := false
target := ev.Target
nick := ev.Nick
srv.mu.RLock()
myNick := MyNick(srv.currentNick)
srv.mu.RUnlock()
myNick := MyNick(srv.CurrentNick())
if target == myNick.string {
// its a direct message!
direct = true
target = nick
}
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if win == nil {
if !direct {
logrus.Warnln("received action message with no Window:", target, ev.Message, nick)
return
} else {
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.WindowManager.Append(ch)
srv.wm.Append(ch)
win = ch
}
}
@ -425,22 +454,20 @@ func onIRCPrivmsg(srv *Server, ev *IRCEvent) {
direct := false
target := ev.Target
nick := ev.Nick
srv.mu.RLock()
myNick := MyNick(srv.currentNick)
srv.mu.RUnlock()
myNick := MyNick(srv.CurrentNick())
if target == myNick.string {
// its a direct message!
direct = true
target = nick
}
win := srv.WindowManager.Named(target)
win := srv.wm.Named(target)
if win == nil {
if !direct {
logrus.Warnln("received message with no Window:", target, ev.Message, nick)
return
} else {
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.WindowManager.Append(ch)
srv.wm.Append(ch)
win = ch
}
}
@ -448,13 +475,33 @@ func onIRCPrivmsg(srv *Server, ev *IRCEvent) {
WritePrivmsg(win, SomeNick(nick), msg)
}
func onIRCNotice(srv *Server, ev *IRCEvent) {
me := srv.CurrentNick()
target := SomeTarget(ev.Target, me)
// "*" is used by at least Freenode when you don't yet have a nick.
if target.string == "*" {
target.me = true
}
if target.me {
target = SomeTarget(ev.Nick, me)
}
win := srv.wm.Named(target.string)
if win == nil {
win = srv.wm.Index(0)
}
if strings.Contains(ev.Message, "\x01") {
WriteCTCP(win, target, false, ev.Message)
return
}
WriteNotice(win, target, false, ev.Message)
}
func onIRCQuit(srv *Server, ev *IRCEvent) {
nick := SomeNick(ev.Nick)
message := ev.Message
srv.mu.RLock()
if ev.Nick == srv.currentNick {
nick = MyNick(srv.currentNick)
if ev.Nick == srv.CurrentNick() {
nick.me = true
}
srv.mu.RUnlock()
WriteQuit(srv.WindowManager, nick, message)
WriteQuit(srv.wm, nick, message)
}

+ 129
- 0
logging.go View File

@ -0,0 +1,129 @@
package squirssi
import (
"fmt"
"os"
"sync"
"time"
"github.com/sirupsen/logrus"
)
// fileFormatter prints log messages formatted for output in files.
type fileFormatter struct{}
func (f *fileFormatter) Format(entry *logrus.Entry) ([]byte, error) {
lvl := ""
switch entry.Level {
case logrus.InfoLevel:
lvl = " INFO"
case logrus.DebugLevel:
lvl = "DEBUG"
case logrus.WarnLevel:
lvl = " WARN"
case logrus.ErrorLevel:
lvl = "ERROR"
case logrus.FatalLevel:
lvl = "FATAL"
case logrus.TraceLevel:
lvl = "TRACE"
case logrus.PanicLevel:
lvl = "PANIC"
}
return []byte(fmt.Sprintf("[%s] %s -> %s\n", time.Now().Format("2006-01-02 15:04:05"), lvl, entry.Message)), nil
}
// statusFormatter prints log messages formatted for the StatusWindow.
type statusFormatter struct{
levelPadding int
}
func (f *statusFormatter) Format(entry *logrus.Entry) ([]byte, error) {
lvl := ""
switch entry.Level {
case logrus.InfoLevel:
lvl = "[ INFO](fg:blue)"
case logrus.DebugLevel:
lvl = "[DEBUG](fg:white,bg:blue)"
case logrus.WarnLevel:
lvl = "[ WARN](fg:yellow)"
case logrus.ErrorLevel:
lvl = "[ERROR](fg:red)"
case logrus.FatalLevel:
lvl = "[FATAL](fg:white,bg:red,mod:bold)"
case logrus.TraceLevel:
lvl = "[TRACE](fg:white,mod:bold)"
case logrus.PanicLevel:
lvl = "[PANIC](fg:white,bg:red,mod:bold)"
}
return []byte(fmt.Sprintf(" %s[│](fg:grey) \x030%s\x03", lvl, entry.Message)), nil
}
// logFileWriterHook ensures that log messages are written to some output.
// This hook writes messages to stdout until Start is called, at which point
// the hook switches to writing to stderr.
// Because log messages are routed to the StatusWindow, it's possible for them
// to get lost if there is a fatal error preventing startup or if a runtime
// panic occurs.
type logFileWriterHook struct {
file *os.File
fmtr logrus.Formatter
started bool
mu sync.RWMutex
}
func newLogFileWriterHook() *logFileWriterHook {
return &logFileWriterHook{file: os.Stdout, fmtr: &fileFormatter{}}
}
func (h *logFileWriterHook) Start() {
h.mu.Lock()
defer h.mu.Unlock()
// switch to stderr once started
h.file = os.Stderr
h.started = true
}
func (h *logFileWriterHook) Fire(entry *logrus.Entry) error {
h.mu.RLock()
defer h.mu.RUnlock()
if h.started {
return nil
}
fire := func(f *os.File) {
line, err := h.fmtr.Format(entry)
if err != nil {
fmt.Fprintln(os.Stderr, "failed to format log message:", err)
return
}
if _, err = f.Write(line); err != nil {
fmt.Fprintln(os.Stderr, "failed to write log message:", err)
}
}
if h.started {
// todo: this is dead code, but writing to stderr messes up rendering
// todo: should this write to a real file instead? need to config such things
go fire(h.file)
} else {
// only block writes if the hook hasn't started yet.
// this is done assuming that log messages that occur before starting
// are usually happening right before the application is about to exit,
// so if we launch a goroutine we risk exiting the process before the
// write can complete.
fire(h.file)
}
return nil
}
func (h *logFileWriterHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
logrus.InfoLevel,
logrus.DebugLevel,
}
}

+ 3
- 0
plugin.go View File

@ -1,6 +1,8 @@
package squirssi
import (
"fmt"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"code.dopame.me/veonik/squircy3/plugin"
@ -30,6 +32,7 @@ func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (irc)", pluginName)
}
ircp.SetVersionString(fmt.Sprintf("squirssi v%s", Version))
srv, err := NewServer(ev, ircp)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed to initialize Server", pluginName)


+ 41
- 177
server.go View File

@ -3,7 +3,6 @@ package squirssi
import (
"fmt"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
@ -16,185 +15,30 @@ import (
"code.dopame.me/veonik/squirssi/widget"
)
type logFormatter struct{}
func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
lvl := ""
switch entry.Level {
case logrus.InfoLevel:
lvl = "[ INFO](fg:blue)"
case logrus.DebugLevel:
lvl = "[DEBUG](fg:white,bg:blue)"
case logrus.WarnLevel:
lvl = "[ WARN](fg:yellow)"
case logrus.ErrorLevel:
lvl = "[ERROR](fg:red)"
case logrus.FatalLevel:
lvl = "[FATAL](fg:white,bg:red,mod:bold)"
case logrus.TraceLevel:
lvl = "[TRACE](fg:white,mod:bold)"
case logrus.PanicLevel:
lvl = "[PANIC](fg:white,bg:red,mod:bold)"
}
return []byte(fmt.Sprintf("%s[│](fg:grey) [%s](fg:gray100)", lvl, entry.Message)), nil
}
type HistoryManager struct {
histories map[Window][]widget.ModedText
cursors map[Window]int
mu sync.Mutex
}
func NewHistoryManager() *HistoryManager {
return &HistoryManager{
histories: make(map[Window][]widget.ModedText),
cursors: make(map[Window]int),
}
}
func (hm *HistoryManager) Append(win Window, input widget.ModedText) {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] = len(hm.histories[win])
hm.append(win, input)
hm.cursors[win] = len(hm.histories[win])
}
func (hm *HistoryManager) Insert(win Window, input widget.ModedText) {
hm.mu.Lock()
defer hm.mu.Unlock()
if hm.current(win) == input {
return
}
hm.append(win, input)
}
func (hm *HistoryManager) append(win Window, input widget.ModedText) {
hm.histories[win] = append(append(append([]widget.ModedText{}, hm.histories[win][:hm.cursors[win]]...), input), hm.histories[win][hm.cursors[win]:]...)
}
func (hm *HistoryManager) current(win Window) widget.ModedText {
if hm.cursors[win] < 0 {
hm.cursors[win] = 0
}
if hm.cursors[win] >= len(hm.histories[win]) {
hm.cursors[win] = len(hm.histories[win])
return widget.ModedText{}
}
return hm.histories[win][hm.cursors[win]]
}
func (hm *HistoryManager) Current(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.current(win)
}
func (hm *HistoryManager) Previous(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] -= 1
res := hm.current(win)
return res
}
func (hm *HistoryManager) Next(win Window) widget.ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] += 1
res := hm.current(win)
return res
}
type Tabber struct {
active bool
input string
match string
matches []string
extra string
pos int
mu sync.Mutex
}
func NewTabber() *Tabber {
return &Tabber{}
}
func (t *Tabber) Active() bool {
t.mu.Lock()
defer t.mu.Unlock()
return t.active
}
func (t *Tabber) Clear() {
t.mu.Lock()
defer t.mu.Unlock()
t.active = false
}
func (t *Tabber) Reset(input string, channel *Channel) string {
t.mu.Lock()
defer t.mu.Unlock()
parts := strings.Split(input, " ")
t.match = parts[len(parts)-1]
t.extra = ""
if t.match == parts[0] {
t.extra = ": "
}
var m []string
for _, v := range channel.Users() {
if strings.HasPrefix(v, t.match) {
m = append(m, v+t.extra)
}
}
// put the match on the end of the stack so we can tab back to it.
m = append(m, t.match)
t.input = input
t.matches = m
t.pos = 0
t.active = true
return strings.Replace(input, t.match, t.matches[t.pos], 1)
}
func (t *Tabber) Tab() string {
t.mu.Lock()
defer t.mu.Unlock()
if !t.active {
return ""
}
t.pos++
if t.pos >= len(t.matches) {
t.pos = 0
}
return strings.Replace(t.input, t.match, t.matches[t.pos], 1)
}
var Version = "SNAPSHOT"
// A Server handles user interaction and displaying screen elements.
type Server struct {
*logrus.Logger
outputLogHook *logFileWriterHook
screenWidth, screenHeight int
pageWidth, pageHeight int
mainWindow *ui.Grid
statusBar *widget.StatusBarPane
mainWindow *ui.Grid
statusBar *widget.StatusBarPane
inputTextBox *widget.ModedTextInput
chatPane *widget.ChatPane
userListPane *widget.UserList
tabber *Tabber
events *event.Dispatcher
irc *irc.Manager
currentNick string
WindowManager *WindowManager
HistoryManager *HistoryManager
wm *WindowManager
history *HistoryManager
tabber *TabCompleter
mu sync.RWMutex
done chan struct{}
@ -205,20 +49,22 @@ type Server struct {
// NewServer creates a new server.
func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
srv := &Server{
Logger: logrus.StandardLogger(),
Logger: logrus.StandardLogger(),
outputLogHook: newLogFileWriterHook(),
events: ev,
irc: irc,
tabber: NewTabber(),
wm: NewWindowManager(ev),
history: NewHistoryManager(),
tabber: NewTabCompleter(),
done: make(chan struct{}),
}
srv.initUI()
srv.HistoryManager = NewHistoryManager()
srv.WindowManager = NewWindowManager(ev)
srv.Logger.SetOutput(srv.WindowManager.Index(0))
srv.Logger.SetFormatter(&logFormatter{})
srv.Logger.SetOutput(srv.wm.Index(0))
srv.Logger.SetFormatter(&statusFormatter{})
srv.Logger.AddHook(srv.outputLogHook)
return srv, nil
}
@ -240,6 +86,18 @@ func (srv *Server) IRCDoAsync(fn func(conn *irc.Connection) error) {
}()
}
func (srv *Server) CurrentNick() string {
srv.mu.RLock()
defer srv.mu.RUnlock()
return srv.currentNick
}
func (srv *Server) setCurrentNick(newNick string) {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = newNick
}
func (srv *Server) initUI() {
ui.StyleParserColorMap["gray"] = colors.Grey35
ui.StyleParserColorMap["grey"] = colors.Grey35
@ -247,6 +105,7 @@ func (srv *Server) initUI() {
ui.StyleParserColorMap["grey82"] = colors.Grey82
ui.StyleParserColorMap["gray100"] = colors.Grey100
ui.StyleParserColorMap["grey100"] = colors.Grey100
ui.StyleParserColorMap["red4"] = colors.Red4
srv.userListPane = widget.NewUserList()
srv.userListPane.Rows = []string{}
@ -306,13 +165,13 @@ func (srv *Server) Close() {
func (srv *Server) Update() {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.statusBar.ActiveTabIndex = srv.WindowManager.ActiveIndex()
win := srv.WindowManager.Active()
srv.statusBar.ActiveTabIndex = srv.wm.ActiveIndex()
win := srv.wm.Active()
if win == nil {
return
}
win.Touch()
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = srv.WindowManager.TabNames()
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = srv.wm.TabNames()
srv.chatPane.SelectedRow = win.CurrentLine()
srv.chatPane.Rows = win.Lines()
srv.chatPane.Title = win.Title()
@ -324,14 +183,18 @@ func (srv *Server) Update() {
srv.chatPane.SubTitle = ""
srv.chatPane.ModeText = ""
}
srv.chatPane.LeftPadding = 12
if srv.statusBar.ActiveTabIndex != 0 {
srv.chatPane.LeftPadding = 17
srv.chatPane.LeftPadding = win.padding() + 7
if srv.statusBar.ActiveTabIndex == 0 {
srv.chatPane.ModeText = srv.currentNick
}
srv.mainWindow.Items = nil
if v, ok := win.(WindowWithUserList); ok {
srv.userListPane.Rows = v.UserList()
srv.userListPane.Title = fmt.Sprintf("%d users", len(srv.userListPane.Rows))
suff := "s"
if len(srv.userListPane.Rows) == 1 {
suff = ""
}
srv.userListPane.Title = fmt.Sprintf("%d user%s", len(srv.userListPane.Rows), suff)
srv.mainWindow.Set(