This commit is contained in:
Michael Zhang 2020-05-13 21:52:06 -05:00
commit 08b843d80f
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
23 changed files with 639 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/ouichat

57
core/buftree.go Normal file
View file

@ -0,0 +1,57 @@
package core
import "container/list"
type BufferNode struct {
name string
children []*BufferNode
messages []*Message
}
func NewBufferNode(name string) *BufferNode {
return &BufferNode{
name: name,
children: make([]*BufferNode, 0),
}
}
const (
WALK_DEPTH_FIRST = 0
WALK_BREADTH_FIRST = 1
)
type WalkEntry struct {
Node *BufferNode
Depth int
}
func (n*BufferNode) AddChild(child*BufferNode) {
n.children = append(n.children, child)
}
func (n *BufferNode) Walk(cb func(*WalkEntry), mode int) {
queue := list.New()
queue.PushBack(&WalkEntry{
Node: n,
Depth: 0,
})
for queue.Len() > 0 {
next := queue.Front()
queue.Remove(next)
entry := next.Value.(*WalkEntry)
cb(entry)
for _, child := range entry.Node.children {
childEntry := &WalkEntry{
Node: child,
Depth: entry.Depth + 1,
}
if mode == WALK_DEPTH_FIRST {
queue.PushFront(childEntry)
} else if mode == WALK_BREADTH_FIRST {
queue.PushBack(childEntry)
}
}
}
}

11
core/message.go Normal file
View file

@ -0,0 +1,11 @@
package core
import "time"
type Message struct {
Time time.Time `json:"time"`
MessageID string
Author string
AuthorID string
Contents string
}

BIN
fonts/roboto-mono.ttf Normal file

Binary file not shown.

BIN
fonts/roboto.ttf Normal file

Binary file not shown.

9
go.mod Normal file
View file

@ -0,0 +1,9 @@
module ouichat
go 1.14
require (
github.com/pkg/errors v0.9.1 // indirect
github.com/veandco/go-sdl2 v0.4.4
gopkg.in/irc.v3 v3.1.3
)

17
go.sum Normal file
View file

@ -0,0 +1,17 @@
github.com/MONKEY-WORKS/cassowary v0.0.0-20180307084501-6ee9ddb566f5 h1:KE9sasTMSjdsNIxvLyKMArAkzDyvwUo3O6ZqpOJNGWc=
github.com/MONKEY-WORKS/cassowary v0.0.0-20180307084501-6ee9ddb566f5/go.mod h1:kx7TtzC7VssZelylEr4cXP8Y/RVcBWT7ngGQg4MVijE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/monkey-works/cassowary v0.0.0-20180307084501-6ee9ddb566f5 h1:PqoJpAn2FCPtiQ0/PcuRDZ14Bl6vg6zukOQmoeYLdsU=
github.com/monkey-works/cassowary v0.0.0-20180307084501-6ee9ddb566f5/go.mod h1:M/BEaa1gf6g5yzH31758qbbAMp0WjZh2KGP6LnCxiJE=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/veandco/go-sdl2 v0.4.4 h1:coOJGftOdvNvGoUIZmm4XD+ZRQF4mg9ZVHmH3/42zFQ=
github.com/veandco/go-sdl2 v0.4.4/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/irc.v3 v3.1.3 h1:yeTiJ365882L8h4AnBKYfesD92y5R5ZhGiylu9DfcPY=
gopkg.in/irc.v3 v3.1.3/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

74
main.go Normal file
View file

