Initial commit

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

2
.gitignore vendored

@ -0,0 +1,2 @@
.idea/

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Tyler Sommer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,5 @@
squirssi
========
An IRC client.

@ -0,0 +1,20 @@
package main
import (
"log"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squirssi"
)
func main() {
ev := event.NewDispatcherLimit(512)
srv, err := squirssi.NewServer(ev)
if err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer srv.Close()
srv.Start()
}

@ -0,0 +1,269 @@
package colors
import (
ui "github.com/gizak/termui/v3"
)
type Xterm = ui.Color
const Clear Xterm = -1
// Adapted from https://jonasjacek.github.io/colors/
const (
Black Xterm = iota
Maroon
Green
Olive
Navy
Purple
Teal
Silver
Grey
Red
Lime
Yellow
Blue
Fuchsia
Aqua
White
Grey0
NavyBlue
DarkBlue
Blue3
Blue2
Blue1
DarkGreen
DeepSkyBlue7
DeepSkyBlue6
DeepSkyBlue5
DodgerBlue3
DodgerBlue2
Green4
SpringGreen6
Turquoise4
DeepSkyBlue4
DeepSkyBlue3
DodgerBlue1
Green3
SpringGreen5
DarkCyan
LightSeaGreen
DeepSkyBlue2
DeepSkyBlue1
Green2
SpringGreen4
SpringGreen3
Cyan3
DarkTurquoise
Turquoise2
Green1
SpringGreen2
SpringGreen1
MediumSpringGreen
Cyan2
Cyan1
DarkRed1
DeepPink8
Purple5
Purple4
Purple3
BlueViolet
Orange5
Grey37
MediumPurple6
SlateBlue4
SlateBlue3
RoyalBlue1
Chartreuse6
DarkSeaGreen9
PaleTurquoise4
SteelBlue
SteelBlue3
CornflowerBlue
Chartreuse4
DarkSeaGreen8
CadetBlue2
CadetBlue
SkyBlue3
SteelBlue2
Chartreuse3
PaleGreen4
SeaGreen4
Aquamarine3
MediumTurquoise
SteelBlue1
Chartreuse2
SeaGreen3
SeaGreen2
SeaGreen1
Aquamarine2
DarkSlateGray2
DarkRed
DeepPink7
DarkMagenta1
DarkMagenta
DarkViolet
Purple2
Orange4
LightPink4
Plum4
MediumPurple5
MediumPurple4
SlateBlue1
Yellow6
Wheat4
Grey53
LightSlateGrey
MediumPurple
LightSlateBlue
Yellow5
DarkOliveGreen6
DarkSeaGreen
LightSkyBlue4
LightSkyBlue3
SkyBlue2
Chartreuse5
DarkOliveGreen5
PaleGreen3
DarkSeaGreen7
DarkSlateGray3
SkyBlue1
Chartreuse1
LightGreen1
LightGreen
PaleGreen2
Aquamarine1
DarkSlateGray1
Red4
DeepPink6
MediumVioletRed
Magenta6
DarkViolet1
Purple1
DarkOrange4
IndianRed3
HotPink5
MediumOrchid3
MediumOrchid
MediumPurple3
DarkGoldenrod
LightSalmon4
RosyBrown
Grey63
MediumPurple2
MediumPurple1
Gold4
DarkKhaki
NavajoWhite3
Grey69
LightSteelBlue3
LightSteelBlue
Yellow4
DarkOliveGreen4
DarkSeaGreen6
DarkSeaGreen5
LightCyan3
LightSkyBlue1
GreenYellow
DarkOliveGreen3
PaleGreen1
DarkSeaGreen4
DarkSeaGreen3
PaleTurquoise1
Red3
DeepPink5
DeepPink4
Magenta5
Magenta4
Magenta3
DarkOrange3
IndianRed
HotPink4
HotPink3
Orchid
MediumOrchid2
Orange3
LightSalmon3
LightPink3
Pink3
Plum3
Violet
Gold3
LightGoldenrod5
Tan
MistyRose3
Thistle3
Plum2
Yellow3
Khaki3
LightGoldenrod4
LightYellow3
Grey84
LightSteelBlue1
Yellow2
DarkOliveGreen2
DarkOliveGreen1
DarkSeaGreen1
Honeydew2
LightCyan1
Red1
DeepPink3
DeepPink2
DeepPink1
Magenta2
Magenta1
OrangeRed1
IndianRed2
IndianRed1
HotPink2
HotPink
MediumOrchid1
DarkOrange
Salmon1
LightCoral
PaleVioletRed1
Orchid2
Orchid1
Orange1
SandyBrown
LightSalmon1
LightPink1
Pink1
Plum1
Gold1
LightGoldenrod3
LightGoldenrod2
NavajoWhite1
MistyRose1
Thistle1
Yellow1
LightGoldenrod1
Khaki1
Wheat1
Cornsilk1
Grey100
Grey3
Grey7
Grey11
Grey15
Grey19
Grey23
Grey27
Grey30
Grey35
Grey39
Grey42
Grey46
Grey50
Grey54
Grey58
Grey62
Grey66
Grey70
Grey74
Grey78
Grey82
Grey85
Grey89
Grey93
)

