Basic initial privmsg sending, flesh out Window interface

master
Tyler Sommer 2020-09-20 20:32:29 -06:00
parent 4d2b6498eb
commit 6938bd6fae
Signed by: tyler-sommer
GPG Key ID: C09C010500DBD008
10 changed files with 206 additions and 107 deletions

View File

@ -3,9 +3,9 @@
SUBPACKAGES := colors
SQUIRCY3_ROOT := ../squircy3
SQUIRCY3_ROOT ?= ../squircy3
PLUGINS := $(patsubst $(SQUIRCY3_ROOT)/plugins/%,%,$(wildcard $(SQUIRCY3_ROOT)/plugins/*))
SOURCES := $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
SOURCES := $(wildcard *.go) $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
OUTPUT_BASE := out

31
builtin_cmds.go Normal file
View File

@ -0,0 +1,31 @@
package squirssi
import (
"strconv"
)
type Command func(*Server, []string)
var builtIns = map[string]Command{
"w": selectWindow,
"wc": closeWindow,
}
func selectWindow(srv *Server, args []string) {
if len(args) < 2 {
return
}
ch, err := strconv.Atoi(args[1])
if err != nil {
return
}
if ch < len(srv.statusBar.TabNames) {
srv.statusBar.ActiveTabIndex = ch
srv.Update()
srv.Render()
}
}
func closeWindow(srv *Server, args []string) {
}

View File

@ -61,7 +61,7 @@ func init() {
if err != nil {
logrus.Fatalln(err)
}
err = os.MkdirAll(bp, os.FileMode(0644))
err = os.MkdirAll(bp, 0644)
if err != nil {
logrus.Fatalln(err)
}
@ -93,11 +93,6 @@ func main() {
if err != nil {
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()
}

2
go.mod
View File

@ -3,7 +3,7 @@ module code.dopame.me/veonik/squirssi
go 1.14
require (
code.dopame.me/veonik/squircy3 v0.0.0-20200920131207-ed20f8761f63
code.dopame.me/veonik/squircy3 v0.0.0-20200921021324-82d536b9a59b
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

8
go.sum
View File

@ -1,7 +1,5 @@
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=
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=
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=
@ -37,8 +35,6 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyh
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=

55
handlers.go Normal file
View File

@ -0,0 +1,55 @@
package squirssi
import (
"fmt"
"code.dopame.me/veonik/squircy3/event"
"github.com/sirupsen/logrus"
)
func (srv *Server) onUIDirty(_ *event.Event) {
srv.Update()
srv.Render()
}
func (srv *Server) onIRCJoin(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{
bufferedWindow: newBufferedWindow(target, srv.events),
users: []string{"veonik"},
}
srv.windows = append(srv.windows, ch)
}
}
func (srv *Server) onIRCPrivmsg(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++
}
if _, err := v.Write([]byte(fmt.Sprintf("<%s> %s", ev.Data["Nick"], ev.Data["Message"]))); err != nil {
logrus.Warnln("error writing to Channel:", err)
return
}
}
}
}

View File

@ -2,6 +2,7 @@ package squirssi
import (
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
"code.dopame.me/veonik/squircy3/plugin"
"github.com/pkg/errors"
)
@ -25,7 +26,11 @@ func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (event)", pluginName)
}
srv, err := NewServer(ev)
irc, err := irc.FromPlugins(m)
if err != nil {
return nil, errors.Wrapf(err, "%s: missing required dependency (irc)", pluginName)
}
srv, err := NewServer(ev, irc)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed to initialize Server", pluginName)
}

View File

@ -4,11 +4,11 @@ package squirssi
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/sirupsen/logrus"
@ -17,6 +17,8 @@ import (
)
type Server struct {
*logrus.Logger
ScreenWidth, ScreenHeight int
mainWindow *ui.Grid
@ -27,21 +29,27 @@ type Server struct {
userListPane *widgets.Table
events *event.Dispatcher
irc *irc.Manager
windows []Window
status *Status
mu sync.Mutex
}
func NewServer(ev *event.Dispatcher) (*Server, error) {
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,
events: ev,
events: ev,
irc: irc,
}
srv.initUI()
return srv, nil
@ -77,14 +85,13 @@ func (srv *Server) initUI() {
srv.statusBar.BorderBottom = false
srv.statusBar.BorderStyle.Fg = colors.DodgerBlue1
srv.inputTextBox = NewModedTextInput()
srv.inputTextBox = NewModedTextInput(CursorFullBlock)
srv.inputTextBox.Border = false
srv.mainWindow = ui.NewGrid()
status := Status{}
srv.windows = []Window{&status}
srv.status = &Status{bufferedWindow: newBufferedWindow("status", srv.events)}
srv.windows = []Window{srv.status}
}
func (srv *Server) Close() {
@ -198,38 +205,24 @@ func (srv *Server) handleKey(e ui.Event) {
if channel == nil {
return
}
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()
}
c := args[0]
if cmd, ok := builtIns[c]; ok {
cmd(srv, args)
}
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)
if err := srv.irc.Do(func(c *irc.Connection) error {
c.Privmsg(channel.Title(), in.Text)
_, err := channel.Write([]byte("<veonik> " + in.Text))
return err
}); err != nil {
logrus.Warnln("failed to send message:", err)
}
srv.Update()
srv.Render()
}
default:
@ -253,42 +246,9 @@ func (srv *Server) resize() {
}
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()
}
}
}))
srv.events.Bind("ui.DIRTY", event.HandlerFunc(srv.onUIDirty))
srv.events.Bind("irc.JOIN", event.HandlerFunc(srv.onIRCJoin))
srv.events.Bind("irc.PRIVMSG", event.HandlerFunc(srv.onIRCPrivmsg))
}
func (srv *Server) Start() {
@ -297,6 +257,7 @@ func (srv *Server) Start() {
srv.resize()
srv.Update()
srv.Render()
srv.Logger.SetOutput(srv.status)
uiEvents := ui.PollEvents()

View File

@ -4,8 +4,7 @@ import (
"github.com/gizak/termui/v3/widgets"
)
const Cursor = "█"
const CursorLength = len(Cursor)
const CursorFullBlock = "█"
// A TextInput is a Paragraph widget with editable contents.
// A cursor block character is printed at the end of the contents and
@ -13,6 +12,12 @@ const CursorLength = len(Cursor)
type TextInput struct {
*widgets.Paragraph
// Character used to indicate the TextInput is focused and
// awaiting input.
cursor string
// Length of the cursor character at the end of the input.
cursorLen int
// Length of the current prefix in the text.
// Calculated only when resetting.
prefixLen int
@ -22,18 +27,26 @@ type TextInput struct {
Prefix func() string
}
func NewTextInput(cursor string) *TextInput {
return &TextInput{
Paragraph: widgets.NewParagraph(),
cursor: cursor,
cursorLen: len(cursor),
}
}
// 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]
return i.Text[i.prefixLen : len(i.Text)-i.cursorLen]
}
// Len returns the length of the contents of the TextInput.
func (i *TextInput) Len() int {
return len(i.Text) - CursorLength - i.prefixLen
return len(i.Text) - i.cursorLen - i.prefixLen
}
// Reset the contents of the TextInput.
@ -43,18 +56,18 @@ func (i *TextInput) Reset() {
prefix = i.Prefix()
}
i.prefixLen = len(prefix)
i.Text = prefix + Cursor
i.Text = prefix + i.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
i.Text = i.Text[0:len(i.Text)-i.cursorLen] + in + i.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
if len(i.Paragraph.Text) > i.prefixLen+i.cursorLen {
i.Text = (i.Text)[0:len(i.Text)-i.cursorLen-1] + i.cursor
}
}
@ -76,12 +89,10 @@ type ModedTextInput struct {
}
// NewModedTextInput creates a new ModedTextInput.
func NewModedTextInput() *ModedTextInput {
func NewModedTextInput(cursor string) *ModedTextInput {
i := &ModedTextInput{
TextInput: TextInput{
Paragraph: widgets.NewParagraph(),
},
mode: ModeMessage,
TextInput: *NewTextInput(cursor),
mode: ModeMessage,
}
i.Prefix = func() string {
if i.mode == ModeCommand {

View File

@ -1,10 +1,28 @@
package squirssi
import (
"io"
"strings"
"sync"
"code.dopame.me/veonik/squircy3/event"
)
type Window interface {
io.Writer
// Title of the Window.
Title() string
// Contents of the Window, separated by line.
Lines() []string
HasActivity() bool
// The bottom-most visible line number, or negative to indicate
// the window is pinned to the end of input.
CurrentLine() int
// Clears the activity indicator for the window, it it's set.
Touch()
// Returns true if the Window has new lines since the last touch.
HasActivity() bool
}
type WindowWithUserList interface {
@ -13,21 +31,59 @@ type WindowWithUserList interface {
}
type bufferedWindow struct {
name string
lines []string
current int
hasUnseen bool
events *event.Dispatcher
mu sync.Mutex
}
func newBufferedWindow(name string, events *event.Dispatcher) bufferedWindow {
return bufferedWindow{
name: name,
events: events,
}
}
func (c *bufferedWindow) Title() string {
return c.name
}
func (c *bufferedWindow) Write(p []byte) (n int, err error) {
c.mu.Lock()
defer c.mu.Unlock()
defer c.events.Emit("ui.DIRTY", map[string]interface{}{
"name": c.name,
})
c.lines = append(c.lines, strings.TrimRight(string(p), "\n"))
c.hasUnseen = true
return len(p), nil
}
func (c *bufferedWindow) Touch() {
c.mu.Lock()
defer c.mu.Unlock()
c.hasUnseen = false
}
func (c *bufferedWindow) HasActivity() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.hasUnseen
}
func (c *bufferedWindow) Lines() []string {
c.mu.Lock()
defer c.mu.Unlock()
return c.lines
}
func (c *bufferedWindow) CurrentLine() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.current
}
@ -42,26 +98,15 @@ func (c *Status) Title() string {
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
}