@ -0,0 +1,74 @@
package main
import (
"log"
"os"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"ouichat/core"
"ouichat/plugins"
"ouichat/ui"
)
func run() int {
var err error
// Initialize libraries
if err = sdl.Init(sdl.INIT_EVERYTHING); err != nil {
panic(err)
}
defer sdl.Quit()
if err = ttf.Init(); err != nil {
panic(err)
}
defer ttf.Quit()
// create window
window, err := sdl.CreateWindow(
"ouichat",
sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
1024, 768,
sdl.WINDOW_SHOWN,
)
if err != nil {
panic(err)
}
defer window.Destroy()
// create buffer tree
tree := core.NewBufferNode("ouichat")
// start plugins
man := plugins.NewPluginManager(tree)
man.Spawn(plugins.NewIrcPlugin("acm"))
// initialize ui
ui, err := ui.NewUI(window, tree)
if err != nil {
log.Println(err)
return 1
}
defer ui.Close()
// main loop
for {
exit := ui.RenderLoop()
window.UpdateSurface()
if exit {
break
}
}
return 0
}
func main() {
var exitcode int
sdl.Main(func() {
exitcode = run()
})
os.Exit(exitcode)
}

54
plugins/irc.go Normal file
View file

@ -0,0 +1,54 @@
package plugins
import (
"crypto/tls"
"fmt"
"ouichat/core"
"gopkg.in/irc.v3"
)
type IrcPlugin struct {
name string
client *irc.Client
messages chan *irc.Message
man *PluginManager
root *core.BufferNode
}
func NewIrcPlugin(name string) *IrcPlugin {
conn, err := tls.Dial("tcp", "acm.umn.edu:6669", &tls.Config{})
if err != nil {
panic(err)
}
// messages := make(chan *irc.Message)
config := irc.ClientConfig{
Nick: "ouichat",
User: "ouichat",
Name: "ouichat",
Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
fmt.Printf("%+v\n", m)
// messages <- m
}),
}
client := irc.NewClient(conn, config)
return &IrcPlugin{
name: name,
client: client,
}
}
func(p*IrcPlugin) Name() string {
return p.name
}
func (p *IrcPlugin) Init(man *PluginManager, root *core.BufferNode) {
p.man = man
p.root = root
go p.client.Run()
}
func (p *IrcPlugin) Buffers() *core.BufferNode {
return p.root
}

23
plugins/manager.go Normal file
View file

@ -0,0 +1,23 @@
package plugins
import "ouichat/core"
// PluginManager is a convenience type that handles everything related to plugins
type PluginManager struct {
root *core.BufferNode
plugins []Plugin
}
func NewPluginManager(root *core.BufferNode) *PluginManager {
return &PluginManager{
root: root,
plugins: make([]Plugin, 0),
}
}
func (man *PluginManager) Spawn(plugin Plugin) {
node := core.NewBufferNode(plugin.Name())
man.root.AddChild(node)
plugin.Init(man, node)
man.plugins = append(man.plugins, plugin)
}

17
plugins/plugin.go Normal file
View file

@ -0,0 +1,17 @@
package plugins
import (
"ouichat/core"
)
type Plugin interface {
// Name is a unique name
Name() string
// Start is executed before anything
Init(man *PluginManager, root *core.BufferNode)
// Buffers gets a list of the buffers underneath this one
// the root one given to the plugin must be returned
Buffers() *core.BufferNode
}

1
plugins/rocketchat.go Normal file
View file

@ -0,0 +1 @@
package plugins

1
plugins/weechat.go Normal file
View file

@ -0,0 +1 @@
package plugins

11
ui/colors.go Normal file
View file

@ -0,0 +1,11 @@
package ui
import "github.com/veandco/go-sdl2/sdl"
var (
BACKGROUND = sdl.Color{R: 46, G: 46, B: 50, A: 255}
BACKGROUND2 = sdl.Color{R: 37, G: 37, B: 41, A: 255}
DIVIDER = sdl.Color{R: 32, G: 32, B: 36, A: 255}
TEXT = sdl.Color{R: 151, G: 151, B: 156, A: 255}
CARET = sdl.Color{R: 147, G: 221, B: 250, A: 255}
)

4
ui/ctr_scroller.go Normal file
View file

@ -0,0 +1,4 @@
package ui
type Scroller struct {
}

81
ui/ui.go Normal file
View file

@ -0,0 +1,81 @@
package ui
import (
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"ouichat/core"
)
const (
FrameRate = 60
)
var (
sansFont *ttf.Font
monoFont *ttf.Font
)
type UI struct {
win *sdl.Window
rdr *sdl.Renderer // SDL renderer
root View
focused *View
}
func NewUI(win *sdl.Window, tree *core.BufferNode) (*UI, error) {
rdr, err := sdl.CreateRenderer(win, 0, 0)
if err != nil {
return nil, err
}
if sansFont == nil {
if sansFont, err = ttf.OpenFont("fonts/roboto.ttf", 16); err != nil {
return nil, err
}
}
if monoFont == nil {
if monoFont, err = ttf.OpenFont("fonts/roboto-mono.ttf", 16); err != nil {
return nil, err
}
}
root := NewRootView(tree)
return &UI{
win: win,
rdr: rdr,
root: root,
focused: nil,
}, nil
}
func (ui *UI) RenderLoop() bool {
// handle events first
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch event.(type) {
case *sdl.KeyboardEvent:
// fmt.Printf("event: %+v\n", event)
case *sdl.QuitEvent:
println("Quit")
return true
}
}
// render
ui.rdr.SetDrawColor(BACKGROUND.R, BACKGROUND.G, BACKGROUND.B, BACKGROUND.A)
ui.rdr.Clear()
w, h := ui.win.GetSize()
ui.root.Draw(ui.rdr, 0, 0, int(w), int(h))
ui.rdr.Present()
sdl.Delay(1000 / FrameRate)
return false
}
func (ui *UI) Close() {
ui.rdr.Destroy()
sansFont.Close()
monoFont.Close()
}

25
ui/util.go Normal file
View file

@ -0,0 +1,25 @@
package ui
import (
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
)
func CreateText(r *sdl.Renderer, font *ttf.Font, text string, color sdl.Color) (*sdl.Texture, int32, int32, error) {
var (
err error
surf *sdl.Surface
txt *sdl.Texture
)
if surf, err = font.RenderUTF8Blended(text, color); err != nil {
return nil, -1, -1, err
}
defer surf.Free()
if txt, err = r.CreateTextureFromSurface(surf); err != nil {
return nil, -1, -1, err
}
return txt, surf.W, surf.H, nil
}

35
ui/view.go Normal file
View file

@ -0,0 +1,35 @@
package ui
import (
"fmt"
"sync"
"github.com/veandco/go-sdl2/sdl"
)
var (
idLock = &sync.Mutex{}
id = 0
)
// View is a rectangular widget
type View interface {
// Name returns a unique way to identify this view
// Use ViewID to create a unique ID
Name() string
// Draw the view using the given dimensions and offset
Draw(r *sdl.Renderer, x, y int32, w, h int) error
GetWidth() int
GetMinWidth() int
}
func ViewID(name string) string {
idLock.Lock()
defer idLock.Unlock()
next := id
id += 1
return fmt.Sprintf("%s_%d", name, next)
}

32
ui/view_buffer.go Normal file
View file

@ -0,0 +1,32 @@
package ui
import "github.com/veandco/go-sdl2/sdl"
type BufferView struct {
name string
textbox *TextBox
}
func NewBufferView(name string) *BufferView {
return &BufferView{
name: ViewID(name),
textbox: NewTextBox(),
}
}
func (v *BufferView) Name() string {
return v.name
}
func (v *BufferView) GetWidth() int {
return -1
}
func (v *BufferView) GetMinWidth() int {
return 400
}
func (v *BufferView) Draw(r *sdl.Renderer, x, y int32, w, h int) error {
v.textbox.Draw(r, x, y+int32(h)-40, w, 40)
return nil
}

42
ui/view_root.go Normal file
View file

@ -0,0 +1,42 @@
package ui
import (
"github.com/veandco/go-sdl2/sdl"
"ouichat/core"
)
type RootView struct {
name string
split *VSplitView
}
func NewRootView(tree *core.BufferNode) *RootView {
name := ViewID("root")
split := NewVSplitView()
split.AddChild(NewTreeView(tree))
split.AddChild(NewBufferView("empty"))
return &RootView{
name,
split,
}
}
func (v *RootView) Name() string {
return v.name
}
func (v *RootView) GetWidth() int {
return -1
}
func (v *RootView) GetMinWidth() int {
return -1
}
func (v *RootView) Draw(r *sdl.Renderer, x, y int32, w, h int) error {
// don't do anything but forward it to the split
return v.split.Draw(r, x, y, w, h)
}

