mirror of https://github.com/veonik/squirssi
Basic initial privmsg sending, flesh out Window interface
parent
4d2b6498eb
commit
6938bd6fae
4
Makefile
4
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
|
@ -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
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-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
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
93
server.go
93
server.go
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
69
window.go
69
window.go
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue