Integrate with squircy3, display actual privmsgs in channel windows

master
Tyler Sommer 3 years ago
parent 2e5ad892be
commit 80b8f32d71
Signed by: tyler-sommer
GPG Key ID: C09C010500DBD008

5
.gitignore vendored

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

@ -0,0 +1,62 @@
# Makefile for squirssi, a proper IRC client.
# https://code.dopame.me/veonik/squirssi
SUBPACKAGES := colors
SQUIRCY3_ROOT := ../squircy3
PLUGINS := $(patsubst $(SQUIRCY3_ROOT)/plugins/%,%,$(wildcard $(SQUIRCY3_ROOT)/plugins/*))
SOURCES := $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
OUTPUT_BASE := out
PLUGIN_TARGETS := $(patsubst %,$(OUTPUT_BASE)/%.so,$(PLUGINS))
SQUIRSSI_TARGET := $(OUTPUT_BASE)/squirssi
RACE ?= -race
TEST_ARGS ?= -count 1
TESTDATA_NODEMODS_TARGET := testdata/node_modules
.PHONY: all build generate run squirssi plugins clean
all: build
clean:
rm -rf plugins/ && cp -r $(SQUIRCY3_ROOT)/plugins .
rm -rf $(OUTPUT_BASE)
build: plugins squirssi
generate: $(OUTPUT_BASE)/.generated
squirssi: $(SQUIRSSI_TARGET)
plugins: $(PLUGIN_TARGETS)
run: build
$(SQUIRSSI_TARGET) 2> squirssi_errors.log
test: $(TESTDATA_NODEMODS_TARGET)
go test -tags netgo $(RACE) $(TEST_ARGS) ./...
$(TESTDATA_NODEMODS_TARGET):
cd testdata && \
yarn install
.SECONDEXPANSION:
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins/%/*) $(SOURCES)
go build -tags netgo $(RACE) -o $@ -buildmode=plugin plugins/$*/*.go
$(SQUIRSSI_TARGET): $(SOURCES)
go build -tags netgo $(RACE) -o $@ cmd/squirssi/*.go
$(OUTPUT_BASE)/.generated: $(GENERATOR_SOURCES)
go generate
touch $@
$(OUTPUT_BASE):
mkdir -p $(OUTPUT_BASE)
$(SOURCES): $(OUTPUT_BASE)
$(GENERATOR_SOURCES): $(OUTPUT_BASE)

@ -1,20 +1,114 @@
package main
import (
"log"
"flag"
"fmt"
"os"
"strings"
"code.dopame.me/veonik/squircy3/event"
"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"
"code.dopame.me/veonik/squirssi"
)
type stringsFlag []string
func (s stringsFlag) String() string {
return strings.Join(s, "")
}
func (s *stringsFlag) Set(str string) error {
*s = append(*s, str)
return nil
}
type stringLevel logrus.Level
func (s stringLevel) String() string {
return logrus.Level(s).String()
}
func (s *stringLevel) Set(str string) error {
l, err := logrus.ParseLevel(str)
if err != nil {
return err
}
*s = stringLevel(l)
return nil
}
var rootDir string
var extraPlugins stringsFlag
var logLevel = stringLevel(logrus.DebugLevel)
func init() {
flag.StringVar(&rootDir, "root", "~/.squirssi", "path to folder containing squirssi data")
flag.Var(&logLevel, "log-level", "controls verbosity of logging output")
flag.Var(&extraPlugins, "plugin", "path to shared plugin .so file, multiple plugins may be given")
flag.Usage = func() {
fmt.Println("Usage: ", os.Args[0], "[options]")
fmt.Println()
fmt.Println("squirssi is a proper IRC client.")
fmt.Println()
fmt.Println("Options:")
flag.PrintDefaults()
}
flag.Parse()
bp, err := tilde.Expand(rootDir)
if err != nil {
logrus.Fatalln(err)
}
err = os.MkdirAll(bp, os.FileMode(0644))
if err != nil {
logrus.Fatalln(err)
}
rootDir = bp
}
func main() {
ev := event.NewDispatcherLimit(512)
srv, err := squirssi.NewServer(ev)
logrus.SetLevel(logrus.Level(logLevel))
m, err := cli.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 {
log.Fatalf("failed to initialize termui: %v", err)
logrus.Fatalln("error starting squirssi:", err)
}
o, err := os.OpenFile("squirssi.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
logrus.Fatalln("error open squirssi log:", err)
}
logrus.SetOutput(o)
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]
}
return nil
}

@ -3,6 +3,12 @@ module code.dopame.me/veonik/squirssi
go 1.14
require (
code.dopame.me/veonik/squircy3 v0.0.0-20200628150707-3ccbcd3262ff
code.dopame.me/veonik/squircy3 v0.0.0-20200920131207-ed20f8761f63
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
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.6.0
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c
)

@ -1,17 +1,26 @@
code.dopame.me/veonik/squircy3 v0.0.0-20200628150707-3ccbcd3262ff h1:V3R5nl3pVJY2lZFUYF4pRMJ8ddtny70VCaUavs0ZLJ0=
code.dopame.me/veonik/squircy3 v0.0.0-20200628150707-3ccbcd3262ff/go.mod h1:FMNo74A2CmtfQKFHBGA0AwZcfkAB8QH9KGcwQTK0iuI=
code.dopame.me/veonik/squircy3 v0.0.0-20200919204616-86f0894e12ac h1:Yu++XHLgBLd8+JkMBuNOTauFnJgW4FdyZJ9/B15qsI8=
code.dopame.me/veonik/squircy3 v0.0.0-20200919204616-86f0894e12ac/go.mod h1:FMNo74A2CmtfQKFHBGA0AwZcfkAB8QH9KGcwQTK0iuI=
code.dopame.me/veonik/squircy3 v0.0.0-20200920131207-ed20f8761f63 h1:F+w2oeimHKvKsuZrEYKGF28+qzKA71o+2YT4bIbPvGs=
code.dopame.me/veonik/squircy3 v0.0.0-20200920131207-ed20f8761f63/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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20200526165454-f1752421c432 h1:EIY1hqp9O08saJ41t7aQy0o1hhq3ByOy61AACthST5M=
github.com/dop251/goja v0.0.0-20200526165454-f1752421c432/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/fatih/structtag v1.0.0 h1:pTHj65+u3RKWYPSGaU290FpI/dXxTaHdVwVwbcPKmEc=
github.com/fatih/structtag v1.0.0/go.mod h1:IKitwq45uXL/yqi5mYghiD3w9H6eTOvI9vnk8tXMphA=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -20,31 +29,43 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb h1:EavwSqheIJl3nb91HhkL73DwnT2Fk8W3yM7T7TuLZvA=
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb/go.mod h1:I0ZT9x8wStY6VOxtNOrLpnDURFs7HS0z1e1vhuKUEVc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7 h1:wYqz/tQaWUgGKyx+B/rssSE6wkIKdY5Ee6ryOmzarIg=
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c h1:/Onz8dZtKBCmB8P0JU7+WSCfMekXry7BflVO0SQQrCU=
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c/go.mod h1:j6QavCO5cYWud1+2/PFTXL1y6tjjkhSs+qcWgibOIc0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

@ -0,0 +1,42 @@
package squirssi
import (
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/plugin"
"github.com/pkg/errors"
)
const pluginName = "squirssi"
func FromPlugins(m *plugin.Manager) (*Server, error) {
plg, err := m.Lookup(pluginName)
if err != nil {
return nil, err
}
mplg, ok := plg.(*squirssiPlugin)
if !ok {
return nil, errors.Errorf("event: received unexpected plugin type")
}
return mplg.server, nil
}
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
ev, err := event.FromPlugins(m)
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (event)", pluginName)
}
srv, err := NewServer(ev)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed to initialize Server", pluginName)
}
p := &squirssiPlugin{srv}
return p, nil
}
type squirssiPlugin struct {
server *Server
}
func (p *squirssiPlugin) Name() string {
return "squirssi"
}

@ -4,87 +4,33 @@ package squirssi
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squirssi/colors"
)
type Window interface {
Title() string
Lines() []string
HasActivity() bool
}
type WindowWithNicklist interface {
Window
Nicklist() []string
}
type Channel struct {
name string
topic string
modes string
users []string
lines []string
current int
hasUnseen bool
}
func (c *Channel) HasActivity() bool {
return c.hasUnseen
}
func (c *Channel) Title() string {
return c.name
}
func (c *Channel) Lines() []string {
return c.lines
}
func (c *Channel) Nicklist() []string {
return c.users
}
type DirectMessage struct {
user string
lines []string
current int
hasUnseen bool
}
func (c *DirectMessage) Title() string {
return c.user
}
func (c *DirectMessage) Lines() []string {
return c.lines
}
func (c *DirectMessage) HasActivity() bool {
return c.hasUnseen
}
type Server struct {
ScreenWidth, ScreenHeight int
mainWindow *ui.Grid
statusBar *widgets.TabPane
statusBar *ActivityTabPane
inputTextBox *widgets.Paragraph
inputTextBox *ModedTextInput
chatPane *widgets.List
nicklistPane *widgets.Table
input *string
userListPane *widgets.Table
events *event.Dispatcher
windows []Window
mu sync.Mutex
}
func NewServer(ev *event.Dispatcher) (*Server, error) {
@ -92,26 +38,68 @@ func NewServer(ev *event.Dispatcher) (*Server, error) {
return nil, err
}
w, h := ui.TerminalDimensions()
return &Server{
srv := &Server{
ScreenWidth: w,
ScreenHeight: h,
events: ev,
}, nil
}
srv.initUI()
return srv, nil
}
func (srv *Server) initUI() {
srv.userListPane = widgets.NewTable()
srv.userListPane.Rows = [][]string{}
srv.userListPane.Border = false
srv.userListPane.BorderStyle.Fg = ui.ColorBlack
srv.userListPane.RowSeparator = false
srv.userListPane.Title = "Users"
srv.userListPane.TextAlignment = ui.AlignRight
srv.userListPane.PaddingRight = 1
srv.chatPane = widgets.NewList()
srv.chatPane.Rows = []string{}
srv.chatPane.BorderStyle.Fg = colors.DodgerBlue1
srv.chatPane.Border = true
srv.chatPane.PaddingLeft = 1
srv.chatPane.PaddingRight = 1
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.ActiveTabStyle.Fg = colors.DodgerBlue1
srv.statusBar.Border = true
srv.statusBar.BorderTop = true
srv.statusBar.BorderLeft = false
srv.statusBar.BorderRight = false
srv.statusBar.BorderBottom = false
srv.statusBar.BorderStyle.Fg = colors.DodgerBlue1
srv.inputTextBox = NewModedTextInput()
srv.inputTextBox.Border = false
srv.mainWindow = ui.NewGrid()
status := Status{}
srv.windows = []Window{&status}
}
func (srv *Server) Close() {
ui.Close()
}
type ScreenItem int
type screenElement int
const (
InputTextBox ScreenItem = iota
InputTextBox screenElement = iota
StatusBar
MainWindow
)
func (srv *Server) RenderOnly(items ...ScreenItem) {
func (srv *Server) RenderOnly(items ...screenElement) {
var its []ui.Drawable
for _, it := range items {
switch it {
@ -126,20 +114,51 @@ func (srv *Server) RenderOnly(items ...ScreenItem) {
ui.Render(its...)
}
func (srv *Server) Render() {
func tabNames(windows []Window) ([]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{}{}
}
res[i] = fmt.Sprintf(" %d ", i)
}
return res, activity
}
func (srv *Server) Update() {
srv.mu.Lock()
defer srv.mu.Unlock()
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
return
}
srv.statusBar.TabNames, srv.statusBar.TabsWithActivity = tabNames(srv.windows)
srv.chatPane.Rows = channel.Lines()
srv.chatPane.Title = channel.Title()
srv.chatPane.SelectedRow = channel.CurrentLine()
srv.mainWindow.Items = nil
var rows [][]string
if v, ok := channel.(WindowWithNicklist); ok {
for _, nick := range v.Nicklist() {
if v, ok := channel.(WindowWithUserList); ok {
for _, nick := range v.Users() {
rows = append(rows, []string{nick})
}
srv.mainWindow.Set(
ui.NewCol(.9, srv.chatPane),
ui.NewCol(.1, srv.userListPane),
)
} else {
srv.mainWindow.Set(
ui.NewCol(1, srv.chatPane),
)
}
srv.nicklistPane.Rows = rows
srv.chatPane.Title = channel.Title()
srv.userListPane.Rows = rows
}
func (srv *Server) Render() {
srv.mu.Lock()
defer srv.mu.Unlock()
ui.Render(srv.mainWindow, srv.statusBar, srv.inputTextBox)
}
@ -156,54 +175,73 @@ func (srv *Server) handleKey(e ui.Event) {
srv.chatPane.ScrollPageDown()
srv.Render()
case "<Space>":
srv.appendInput(" ")
srv.inputTextBox.Append(" ")
srv.RenderOnly(InputTextBox)
case "<Backspace>":
srv.backspaceInput()
srv.inputTextBox.Backspace()
srv.RenderOnly(InputTextBox)
case "<C-5>":
srv.statusBar.FocusRight()
srv.Update()
srv.Render()
case "<Escape>":
srv.statusBar.FocusLeft()
srv.Update()
srv.Render()
case "<Tab>":
case "<Enter>":
in := srv.inputTextBox.Consume()
if srv.inputTextBox.Mode() == ModeCommand {
srv.inputTextBox.ToggleMode()
}
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
return
}
if c, ok := channel.(*Channel); ok {
c.lines = append(c.lines, "<veonik> "+srv.consumeInput())
defer srv.Render()
if len(in.Text) == 0 {
return
}
switch in.Kind {
case ModeCommand:
args := strings.Split(in.Text, " ")
cmd := args[0]
switch cmd {
case "w":
if len(args) < 2 {
return
}
ch, err := strconv.Atoi(args[1])
if err != nil {
panic(err)
return
}
if ch < len(srv.statusBar.TabNames) {
srv.statusBar.ActiveTabIndex = ch
srv.Update()
srv.Render()
}
}
case ModeMessage:
if c, ok := channel.(*Channel); ok {
c.lines = append(c.lines, "<veonik> "+in.Text)
} else if dm, ok := channel.(*DirectMessage); ok {
dm.lines = append(dm.lines, "<veonik> "+in.Text)
}
srv.Update()
srv.Render()
}
default:
if len(e.ID) != 1 {
// a single key resulted in more than one character, probably not a regular char
return
}
srv.appendInput(e.ID)
srv.RenderOnly(InputTextBox)
}
}
func (srv *Server) consumeInput() string {
defer srv.resetInput()
if len(*srv.input) < 2 {
return ""
}
return (*srv.input)[2:]
}
func (srv *Server) resetInput() {
*srv.input = "> "
}
func (srv *Server) appendInput(in string) {
*srv.input = *srv.input + in
}
func (srv *Server) backspaceInput() {
if len(srv.inputTextBox.Text) > 2 {
*srv.input = (*srv.input)[0 : len(*srv.input)-1]
if e.ID == "/" && srv.inputTextBox.Len() == 0 {
srv.inputTextBox.ToggleMode()
} else {
srv.inputTextBox.Append(e.ID)
}
srv.RenderOnly(InputTextBox)
}
}
@ -214,71 +252,50 @@ func (srv *Server) resize() {
srv.mainWindow.SetRect(0, 0, srv.ScreenWidth, srv.ScreenHeight-srv.statusBar.Dy()-srv.inputTextBox.Dy())
}
func (srv *Server) Start() {
nicklist := widgets.NewTable()
nicklist.Rows = [][]string{}
nicklist.Border = false
nicklist.BorderStyle.Fg = ui.ColorBlack
nicklist.RowSeparator = false
nicklist.Title = "Users"
nicklist.TextAlignment = ui.AlignRight
nicklist.PaddingRight = 1
chat := widgets.NewList()
chat.Rows = []string{}
chat.BorderStyle.Fg = colors.DodgerBlue1
chat.Border = true
chat.PaddingLeft = 1
chat.PaddingRight = 1
statusbar := widgets.NewTabPane(" 0 ", " 1 ", " 2 ")
statusbar.SetRect(0, srv.ScreenHeight-3, srv.ScreenWidth, srv.ScreenHeight)
statusbar.ActiveTabStyle.Fg = colors.DodgerBlue1
statusbar.Border = true
statusbar.BorderTop = true
statusbar.BorderLeft = false
statusbar.BorderRight = false
statusbar.BorderBottom = false
statusbar.BorderStyle.Fg = colors.DodgerBlue1
input := widgets.NewParagraph()
input.Border = false
input.Text = "> "
window := ui.NewGrid()
window.Set(
ui.NewCol(.9, chat),
ui.NewCol(.1, nicklist),
)
chan0 := Channel{
name: "##somechan",
lines: []string{"<veonik> this is a test", "<squishyj> this is only a test"},
users: []string{"@veonik", "+squishyj"},
}
chan1 := Channel{
name: "#uwot",
lines: []string{"<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi", "<trub0> u wot m8", "<veonik> hi"},
users: []string{"trub0", "veonik"},
}
chan2 := Channel{
name: "#squishyslab",
lines: []string{"<angrywombat> i dont think thats right", "<veonik> you are right"},
users: []string{"angrywombat", "veonik"},
}
srv.windows = []Window{&chan0, &chan1, &chan2}
srv.statusBar = statusbar
srv.mainWindow = window
srv.inputTextBox = input
srv.chatPane = chat
srv.nicklistPane = nicklist
srv.input = &srv.inputTextBox.Text
func (srv *Server) bind() {
srv.events.Bind("irc.JOIN", event.HandlerFunc(func(ev *event.Event) {
var win Window
target := ev.Data["Target"].(string)
for _, w := range srv.windows {
if w.Title() == target {
win = w
break
}
}
if win == nil {
ch := &Channel{name: target, users: []string{"veonik"}}
srv.windows = append(srv.windows, ch)
}
}))
srv.events.Bind("irc.PRIVMSG", event.HandlerFunc(func(ev *event.Event) {
var win Window
target := ev.Data["Target"].(string)
for _, w := range srv.windows {
if w.Title() == target {
win = w
break
}
}
if win == nil {
logrus.Warnln("received message with no Window:", target, ev.Data["Message"], ev.Data["Nick"])
} else {
if v, ok := win.(*Channel); ok {
if v.current == len(v.lines)-1 {
v.current++
}
v.lines = append(v.lines, fmt.Sprintf("<%s> %s", ev.Data["Nick"], ev.Data["Message"]))
srv.Update()
srv.Render()
}
}
}))
}
func (srv *Server) Start() {
srv.bind()
srv.inputTextBox.Reset()
srv.resize()
srv.Update()
srv.Render()
uiEvents := ui.PollEvents()
@ -298,8 +315,8 @@ func (srv *Server) Start() {
}
srv.events.Emit("ui.MOUSE", map[string]interface{}{
"kind": e.ID,
"x": mouse.X,
"y": mouse.Y,
"x": mouse.X,
"y": mouse.Y,
"drag": mouse.Drag,
})
case ui.ResizeEvent:
@ -308,10 +325,10 @@ func (srv *Server) Start() {
panic(fmt.Sprintf("received termui Resize event but Payload was unexpected type %T", e.Payload))
}
srv.ScreenHeight = resize.Height
srv.ScreenWidth = resize.Width
srv.ScreenWidth = resize.Width
srv.resize()
srv.events.Emit("ui.RESIZE", map[string]interface{}{
"width": resize.Width,
"width": resize.Width,
"height": resize.Height,
})
srv.Render()

@ -0,0 +1,122 @@
package squirssi
import (
"github.com/gizak/termui/v3/widgets"
)
const Cursor = "█"
const CursorLength = len(Cursor)
// A TextInput is a Paragraph widget with editable contents.
// A cursor block character is printed at the end of the contents and
// transparently handled when updating those contents.
type TextInput struct {
*widgets.Paragraph
// Length of the current prefix in the text.
// Calculated only when resetting.
prefixLen int
// If specified, called during Reset and the result is used
// as the initial text in the text box.
Prefix func() string
}
// Consume returns and clears the current input in the TextInput.
func (i *TextInput) Consume() string {
defer i.Reset()
if i.Len() < 1 {
return ""
}
return i.Text[i.prefixLen : len(i.Text)-CursorLength]
}
// Len returns the length of the contents of the TextInput.
func (i *TextInput) Len() int {
return len(i.Text) - CursorLength - i.prefixLen
}
// Reset the contents of the TextInput.
func (i *TextInput) Reset() {
prefix := ""
if i.Prefix != nil {
prefix = i.Prefix()
}
i.prefixLen = len(prefix)
i.Text = prefix + Cursor
}
// Append adds the given string to the end of the editable content.
func (i *TextInput) Append(in string) {
i.Text = i.Text[0:len(i.Text)-CursorLength] + in + Cursor
}
// Remove the last character from the end of the editable content.
func (i *TextInput) Backspace() {
if len(i.Paragraph.Text) > i.prefixLen+CursorLength {
i.Text = (i.Text)[0:len(i.Text)-CursorLength-1] + Cursor
}
}
// InputMode defines different kinds of input handled by a ModedTextInput.
type InputMode int
const (
// Regular text.
ModeMessage InputMode = iota
// A command and arguments separated by spaces.
ModeCommand
)
// A ModedTextInput tracks the current editing mode of a TextInput.
type ModedTextInput struct {
TextInput
mode InputMode
}
// NewModedTextInput creates a new ModedTextInput.
func NewModedTextInput() *ModedTextInput {
i := &ModedTextInput{
TextInput: TextInput{
Paragraph: widgets.NewParagraph(),
},
mode: ModeMessage,
}
i.Prefix = func() string {
if i.mode == ModeCommand {
return "/ "
}
return "> "
}
return i
}
// Mode returns the current editing mode.
func (i *ModedTextInput) Mode() InputMode {
return i.mode
}
// ToggleMode switches between the editing modes.
func (i *ModedTextInput) ToggleMode() {
if i.mode == ModeMessage {
i.mode = ModeCommand
} else {
i.mode = ModeMessage
}
i.Reset()
}
// ModedText is some text with an editing mode specified.
type ModedText struct {
Kind InputMode
Text string
}
// Consume returns and clears the ModedText in the ModedTextInput.
func (i *ModedTextInput) Consume() ModedText {
return ModedText{
Kind: i.mode,
Text: i.TextInput.Consume(),
}
}

@ -0,0 +1,47 @@
package squirssi
import (
"image"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type ActivityTabPane struct {
*widgets.TabPane
TabsWithActivity map[int]struct{}
ActivityStyle ui.Style
}
func (self *ActivityTabPane) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
xCoordinate := self.Inner.Min.X
for i, name := range self.TabNames {
ColorPair := self.InactiveTabStyle
if _, ok := self.TabsWithActivity[i]; ok {
ColorPair = self.ActivityStyle
}
if i == self.ActiveTabIndex {
ColorPair = self.ActiveTabStyle
}
buf.SetString(
ui.TrimString(name, self.Inner.Max.X-xCoordinate),
ColorPair,
image.Pt(xCoordinate, self.Inner.Min.Y),
)
xCoordinate += 1 + len(name)
if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X {
buf.SetCell(
ui.NewCell(ui.VERTICAL_LINE, ui.NewStyle(ui.ColorWhite)),
image.Pt(xCoordinate, self.Inner.Min.Y),
)
}
xCoordinate += 2
}
}

@ -0,0 +1,67 @@
package squirssi
type Window interface {
Title() string
Lines() []string
HasActivity() bool
CurrentLine() int
}
type WindowWithUserList interface {
Window
Users() []string
}
type bufferedWindow struct {
lines []string
current int
hasUnseen bool
}
func (c *bufferedWindow) HasActivity() bool {
return c.hasUnseen
}
func (c *bufferedWindow) Lines() []string {
return c.lines
}
func (c *bufferedWindow) CurrentLine() int {
return c.current
}
type Status struct {
bufferedWindow
}
func (c *Status) Title() string {
return "status"
}
type Channel struct {
bufferedWindow
name string
topic string
modes string
users []string
}
func (c *Channel) Title() string {
return c.name
}
func (c *Channel) Users() []string {
return c.users
}
type DirectMessage struct {
bufferedWindow
user string
}
func (c *DirectMessage) Title() string {
return c.user
}
Loading…
Cancel
Save