55
ui/view_tree.go Normal file
View file

@ -0,0 +1,55 @@
package ui
import (
"fmt"
"strings"
"github.com/veandco/go-sdl2/sdl"
"ouichat/core"
)
type TreeView struct {
name string
tree *core.BufferNode
width uint
}
func NewTreeView(tree *core.BufferNode) *TreeView {
return &TreeView{
name: ViewID("tree"),
tree: tree,
width: 200,
}
}
func (v *TreeView) Name() string {
return v.name
}
func (v *TreeView) GetWidth() int {
return int(v.width)
}
func (v *TreeView) GetMinWidth() int {
return 200
}
func (v *TreeView) Draw(r *sdl.Renderer, x, y int32, w, h int) error {
var (
txt *sdl.Texture
tw, th int32
err error
)
if txt, tw, th, err = CreateText(r, sansFont, "BUFFERS", TEXT); err != nil {
return err
}
defer txt.Destroy()
r.Copy(txt, nil, &sdl.Rect{X: 5, Y: 5, W: tw, H: th})
v.tree.Walk(func(entry *core.WalkEntry) {
fmt.Printf("%s%+v\n", strings.Repeat(" ", entry.Depth), entry.Node)
}, core.WALK_DEPTH_FIRST)
return nil
}

56
ui/view_vsplit.go Normal file
View file

@ -0,0 +1,56 @@
package ui
import (
"github.com/veandco/go-sdl2/sdl"
)
type VSplitView struct {
children []View
}
func NewVSplitView() *VSplitView {
children := make([]View, 0)
return &VSplitView{
children,
}
}
func (v *VSplitView) AddChild(child View) {
v.children = append(v.children, child)
}
func (v *VSplitView) Draw(r *sdl.Renderer, x, y int32, w, h int) error {
totalWidth := w - int(len(v.children)) + 1
widths := make([]int, len(v.children))
remainingChildren := 0
for i, child := range v.children {
cw := child.GetWidth()
if cw >= 0 {
totalWidth -= cw
widths[i] = cw
} else {
remainingChildren += 1
widths[i] = -1
}
}
eachChild := totalWidth / remainingChildren
for i := range v.children {
if widths[i] == -1 {
widths[i] = eachChild
}
}
runningX := int32(0)
for i, child := range v.children {
child.Draw(r, runningX, y, widths[i], h)
runningX += int32(widths[i] + 1)
if err := r.SetDrawColor(DIVIDER.R, DIVIDER.G, DIVIDER.B, DIVIDER.A); err != nil {
return err
}
if err := r.DrawLine(runningX-1, y, int32(runningX)-1, y+int32(h)); err != nil {
return err
}
}
return nil
}

32
ui/widget_textbox.go Normal file
View file

@ -0,0 +1,32 @@
package ui
import (
"time"
"github.com/veandco/go-sdl2/gfx"
"github.com/veandco/go-sdl2/sdl"
)
var (
blinkTimer = time.Now()
)
type TextBox struct {
value string
hasFocus bool
}
func NewTextBox() *TextBox {
return &TextBox{}
}
func (b *TextBox) Draw(r *sdl.Renderer, x, y int32, w, h int) error {
r.SetDrawColor(BACKGROUND2.R, BACKGROUND2.G, BACKGROUND2.B, BACKGROUND2.A)
r.FillRect(&sdl.Rect{X: x, Y: y, W: int32(w), H: int32(h)})
if int64(time.Now().Sub(blinkTimer).Seconds())%2 == 0 {
cx := x + 5
gfx.ThickLineColor(r, cx, y+5, cx, y+int32(h)-5, 3, CARET)
}
return nil
}