@ -0,0 +1,8 @@
module code.dopame.me/veonik/squirssi
go 1.14
require (
code.dopame.me/veonik/squircy3 v0.0.0-20200628150707-3ccbcd3262ff
github.com/gizak/termui/v3 v3.1.0
)

@ -0,0 +1,50 @@
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=
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/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20200526165454-f1752421c432/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
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/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/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=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/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/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/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/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/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/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/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,320 @@
// import "code.dopame.me/veonik/squirssi"
package squirssi
import (
"fmt"
"os"
"code.dopame.me/veonik/squircy3/event"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"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
inputTextBox *widgets.Paragraph
chatPane *widgets.List
nicklistPane *widgets.Table
input *string
events *event.Dispatcher
windows []Window
}
func NewServer(ev *event.Dispatcher) (*Server, error) {
if err := ui.Init(); err != nil {
return nil, err
}
w, h := ui.TerminalDimensions()
return &Server{
ScreenWidth: w,
ScreenHeight: h,
events: ev,
}, nil
}
func (srv *Server) Close() {
ui.Close()
}
type ScreenItem int
const (
InputTextBox ScreenItem = iota
StatusBar
MainWindow
)
func (srv *Server) RenderOnly(items ...ScreenItem) {
var its []ui.Drawable
for _, it := range items {
switch it {
case InputTextBox:
its = append(its, srv.inputTextBox)
case StatusBar:
its = append(its, srv.statusBar)
case MainWindow:
its = append(its, srv.mainWindow)
}
}
ui.Render(its...)
}
func (srv *Server) Render() {
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
return
}
srv.chatPane.Rows = channel.Lines()
var rows [][]string
if v, ok := channel.(WindowWithNicklist); ok {
for _, nick := range v.Nicklist() {
rows = append(rows, []string{nick})
}
}
srv.nicklistPane.Rows = rows
srv.chatPane.Title = channel.Title()
ui.Render(srv.mainWindow, srv.statusBar, srv.inputTextBox)
}
func (srv *Server) handleKey(e ui.Event) {
switch e.ID {
case "<C-c>":
srv.Close()
os.Exit(0)
return
case "<PageUp>":
srv.chatPane.ScrollPageUp()
srv.Render()
case "<PageDown>":
srv.chatPane.ScrollPageDown()
srv.Render()
case "<Space>":
srv.appendInput(" ")
srv.RenderOnly(InputTextBox)
case "<Backspace>":
srv.backspaceInput()
case "<C-5>":
srv.statusBar.FocusRight()
srv.Render()
case "<Escape>":
srv.statusBar.FocusLeft()
srv.Render()
case "<Enter>":
channel := srv.windows[srv.statusBar.ActiveTabIndex]
if channel == nil {
return
}
if c, ok := channel.(*Channel); ok {
c.lines = append(c.lines, "<veonik> "+srv.consumeInput())
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]
srv.RenderOnly(InputTextBox)
}
}
func (srv *Server) resize() {
srv.statusBar.SetRect(0, srv.ScreenHeight-3, srv.ScreenWidth, srv.ScreenHeight)
srv.inputTextBox.SetRect(0, srv.ScreenHeight-srv.statusBar.Dy()-1, srv.ScreenWidth, srv.ScreenHeight-srv.statusBar.Dy())
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
srv.resize()
srv.Render()
uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.Type {
case ui.KeyboardEvent:
srv.handleKey(e)
srv.events.Emit("ui.KEYPRESS", map[string]interface{}{
"key": e.ID,
})
case ui.MouseEvent:
mouse, ok := e.Payload.(ui.Mouse)
if !ok {
panic(fmt.Sprintf("received termui Mouse event but Payload was unexpected type %T", e.Payload))
}
srv.events.Emit("ui.MOUSE", map[string]interface{}{
"kind": e.ID,
"x": mouse.X,
"y": mouse.Y,
"drag": mouse.Drag,
})
case ui.ResizeEvent:
resize, ok := e.Payload.(ui.Resize)
if !ok {
panic(fmt.Sprintf("received termui Resize event but Payload was unexpected type %T", e.Payload))
}
srv.ScreenHeight = resize.Height
srv.ScreenWidth = resize.Width
srv.resize()
srv.events.Emit("ui.RESIZE", map[string]interface{}{
"width": resize.Width,
"height": resize.Height,
})
srv.Render()
}
}
}
Loading…
Cancel
Save