Browse Source

Commit work from last couple of evenings

master
Tyler Sommer 1 year ago
parent
commit
0a1fda6ab5
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
  1. 1
      .gitignore
  2. 6
      Makefile
  3. 88
      builtin_cmds.go
  4. 61
      cmd/squirssi/main.go
  5. 57
      colors/irc.go
  6. 2
      go.mod
  7. 2
      go.sum
  8. 388
      handlers.go
  9. 317
      handlers_irc.go
  10. 8
      plugin.go
  11. 398
      server.go
  12. 51
      text_input.go
  13. 220
      widget.go
  14. 71
      window.go
  15. 181
      window_manager.go
  16. 183
      window_writer.go

1
.gitignore

@ -1,5 +1,6 @@
.idea/
out/
plugins/
plugins
squirssi*.log
vendor/

6
Makefile

@ -5,7 +5,7 @@ SUBPACKAGES := colors
SQUIRCY3_ROOT ?= ../squircy3
PLUGINS := $(patsubst $(SQUIRCY3_ROOT)/plugins/%,%,$(wildcard $(SQUIRCY3_ROOT)/plugins/*))
SOURCES := $(wildcard *.go) $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
SOURCES := $(wildcard *.go) $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES))) $(shell find vendor/ -type f -name '*.go')
OUTPUT_BASE := out
@ -25,7 +25,7 @@ clean:
rm -rf plugins/ && cp -r $(SQUIRCY3_ROOT)/plugins .
rm -rf $(OUTPUT_BASE)
build: plugins squirssi
build: squirssi
generate: $(OUTPUT_BASE)/.generated
@ -34,7 +34,7 @@ squirssi: $(SQUIRSSI_TARGET)
plugins: $(PLUGIN_TARGETS)
run: build
$(SQUIRSSI_TARGET) 2> squirssi_errors.log
$(SQUIRSSI_TARGET) 2>> squirssi_errors.log
test: $(TESTDATA_NODEMODS_TARGET)
go test -tags netgo $(RACE) $(TEST_ARGS) ./...

88
builtin_cmds.go

@ -12,6 +12,7 @@ import (
type Command func(*Server, []string)
var builtIns = map[string]Command{
"exit": exitProgram,
"w": selectWindow,
"wc": closeWindow,
"join": joinChannel,
@ -19,6 +20,16 @@ var builtIns = map[string]Command{
"whois": whoisNick,
"names": namesChannel,
"nick": changeNick,
"me": actionMessage,
"msg": msgTarget,
}
func exitProgram(srv *Server, _ []string) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.interrupt != nil {
srv.interrupt()
}
}
func selectWindow(srv *Server, args []string) {
@ -32,24 +43,13 @@ func selectWindow(srv *Server, args []string) {
logrus.Warnln("selectWindow: expected first argument to be an integer")
return
}
srv.mu.Lock()
if ch >= len(srv.windows) {
logrus.Warnf("selectWindow: no window #%d", ch)
srv.mu.Unlock()
return
}
srv.statusBar.ActiveTabIndex = ch
srv.mu.Unlock()
srv.Update()
srv.Render()
srv.WindowManager.SelectIndex(ch)
}
func closeWindow(srv *Server, args []string) {
var ch int
if len(args) < 2 {
srv.mu.Lock()
ch = srv.statusBar.ActiveTabIndex
srv.mu.Unlock()
ch = srv.WindowManager.ActiveIndex()
} else {
var err error
ch, err = strconv.Atoi(args[1])
@ -58,7 +58,7 @@ func closeWindow(srv *Server, args []string) {
return
}
}
win := srv.windows[ch]
win := srv.WindowManager.Index(ch)
if strings.HasPrefix(win.Title(), "#") {
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Part(win.Title())
@ -67,7 +67,7 @@ func closeWindow(srv *Server, args []string) {
logrus.Warnln("closeWindow: failed to part channel before closing window")
}
}
srv.CloseWindow(ch)
srv.WindowManager.CloseIndex(ch)
}
func joinChannel(srv *Server, args []string) {
@ -115,7 +115,7 @@ func namesChannel(srv *Server, args []string) {
return
}
channel := args[1]
win := srv.WindowNamed(channel)
win := srv.WindowManager.Named(channel)
if win == nil {
logrus.Warnln("namesChannel: no window named", channel)
return
@ -156,3 +156,59 @@ func changeNick(srv *Server, args []string) {
logrus.Warnln("changeNick: error changing nick:", err)
}
}
func actionMessage(srv *Server, args []string) {
message := strings.Join(args[1:], " ")
window := srv.WindowManager.Active()
if window == nil || window.Title() == "status" {
return
}
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Action(window.Title(), message)
return nil
}); err != nil {
logrus.Warnln("actionMessage: error sending message:", err)
}
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
WriteAction(window, myNick, MyMessage(message))
}
func msgTarget(srv *Server, args []string) {
if len(args) < 3 {
logrus.Warnln("msgTarget: expects at least 2 arguments")
return
}
target := args[1]
if target == "status" {
return
}
message := strings.Join(args[2:], " ")
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Privmsg(target, message)
return nil
}); err != nil {
logrus.Warnln("msgTarget: error sending message:", err)
}
window := srv.WindowManager.Named(target)
if !strings.HasPrefix(target, "#") {
// direct message!
if window == nil {
dm := &DirectMessage{
newBufferedWindow(target, srv.events),
}
srv.WindowManager.Append(dm)
window = dm
}
}
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
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)
message = target + " -> " + message
}
WritePrivmsg(window, myNick, MyMessage(message))
}

61
cmd/squirssi/main.go

@ -8,7 +8,6 @@ import (
"code.dopame.me/veonik/squircy3/cli"
"code.dopame.me/veonik/squircy3/plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
tilde "gopkg.in/mattes/go-expand-tilde.v1"
@ -41,7 +40,7 @@ func (s *stringLevel) Set(str string) error {
var rootDir string
var extraPlugins stringsFlag
var logLevel = stringLevel(logrus.DebugLevel)
var logLevel = stringLevel(logrus.InfoLevel)
func init() {
flag.StringVar(&rootDir, "root", "~/.squirssi", "path to folder containing squirssi data")
@ -68,42 +67,42 @@ func init() {
rootDir = bp
}
type Manager struct {
*cli.Manager
}
func (m *Manager) Start() error {
if err := m.Manager.Start(); err != nil {
return err
}
plugins := m.Plugins()
srv, err := squirssi.FromPlugins(plugins)
if err != nil {
return err
}
srv.OnInterrupt(m.Stop)
return srv.Start()
}
func NewManager(rootDir string, extraPlugins ...string) (*Manager, error) {
cm, err := cli.NewManager(rootDir, extraPlugins...)
if err != nil {
return nil, err
}
cm.LinkedPlugins = append(cm.LinkedPlugins, plugin.InitializerFunc(squirssi.Initialize))
return &Manager{cm}, nil
}
func main() {
logrus.SetLevel(logrus.Level(logLevel))
m, err := cli.NewManager(rootDir, extraPlugins...)
m, err := NewManager(rootDir, extraPlugins...)
if err != nil {
logrus.Fatalln("error initializing squirssi:", err)
}
if err := m.Start(); err != nil {
logrus.Fatalln("error starting squirssi:", err)
}
plugins := m.Plugins()
plugins.RegisterFunc(squirssi.Initialize)
if err := configure(plugins); err != nil {
logrus.Fatalln("error starting squirssi:", err)
}
wait := make(chan struct{})
go func() {
defer close(wait)
if err = m.Loop(); err != nil {
logrus.Fatalln("exiting main loop with error:", err)
}
}()
srv, err := squirssi.FromPlugins(plugins)
if err != nil {
logrus.Fatalln("error starting squirssi:", err)
}
defer srv.Close()
srv.Start()
}
func configure(m *plugin.Manager) error {
errs := m.Configure()
if errs != nil && len(errs) > 0 {
if len(errs) > 1 {
return errors.WithMessage(errs[0], fmt.Sprintf("(and %d more...)", len(errs)-1))
}
return errs[0]
if err = m.Loop(); err != nil {
logrus.Fatalln("exiting main loop with error:", err)
}
return nil
}

57
colors/irc.go

@ -0,0 +1,57 @@
package colors
import (
ui "github.com/gizak/termui/v3"
)
type IRC int
const (
IRCWhite IRC = iota
IRCBlack
IRCBlue
IRCGreen
IRCRed
IRCBrown
IRCMagenta
IRCOrange
IRCYellow
IRCLightGreen
IRCCyan
IRCLightCyan
IRCLightBlue
IRCPink
IRCGrey
IRCLightGrey
IRCGray = IRCGrey
IRCLightGray = IRCLightGrey
IRCDefault = 99
)
var ircToUIMap = map[IRC]ui.Color{
IRCWhite: White,
IRCBlack: Black,
IRCBlue: Blue,
IRCGreen: Green,
IRCRed: Red,
IRCBrown: Maroon,
IRCMagenta: Magenta1,
IRCOrange: Orange1,
IRCYellow: Yellow,
IRCLightGreen: LightGreen,
IRCCyan: Cyan1,
IRCLightCyan: LightCyan1,
IRCLightBlue: LightSkyBlue1,
IRCPink: Pink1,
IRCGrey: Grey,
IRCLightGrey: Grey30,
IRCDefault: Clear,
}
func IRCToUI(irc IRC) ui.Color {
if v, ok := ircToUIMap[irc]; ok {
return v
}
return IRCDefault
}

2
go.mod

@ -3,7 +3,7 @@ module code.dopame.me/veonik/squirssi
go 1.14
require (
code.dopame.me/veonik/squircy3 v0.0.0-20200921021324-82d536b9a59b
code.dopame.me/veonik/squircy3 v0.0.0-20200924052855-a0f559182525
github.com/dop251/goja v0.0.0-20200526165454-f1752421c432
github.com/gizak/termui/v3 v3.1.0
github.com/mattn/go-runewidth v0.0.9 // indirect

2
go.sum

@ -1,5 +1,7 @@
code.dopame.me/veonik/squircy3 v0.0.0-20200921021324-82d536b9a59b h1:3wWnqhxFq3DkYgFRLzdRb77BZVnWAdgbs6cDgLE9e8g=
code.dopame.me/veonik/squircy3 v0.0.0-20200921021324-82d536b9a59b/go.mod h1:D0PZ58ANI0zuFIgCLrhmBSZQmLeKFd0vL+6YP2AamYc=
code.dopame.me/veonik/squircy3 v0.0.0-20200924052855-a0f559182525 h1:NkCqYaQxM02/IPmh2Epl1yi3/+SBrskLDwx3e7R3XNg=
code.dopame.me/veonik/squircy3 v0.0.0-20200924052855-a0f559182525/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=

388
handlers.go

@ -1,43 +1,26 @@
package squirssi
import (
"fmt"
"os"
"math"
"strings"
"sync"
"time"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"github.com/sirupsen/logrus"
)
func (srv *Server) bind() {
srv.events.Bind("ui.DIRTY", event.HandlerFunc(srv.onUIDirty))
func bindUIHandlers(srv *Server, events *event.Dispatcher) {
events.Bind("ui.DIRTY", HandleUIEvent(srv, onUIDirty))
}
srv.events.Bind("irc.CONNECT", event.HandlerFunc(srv.onIRCConnect))
srv.events.Bind("irc.DISCONNECT", event.HandlerFunc(srv.onIRCDisconnect))
srv.events.Bind("irc.PRIVMSG", event.HandlerFunc(srv.onIRCPrivmsg))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCJoin))
srv.events.Bind("irc.PART", event.HandlerFunc(srv.onIRCPart))
srv.events.Bind("irc.KICK", event.HandlerFunc(srv.onIRCKick))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.PART", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.KICK", event.HandlerFunc(srv.onIRCNames))
srv.events.Bind("irc.NICK", event.HandlerFunc(srv.onIRCNick))
srv.events.Bind("irc.353", event.HandlerFunc(srv.onIRC353))
srv.events.Bind("irc.366", event.HandlerFunc(srv.onIRC366))
errorCodes := []string{"irc.401", "irc.403", "irc.404", "irc.405", "irc.406", "irc.407", "irc.408", "irc.421"}
for _, code := range errorCodes {
srv.events.Bind(code, event.HandlerFunc(srv.onIRCError))
}
whoisCodes := []string{"irc.311", "irc.312", "irc.313", "irc.317", "irc.318", "irc.319", "irc.314", "irc.369"}
for _, code := range whoisCodes {
srv.events.Bind(code, event.HandlerFunc(srv.onIRCWhois))
}
type UIHandler func(*Server, *event.Event)
func HandleUIEvent(srv *Server, fn UIHandler) event.Handler {
return event.HandlerFunc(func(ev *event.Event) {
fn(srv, ev)
})
}
func (srv *Server) onUIDirty(_ *event.Event) {
func onUIDirty(srv *Server, _ *event.Event) {
srv.Update()
srv.Render()
}
@ -46,29 +29,36 @@ func (srv *Server) onUIDirty(_ *event.Event) {
// Not a regular event handler but instead called before the actual
// ui.KEYPRESS event is emitted. This is done to avoid extra lag between
// pressing a key and seeing the UI react.
func (srv *Server) onUIKeyPress(key string) {
func onUIKeyPress(srv *Server, key string) {
switch key {
case "<C-c>":
srv.Close()
os.Exit(0)
return
srv.inputTextBox.Append(string(0x03))
srv.RenderOnly(InputTextBox)
case "<C-u>":
srv.inputTextBox.Append(string(0x1F))
srv.RenderOnly(InputTextBox)
case "<C-b>":
srv.inputTextBox.Append(string(0x02))
srv.RenderOnly(InputTextBox)
case "<PageUp>":
srv.ScrollPageUp()
srv.mu.RLock()
h := srv.pageHeight
srv.mu.RUnlock()
srv.WindowManager.ScrollOffset(-h)
case "<PageDown>":
srv.ScrollPageDown()
srv.mu.RLock()
h := srv.pageHeight
srv.mu.RUnlock()
srv.WindowManager.ScrollOffset(h)
case "<Home>":
srv.ScrollTop()
srv.WindowManager.ScrollTo(0)
case "<End>":
srv.ScrollBottom()
srv.WindowManager.ScrollTo(math.MaxInt32)
case "<Space>":
srv.mu.Lock()
srv.inputTextBox.Append(" ")
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
case "<Backspace>":
srv.mu.Lock()
srv.inputTextBox.Backspace()
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
case "<C-5>":
srv.mu.Lock()
@ -82,11 +72,33 @@ func (srv *Server) onUIKeyPress(key string) {
srv.mu.Unlock()
srv.Update()
srv.Render()
case "<Up>":
win := srv.WindowManager.Active()
if win == nil {
return
}
cur := srv.inputTextBox.Consume()
if cur.Text != "" {
srv.HistoryManager.Insert(win, cur)
}
msg := srv.HistoryManager.Previous(win)
srv.inputTextBox.Set(msg)
srv.RenderOnly(InputTextBox)
case "<Down>":
win := srv.WindowManager.Active()
if win == nil {
return
}
cur := srv.inputTextBox.Consume()
if cur.Text != "" {
srv.HistoryManager.Insert(win, cur)
}
msg := srv.HistoryManager.Next(win)
srv.inputTextBox.Set(msg)
srv.RenderOnly(InputTextBox)
case "<Tab>":
srv.mu.Lock()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if ch, ok := channel.(WindowWithUserList); ok {
win := srv.WindowManager.Active()
if ch, ok := win.(WindowWithUserList); ok {
msg := srv.inputTextBox.Peek()
parts := strings.Split(msg, " ")
match := parts[len(parts)-1]
@ -104,316 +116,46 @@ func (srv *Server) onUIKeyPress(key string) {
}
srv.RenderOnly(InputTextBox)
case "<Enter>":
srv.mu.Lock()
in := srv.inputTextBox.Consume()
if srv.inputTextBox.Mode() == ModeCommand {
srv.inputTextBox.ToggleMode()
}
active := srv.statusBar.ActiveTabIndex
channel := srv.windows[active]
myNick := srv.currentNick
srv.mu.Unlock()
active := srv.WindowManager.ActiveIndex()
channel := srv.WindowManager.Active()
if channel == nil {
return
}
if len(in.Text) == 0 {
// render anyway incase the textbox mode was changed
srv.RenderOnly(MainWindow)
srv.RenderOnly(MainWindow, InputTextBox)
return
}
defer srv.HistoryManager.Append(channel, in)
switch in.Kind {
case ModeCommand:
args := strings.Split(in.Text, " ")
c := args[0]
if cmd, ok := builtIns[c]; ok {
cmd(srv, args)
} else {
logrus.Warnln("no command named:", c)
}
case ModeMessage:
srv.RenderOnly(InputTextBox)
if active == 0 {
// status window doesn't accept messages
return
}
if err := srv.irc.Do(func(c *irc.Connection) error {
c.Privmsg(channel.Title(), in.Text)
return nil
}); err != nil {
logrus.Warnln("failed to send message:", err)
}
if _, err := channel.Write([]byte("<" + myNick + "> " + in.Text)); err != nil {
logrus.Warnln("failed to write message:", err)
}
msgTarget(srv, []string{"msg", channel.Title(), in.Text})
}
default:
if len(key) != 1 {
// a single key resulted in more than one character, probably not a regular char
logrus.Debugln("received unhandled keypress:", key)
return
}
srv.mu.Lock()
if key == "/" && srv.inputTextBox.Len() == 0 {
srv.inputTextBox.ToggleMode()
} else {
srv.inputTextBox.Append(key)
}
srv.mu.Unlock()
srv.RenderOnly(InputTextBox)
}
}
func (srv *Server) onIRCConnect(_ *event.Event) {
err := srv.irc.Do(func(conn *irc.Connection) error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = conn.GetNick()
return nil
})
if err != nil {
logrus.Warnln("failed to set current nick:", err)
}
}
func (srv *Server) onIRCDisconnect(_ *event.Event) {
logrus.Infoln("*** Disconnected")
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = ""
}
func (srv *Server) onIRCNick(ev *event.Event) {
logrus.Infoln("irc.NICK:", ev.Data)
nick := ev.Data["Nick"].(string)
newNick := ev.Data["Message"].(string)
srv.mu.Lock()
if nick == srv.currentNick {
srv.currentNick = newNick
}
wins := make([]Window, len(srv.windows))
copy(wins, srv.windows)
srv.mu.Unlock()
for _, win := range wins {
if win.Title() == nick {
// direct message with nick, update title and print there
if dm, ok := win.(*DirectMessage); ok {
dm.mu.Lock()
dm.name = nick
dm.mu.Unlock()
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s is now known as %s", nick, newNick))); err != nil {
logrus.Warnln("failed to write nick change:", err)
}
} else if ch, ok := win.(*Channel); ok {
hasNick := false
ch.mu.Lock()
for i, u := range ch.users {
if strings.ReplaceAll(strings.ReplaceAll(u, "@", ""), "+", "") == nick {
ch.users[i] = newNick
hasNick = true
break
}
}
ch.mu.Unlock()
if hasNick {
if _, err := win.Write([]byte(fmt.Sprintf("*** %s is now known as %s", nick, newNick))); err != nil {
logrus.Warnln("failed to write nick change:", err)
}
}
}
}
}
func (srv *Server) onIRCKick(ev *event.Event) {
args := ev.Data["Args"].([]string)
channel := ev.Data["Target"].(string)
kicked := args[1]
srv.mu.Lock()
myNick := srv.currentNick
srv.mu.Unlock()
if kicked == myNick {
go func() {
<-time.After(2 * time.Second)
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Join(channel)
return nil
}); err != nil {
logrus.Warnln("failed to rejoin after kick:", err)
}
}()
return
}
win := srv.WindowNamed(channel)
if win == nil {
logrus.Errorln("received kick with no Window:", channel, ev.Data["Message"], ev.Data["Nick"])
return
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s got kicked from %s (%s)", kicked, channel, ev.Data["Message"].(string)))); err != nil {
logrus.Warnln("failed to write message:", err)
}
}
var namesCache = &struct {
sync.Mutex
values map[string][]string
}{values: make(map[string][]string)}
func (srv *Server) onIRC353(ev *event.Event) {
// NAMES
args := ev.Data["Args"].([]string)
chanName := args[2]
nicks := strings.Split(args[3], " ")
win := srv.WindowNamed(chanName)
if win == nil {
logrus.Warnln("received NAMES for channel with no window:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
namesCache.values[chanName] = append(namesCache.values[chanName], nicks...)
}
func (srv *Server) onIRC366(ev *event.Event) {
// END NAMES
args := ev.Data["Args"].([]string)
chanName := args[1]
win := srv.WindowNamed(chanName)
if win == nil {
logrus.Warnln("received END NAMES for channel with no window:", chanName)
return
}
ch, ok := win.(*Channel)
if !ok {
logrus.Warnln("received END NAMES for a non channel:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
ch.mu.Lock()
ch.users = namesCache.values[chanName]
ch.mu.Unlock()
delete(namesCache.values, chanName)
}
func (srv *Server) onIRCError(ev *event.Event) {
args := ev.Data["Args"].([]string)
var kind string
if len(args) > 1 {
kind = args[1]
} else {
kind = ev.Data["Target"].(string)
}
logrus.Errorf("%s: %s", kind, ev.Data["Message"])
}
func (srv *Server) onIRCWhois(ev *event.Event) {
args := ev.Data["Args"].([]string)
kind := args[1]
data := args[2:]
if _, err := srv.status.Write([]byte(fmt.Sprintf("WHOIS %s => %s", kind, strings.Join(data, " ")))); err != nil {
logrus.Warnln("failed to write whois result to status:", err)
}
}
func (srv *Server) onIRCNames(ev *event.Event) {
if ev.Name == "irc.PART" || ev.Name == "irc.KICK" {
srv.mu.Lock()
myNick := srv.currentNick
srv.mu.Unlock()
nick := ev.Data["Nick"].(string)
if nick == myNick {
// dont bother trying to get names when we are the one leaving
return
}
}
target := ev.Data["Target"].(string)
if strings.HasPrefix(target, "#") {
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.SendRawf("NAMES :%s", target)
return nil
}); err != nil {
logrus.Warnln("failed to run NAMES on user change:", err)
}
}
}
func (srv *Server) onIRCJoin(ev *event.Event) {
target := ev.Data["Target"].(string)
win := srv.WindowNamed(target)
if win == nil {
srv.mu.Lock()
ch := &Channel{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []string{srv.currentNick},
}
srv.windows = append(srv.windows, ch)
srv.mu.Unlock()
win = ch
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s joined %s", ev.Data["Nick"], win.Title()))); err != nil {
logrus.Warnln("%s: failed to write join message:", err)
}
}
func (srv *Server) onIRCPart(ev *event.Event) {
var win Window
var ch int
target := ev.Data["Target"].(string)
nick := ev.Data["Nick"].(string)
srv.mu.Lock()
for i, w := range srv.windows {
if w.Title() == target {
win = w
ch = i
break
}
}
myNick := srv.currentNick
srv.mu.Unlock()
if win == nil {
if nick != myNick {
// dont bother logging if we are the ones leaving
logrus.Errorln("received message with no Window:", target, ev.Data["Message"], ev.Data["Nick"])
}
return
}
if nick == myNick {
// its me!
srv.CloseWindow(ch)
}
if _, err := win.Write([]byte(fmt.Sprintf("*** %s parted %s", nick, win.Title()))); err != nil {
logrus.Warnln("%s: failed to write part message:", err)
}
}
func (srv *Server) onIRCPrivmsg(ev *event.Event) {
var win Window
direct := false
target := ev.Data["Target"].(string)
nick := ev.Data["Nick"].(string)
srv.mu.Lock()
if target == srv.currentNick {
// its a direct message!
direct = true
target = nick
}
for _, w := range srv.windows {
if w.Title() == target {
win = w
break
}
}
srv.mu.Unlock()
if win == nil {
if !direct {
logrus.Warnln("received message with no Window:", target, ev.Data["Message"], nick)
return
} else {
srv.mu.Lock()
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.windows = append(srv.windows, ch)
win = ch
srv.mu.Unlock()
}
}
if _, err := win.Write([]byte(fmt.Sprintf("<%s> %s", nick, ev.Data["Message"]))); err != nil {
logrus.Warnln("error writing to Window:", err)
}
}

317
handlers_irc.go

@ -0,0 +1,317 @@
package squirssi
import (
"fmt"
"strings"
"sync"
"time"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"github.com/sirupsen/logrus"
)
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.CTCP_ACTION", HandleIRCEvent(srv, onIRCAction))
events.Bind("irc.JOIN", HandleIRCEvent(srv, onIRCJoin))
events.Bind("irc.PART", HandleIRCEvent(srv, onIRCPart))
events.Bind("irc.KICK", HandleIRCEvent(srv, onIRCKick))
events.Bind("irc.JOIN", HandleIRCEvent(srv, onIRCNames))
events.Bind("irc.PART", HandleIRCEvent(srv, onIRCNames))
events.Bind("irc.KICK", HandleIRCEvent(srv, onIRCNames))
events.Bind("irc.NICK", HandleIRCEvent(srv, onIRCNick))
events.Bind("irc.353", HandleIRCEvent(srv, onIRC353))
events.Bind("irc.366", HandleIRCEvent(srv, onIRC366))
events.Bind("irc.QUIT", HandleIRCEvent(srv, onIRCQuit))
errorCodes := []string{"irc.401", "irc.403", "irc.404", "irc.405", "irc.406", "irc.407", "irc.408", "irc.421"}
for _, code := range errorCodes {
events.Bind(code, HandleIRCEvent(srv, onIRCError))
}
whoisCodes := []string{"irc.311", "irc.312", "irc.313", "irc.317", "irc.318", "irc.319", "irc.314", "irc.369"}
for _, code := range whoisCodes {
events.Bind(code, HandleIRCEvent(srv, onIRCWhois))
}
}
type IRCEvent struct {
Code string
Raw string
Nick string // <nick>
Host string // <nick>!<usr>@<host>
Source string // <host>
User string // <usr>
Target string
Message string
Args []string
}
func NormalizeIRCEvent(ev *event.Event) *IRCEvent {
if ev.Data == nil {
return nil
}
return &IRCEvent{
Code: ev.Data["Code"].(string),
Raw: ev.Data["Raw"].(string),
Nick: ev.Data["Nick"].(string),
Host: ev.Data["Host"].(string),
Source: ev.Data["Source"].(string),
User: ev.Data["User"].(string),
Target: ev.Data["Target"].(string),
Message: ev.Data["Message"].(string),
Args: ev.Data["Args"].([]string),
}
}
type IRCEventHandler func(srv *Server, ev *IRCEvent)
func HandleIRCEvent(srv *Server, h IRCEventHandler) event.Handler {
return event.HandlerFunc(func(ev *event.Event) {
nev := NormalizeIRCEvent(ev)
h(srv, nev)
})
}
func onIRCConnect(srv *Server, _ *IRCEvent) {
err := srv.irc.Do(func(conn *irc.Connection) error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = conn.GetNick()
return nil
})
if err != nil {
logrus.Warnln("failed to set current nick:", err)
}
}
func onIRCDisconnect(srv *Server, _ *IRCEvent) {
logrus.Infoln("*** Disconnected")
srv.mu.Lock()
defer srv.mu.Unlock()
srv.currentNick = ""
}
func onIRCNick(srv *Server, ev *IRCEvent) {
nick := SomeNick(ev.Nick)
newNick := ev.Message
srv.mu.Lock()
if ev.Nick == srv.currentNick {
nick = MyNick(srv.currentNick)
}
srv.mu.Unlock()
WriteNick(srv.WindowManager, nick, newNick)
}
func onIRCKick(srv *Server, ev *IRCEvent) {
channel := ev.Target
kicked := SomeNick(ev.Args[1])
srv.mu.Lock()
if kicked.string == srv.currentNick {
kicked = MyNick(srv.currentNick)
}
srv.mu.Unlock()
if kicked.me {
go func() {
<-time.After(2 * time.Second)
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.Join(channel)
return nil
}); err != nil {
logrus.Warnln("failed to rejoin after kick:", err)
}
}()
}
win := srv.WindowManager.Named(channel)
if win == nil {
logrus.Errorln("received kick with no Window:", channel, ev.Message, ev.Nick)
return
}
if kicked.me {
win.Notice()
}
WriteKick(win, kicked, ev.Message)
}
var namesCache = &struct {
sync.Mutex
values map[string][]string
}{values: make(map[string][]string)}
func onIRC353(srv *Server, ev *IRCEvent) {
// NAMES
chanName := ev.Args[2]
nicks := strings.Split(ev.Args[3], " ")
win := srv.WindowManager.Named(chanName)
if win == nil {
logrus.Warnln("received NAMES for channel with no window:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
namesCache.values[chanName] = append(namesCache.values[chanName], nicks...)
}
func onIRC366(srv *Server, ev *IRCEvent) {
// END NAMES
chanName := ev.Args[1]
win := srv.WindowManager.Named(chanName)
if win == nil {
logrus.Warnln("received END NAMES for channel with no window:", chanName)
return
}
ch, ok := win.(*Channel)
if !ok {
logrus.Warnln("received END NAMES for a non channel:", chanName)
return
}
namesCache.Lock()
defer namesCache.Unlock()
ch.mu.Lock()
ch.users = namesCache.values[chanName]
ch.mu.Unlock()
delete(namesCache.values, chanName)
}
func onIRCError(_ *Server, ev *IRCEvent) {
var kind string
if len(ev.Args) > 1 {
kind = ev.Args[1]
} else {
kind = ev.Target
}
logrus.Errorf("%s: %s", kind, ev.Message)
}
func onIRCWhois(srv *Server, ev *IRCEvent) {
kind := ev.Args[1]
data := ev.Args[2:]
if _, err := srv.WindowManager.status.Write([]byte(fmt.Sprintf("WHOIS %s => %s", kind, strings.Join(data, " ")))); err != nil {
logrus.Warnln("failed to write whois result to status:", err)
}
}
func onIRCNames(srv *Server, ev *IRCEvent) {
if ev.Code == "PART" || ev.Code == "KICK" {
srv.mu.Lock()
myNick := srv.currentNick
srv.mu.Unlock()
if ev.Nick == myNick {
// dont bother trying to get names when we are the one leaving
return
}
}
target := ev.Target
if strings.HasPrefix(target, "#") {
if err := srv.irc.Do(func(conn *irc.Connection) error {
conn.SendRawf("NAMES :%s", target)
return nil
}); err != nil {
logrus.Warnln("failed to run NAMES on user change:", err)
}
}
}
func onIRCJoin(srv *Server, ev *IRCEvent) {
target := ev.Target
win := srv.WindowManager.Named(target)
myNick := srv.currentNick
if win == nil {
ch := &Channel{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []string{},
}
srv.WindowManager.Append(ch)
win = ch
}
WriteJoin(win, Nick{ev.Nick, ev.Nick == myNick})
}
func onIRCPart(srv *Server, ev *IRCEvent) {
target := ev.Target
nick := SomeNick(ev.Nick)
win := srv.WindowManager.Named(target)
srv.mu.Lock()
if ev.Nick == srv.currentNick {
nick = MyNick(srv.currentNick)
}
srv.mu.Unlock()
if win == nil {
if !nick.me {
// dont bother logging if we are the ones leaving
logrus.Errorln("received message with no Window:", target, ev.Message, nick)
}
return
}
WritePart(win, nick, ev.Message)
}
func onIRCAction(srv *Server, ev *IRCEvent) {
direct := false
target := ev.Target
nick := ev.Nick
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
if target == myNick.string {
// its a direct message!
direct = true
target = nick
}
win := srv.WindowManager.Named(target)
if win == nil {
if !direct {
logrus.Warnln("received action message with no Window:", target, ev.Message, nick)
return
} else {
srv.mu.Lock()
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.WindowManager.Append(ch)
win = ch
srv.mu.Unlock()
}
}
msg := SomeMessage(ev.Message, myNick)
WriteAction(win, SomeNick(nick), msg)
}
func onIRCPrivmsg(srv *Server, ev *IRCEvent) {
direct := false
target := ev.Target
nick := ev.Nick
srv.mu.Lock()
myNick := MyNick(srv.currentNick)
srv.mu.Unlock()
if target == myNick.string {
// its a direct message!
direct = true
target = nick
}
win := srv.WindowManager.Named(target)
if win == nil {
if !direct {
logrus.Warnln("received message with no Window:", target, ev.Message, nick)
return
} else {
srv.mu.Lock()
ch := &DirectMessage{bufferedWindow: newBufferedWindow(target, srv.events)}
srv.WindowManager.Append(ch)
win = ch
srv.mu.Unlock()
}
}
msg := SomeMessage(ev.Message, myNick)
WritePrivmsg(win, SomeNick(nick), msg)
}
func onIRCQuit(srv *Server, ev *IRCEvent) {
nick := SomeNick(ev.Nick)
message := ev.Message
srv.mu.Lock()
if ev.Nick == srv.currentNick {
nick = MyNick(srv.currentNick)
}
srv.mu.Unlock()
WriteQuit(srv.WindowManager, nick, message)
}

8
plugin.go

@ -26,11 +26,11 @@ func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (event)", pluginName)
}
irc, err := irc.FromPlugins(m)
ircp, err := irc.FromPlugins(m)
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (irc)", pluginName)
}
srv, err := NewServer(ev, irc)
srv, err := NewServer(ev, ircp)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed to initialize Server", pluginName)
}
@ -45,3 +45,7 @@ type squirssiPlugin struct {
func (p *squirssiPlugin) Name() string {
return "squirssi"
}
func (p *squirssiPlugin) HandleShutdown() {
p.server.Close()
}

398
server.go

@ -9,35 +9,12 @@ import (
"code.dopame.me/veonik/squircy3/irc"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
tb "github.com/nsf/termbox-go"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/colors"
)
// A Server handles user interaction and displaying screen elements.
type Server struct {
*logrus.Logger
ScreenWidth, ScreenHeight int
mainWindow *ui.Grid
statusBar *ActivityTabPane
inputTextBox *ModedTextInput
chatPane *widgets.List
userListPane *widgets.Table
events *event.Dispatcher
irc *irc.Manager
currentNick string
windows []Window
status *Status
mu sync.Mutex
}
type logFormatter struct{}
func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
@ -46,136 +23,158 @@ func (f *logFormatter) Format(entry *logrus.Entry) ([]byte, error) {
case logrus.InfoLevel:
lvl = "[INFO ](fg:blue)"
case logrus.DebugLevel:
lvl = "[DEBUG](fg:white)"
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)"
lvl = "[FATAL](fg:white,bg:red,mod:bold)"
case logrus.TraceLevel:
lvl = "[TRACE](fg:white)"
lvl = "[TRACE](fg:white,mod:bold)"
case logrus.PanicLevel:
lvl = "[PANIC](fg:white,bg:red)"
lvl = "[PANIC](fg:white,bg:red,mod:bold)"
}
return []byte(fmt.Sprintf("%s: %s", lvl, entry.Message)), nil
return []byte(fmt.Sprintf("%s[|](fg:grey) [%s](fg:gray100)", lvl, entry.Message)), nil
}
// NewServer creates a new server.
func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
if err := ui.Init(); err != nil {
return nil, err
}
w, h := ui.TerminalDimensions()
srv := &Server{
Logger: logrus.StandardLogger(),
ScreenWidth: w,
ScreenHeight: h,
type HistoryManager struct {
histories map[Window][]ModedText
cursors map[Window]int
events: ev,
irc: irc,
mu sync.Mutex
}
status: &Status{bufferedWindow: newBufferedWindow("status", ev)},
func NewHistoryManager() *HistoryManager {
return &HistoryManager{
histories: make(map[Window][]ModedText),
cursors: make(map[Window]int),
}
srv.windows = []Window{srv.status}
srv.Logger.SetOutput(srv.status)
srv.Logger.SetFormatter(&logFormatter{})
srv.initUI()
return srv, nil
}
// WindowNamed returns the window with the given name, if it exists.
func (srv *Server) WindowNamed(name string) Window {
var win Window
srv.mu.Lock()
defer srv.mu.Unlock()
for _, w := range srv.windows {
if w.Title() == name {
win = w
break
}
}
return win
func (hm *HistoryManager) Append(win Window, input 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])
logrus.Infoln("resetting cursor for", win.Title(), "now on", hm.cursors[win])
}
// CloseWindow closes a window denoted by tab index.
func (srv *Server) CloseWindow(ch int) {
if ch == 0 {
logrus.Warnln("CloseWindow: cant close status window")
func (hm *HistoryManager) Insert(win Window, input ModedText) {
hm.mu.Lock()
defer hm.mu.Unlock()
if hm.current(win) == input {
return
}
srv.mu.Lock()
if ch >= len(srv.windows) {
logrus.Warnf("CloseWindow: no window #%d", ch)
srv.mu.Unlock()
return
hm.append(win, input)
}
func (hm *HistoryManager) append(win Window, input ModedText) {
logrus.Infoln("inserting to history for", win.Title(), input, "at index", hm.cursors[win])
hm.histories[win] = append(append(append([]ModedText{}, hm.histories[win][:hm.cursors[win]]...), input), hm.histories[win][hm.cursors[win]:]...)
}
func (hm *HistoryManager) current(win Window) ModedText {
if hm.cursors[win] < 0 {
hm.cursors[win] = 0
}
srv.windows = append(srv.windows[:ch], srv.windows[ch+1:]...)
if ch >= srv.statusBar.ActiveTabIndex {
srv.statusBar.ActiveTabIndex = ch - 1
logrus.Infof("currently have %d records for %s", len(hm.histories[win]), win.Title())
if hm.cursors[win] >= len(hm.histories[win]) {
hm.cursors[win] = len(hm.histories[win])
return ModedText{}
}
srv.mu.Unlock()
srv.Update()
srv.Render()
return hm.histories[win][hm.cursors[win]]
}
// ScrollPageUp scrolls one opage up in the current window.
func (srv *Server) ScrollPageUp() {
srv.mu.Lock()
srv.chatPane.ScrollPageUp()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(row)
srv.Render()
func (hm *HistoryManager) Current(win Window) ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.current(win)
}
// ScrollPageDown scrolls one page down in the current window.
func (srv *Server) ScrollPageDown() {
srv.mu.Lock()
srv.chatPane.ScrollPageDown()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(row)
srv.Render()
func (hm *HistoryManager) Previous(win Window) ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] -= 1
res := hm.current(win)
logrus.Infoln("previous history for", win.Title(), hm.cursors[win])
return res
}
// ScrollTop scrolls to the top of the current window.
func (srv *Server) ScrollTop() {
srv.mu.Lock()
srv.chatPane.ScrollTop()
row := srv.chatPane.SelectedRow
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
func (hm *HistoryManager) Next(win Window) ModedText {
hm.mu.Lock()
defer hm.mu.Unlock()
hm.cursors[win] += 1
res := hm.current(win)
logrus.Infoln("next history for", win.Title(), hm.cursors[win])
return res
}
// A Server handles user interaction and displaying screen elements.
type Server struct {
*logrus.Logger
screenWidth, screenHeight int
pageWidth, pageHeight int
mainWindow *ui.Grid
statusBar *ActivityTabPane
inputTextBox *ModedTextInput
chatPane *ChatPane
userListPane *widgets.Table
events *event.Dispatcher
irc *irc.Manager
currentNick string
WindowManager *WindowManager
HistoryManager *HistoryManager
mu sync.RWMutex
done chan struct{}
interrupt Interrupter
}
// NewServer creates a new server.
func NewServer(ev *event.Dispatcher, irc *irc.Manager) (*Server, error) {
srv := &Server{
Logger: logrus.StandardLogger(),
events: ev,
irc: irc,
done: make(chan struct{}),
}
channel.ScrollTo(row)
srv.Render()
srv.initUI()
srv.HistoryManager = NewHistoryManager()
srv.WindowManager = NewWindowManager(ev)
srv.Logger.SetOutput(srv.WindowManager.status)
srv.Logger.SetFormatter(&logFormatter{})
return srv, nil
}
// ScrollBottom scrolls to the end of the current window.
func (srv *Server) ScrollBottom() {
type Interrupter func()
func (srv *Server) OnInterrupt(fn Interrupter) *Server {
srv.mu.Lock()
srv.chatPane.ScrollBottom()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
srv.mu.Unlock()
if channel == nil {
return
}
channel.ScrollTo(-1)
srv.Render()
defer srv.mu.Unlock()
srv.interrupt = fn
return srv
}
func (srv *Server) initUI() {
ui.StyleParserColorMap["gray"] = colors.Grey66
ui.StyleParserColorMap["grey"] = colors.Grey66
ui.StyleParserColorMap["gray82"] = colors.Grey82
ui.StyleParserColorMap["grey82"] = colors.Grey82
ui.StyleParserColorMap["gray100"] = colors.Grey100
ui.StyleParserColorMap["grey100"] = colors.Grey100
srv.userListPane = widgets.NewTable()
srv.userListPane.Rows = [][]string{}
srv.userListPane.Border = false
@ -185,7 +184,7 @@ func (srv *Server) initUI() {
srv.userListPane.TextAlignment = ui.AlignRight
srv.userListPane.PaddingRight = 1
srv.chatPane = widgets.NewList()
srv.chatPane = NewChatPane()
srv.chatPane.Rows = []string{}
srv.chatPane.BorderStyle.Fg = colors.DodgerBlue1
srv.chatPane.Border = true
@ -193,12 +192,10 @@ func (srv *Server) initUI() {
srv.chatPane.PaddingRight = 1
srv.chatPane.WrapText = true
srv.statusBar = &ActivityTabPane{
TabPane: widgets.NewTabPane(" 0 "),
ActivityStyle: ui.NewStyle(ui.ColorBlack, ui.ColorWhite),
}
srv.statusBar.SetRect(0, srv.ScreenHeight-3, srv.ScreenWidth, srv.ScreenHeight)
srv.statusBar = NewActivityTabPane()
srv.statusBar.ActiveTabStyle.Fg = colors.DodgerBlue1
srv.statusBar.NoticeStyle = ui.NewStyle(colors.DodgerBlue1, colors.White)
srv.statusBar.ActivityStyle = ui.NewStyle(ui.ColorBlack, ui.ColorWhite)
srv.statusBar.Border = true
srv.statusBar.BorderTop = true
srv.statusBar.BorderLeft = false
@ -216,39 +213,34 @@ func (srv *Server) initUI() {
func (srv *Server) Close() {
srv.mu.Lock()
defer srv.mu.Unlock()
ui.Close()
}
func tabNames(windows []Window, active int) ([]string, map[int]struct{}) {
res := make([]string, len(windows))
activity := make(map[int]struct{})
for i := 0; i < len(windows); i++ {
win := windows[i]
if win.HasActivity() {
activity[i] = struct{}{}
}