commit 08b843d80fb0188eb01a9554af009ecaa237fd2d Author: Michael Zhang Date: Wed May 13 21:52:06 2020 -0500 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfbbe2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/ouichat + diff --git a/core/buftree.go b/core/buftree.go new file mode 100644 index 0000000..223e277 --- /dev/null +++ b/core/buftree.go @@ -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) + } + } + } +} diff --git a/core/message.go b/core/message.go new file mode 100644 index 0000000..9cda20a --- /dev/null +++ b/core/message.go @@ -0,0 +1,11 @@ +package core + +import "time" + +type Message struct { + Time time.Time `json:"time"` + MessageID string + Author string + AuthorID string + Contents string +} diff --git a/fonts/roboto-mono.ttf b/fonts/roboto-mono.ttf new file mode 100644 index 0000000..5919b5d Binary files /dev/null and b/fonts/roboto-mono.ttf differ diff --git a/fonts/roboto.ttf b/fonts/roboto.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/fonts/roboto.ttf differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1d86b6c --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4633d87 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c077c76 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/plugins/irc.go b/plugins/irc.go new file mode 100644 index 0000000..1fec1df --- /dev/null +++ b/plugins/irc.go @@ -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 +} diff --git a/plugins/manager.go b/plugins/manager.go new file mode 100644 index 0000000..8b1b09e --- /dev/null +++ b/plugins/manager.go @@ -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) +} diff --git a/plugins/plugin.go b/plugins/plugin.go new file mode 100644 index 0000000..a052b1b --- /dev/null +++ b/plugins/plugin.go @@ -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 +} diff --git a/plugins/rocketchat.go b/plugins/rocketchat.go new file mode 100644 index 0000000..d5c343e --- /dev/null +++ b/plugins/rocketchat.go @@ -0,0 +1 @@ +package plugins diff --git a/plugins/weechat.go b/plugins/weechat.go new file mode 100644 index 0000000..d5c343e --- /dev/null +++ b/plugins/weechat.go @@ -0,0 +1 @@ +package plugins diff --git a/ui/colors.go b/ui/colors.go new file mode 100644 index 0000000..e537f00 --- /dev/null +++ b/ui/colors.go @@ -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} +) diff --git a/ui/ctr_scroller.go b/ui/ctr_scroller.go new file mode 100644 index 0000000..edc3af8 --- /dev/null +++ b/ui/ctr_scroller.go @@ -0,0 +1,4 @@ +package ui + +type Scroller struct { +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..56aace6 --- /dev/null +++ b/ui/ui.go @@ -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() +} diff --git a/ui/util.go b/ui/util.go new file mode 100644 index 0000000..dd11cca --- /dev/null +++ b/ui/util.go @@ -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 +} diff --git a/ui/view.go b/ui/view.go new file mode 100644 index 0000000..c082639 --- /dev/null +++ b/ui/view.go @@ -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) +} diff --git a/ui/view_buffer.go b/ui/view_buffer.go new file mode 100644 index 0000000..ce103bb --- /dev/null +++ b/ui/view_buffer.go @@ -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 +} diff --git a/ui/view_root.go b/ui/view_root.go new file mode 100644 index 0000000..2b9e082 --- /dev/null +++ b/ui/view_root.go @@ -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) +} diff --git a/ui/view_tree.go b/ui/view_tree.go new file mode 100644 index 0000000..b95f880 --- /dev/null +++ b/ui/view_tree.go @@ -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 +} diff --git a/ui/view_vsplit.go b/ui/view_vsplit.go new file mode 100644 index 0000000..8ff38f7 --- /dev/null +++ b/ui/view_vsplit.go @@ -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 +} diff --git a/ui/widget_textbox.go b/ui/widget_textbox.go new file mode 100644 index 0000000..cf45482 --- /dev/null +++ b/ui/widget_textbox.go @@ -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 +}