diff options
| -rw-r--r-- | client/application.go | 127 | ||||
| -rw-r--r-- | client/buffer/buffer.go | 94 | ||||
| -rw-r--r-- | client/client/client.go | 135 | ||||
| -rw-r--r-- | client/cmd_buffer.go | 77 | ||||
| -rw-r--r-- | client/main.go | 31 | ||||
| -rw-r--r-- | client/object/object.go | 95 | ||||
| -rw-r--r-- | tui/style.go | 1 |
7 files changed, 559 insertions, 1 deletions
diff --git a/client/application.go b/client/application.go new file mode 100644 index 0000000..fdb0216 --- /dev/null +++ b/client/application.go @@ -0,0 +1,127 @@ +package main + +import ( + "citrons.xyz/talk/client/client" + "citrons.xyz/talk/client/object" + "citrons.xyz/talk/client/buffer" + "citrons.xyz/talk/proto" + "citrons.xyz/talk/tui" + "zgo.at/termfo/keys" +) + +type application struct { + client.Client + reconnecting bool + authenticated bool + uid string + cache object.ObjCache + currentBuffer *buffer.Buffer + cmdBuffer cmdBuffer +} + +func newApplication(serverAddress string) *application { + var app application + app.Client = client.New(serverAddress) + app.cache = object.NewCache(&app) + app.cmdBuffer.Buffer = buffer.New("buffer") + app.currentBuffer = &app.cmdBuffer.Buffer + + app.cmdBuffer.info("connecting to %s", app.Client.Address) + return &app +} + +func (a *application) OnConnect() { + a.reconnecting = false + a.cmdBuffer.info("connected to %s", a.Client.Address) + + a.auth("test user") +} + +func (a *application) OnDisconnect(err error) { + a.authenticated = false + a.uid = "" + if !a.reconnecting { + a.cmdBuffer.err( + "disconnected from %s: %s\nreconnecting...", a.Client.Address, err, + ) + a.reconnecting = true + } +} + +func (a *application) OnEvent(cmd proto.Command) { + switch cmd.Kind { + case "update": + if len(cmd.Args) > 0 { + a.cache.Update(cmd.Target, cmd.Args[0]) + } + case "delete": + a.cache.Gone(cmd.Target) + } +} + +func (a *application) OnResponse(requestId string, cmd proto.Command) { + switch cmd.Kind { + case "you-are": + if len(cmd.Args) > 0 { + a.cmdBuffer.info("your name is: %s", cmd.Args[0].Fields[""]) + } + } +} + +func (a *application) Sub(id string) { + a.Request(proto.NewCmd("s", id), func(cmd proto.Command) { + if cmd.Kind == "i" && len(cmd.Args) > 0 { + a.cache.Update(id, cmd.Args[0]) + } + }) +} + +func (a *application) Unsub(id string) { + a.Request(proto.NewCmd("u", id), nil) +} + +func (a *application) GetInfo(id string, callback func(proto.Command)) { + a.Request(proto.NewCmd("i", id), callback) +} + +func (a *application) auth(name string) { + callback := func(response proto.Command) { + switch response.Kind { + case "you-are": + if len(response.Args) == 0 { + break + } + me := response.Args[0] + a.authenticated = true + a.uid = me.Id + a.cache.Watch(a.uid) + case "fail": + // todo + } + } + a.Request(proto.NewCmd("auth", "", proto.Object { + "anonymous", "", map[string]string {"": name}, + }), callback) +} + +func (a *application) onInput(ev tui.Event) { + a.currentBuffer.Scroll(-ev.Mouse.Scroll * 2) + switch ev.Key { + case keys.Up: + a.currentBuffer.Scroll(1) + case keys.Down: + a.currentBuffer.Scroll(-1) + } +} + +func (a *application) show() { + tui.Clear() + s := tui.Size() + tui.Push("", tui.Box { + Width: tui.BoxSize(s.Width), Height: tui.BoxSize(s.Height), + }) + a.currentBuffer.Show() + tui.Pop() + tui.DrawLayout() + tui.Present() +} diff --git a/client/buffer/buffer.go b/client/buffer/buffer.go new file mode 100644 index 0000000..bb782fb --- /dev/null +++ b/client/buffer/buffer.go @@ -0,0 +1,94 @@ +package buffer + +import ( + "citrons.xyz/talk/tui" +) + +type Buffer struct { + id string + top *bufList + bottom *bufList + scroll tui.ScrollState + Closed bool +} + +type Message interface { + Id() string + Show(odd bool) +} + +type bufList struct { + msg Message + odd bool + prev *bufList + next *bufList +} + +func New(id string) Buffer { + return Buffer {id: id} +} + +func (b *Buffer) Add(msg Message) { + l := &bufList {msg: msg} + if b.bottom != nil { + b.bottom.next = l + l.prev = b.bottom + l.odd = !b.bottom.odd + } + b.bottom = l + if b.top == nil { + b.top = b.bottom + } +} + +func (b *Buffer) AddTop(msg Message) { + l := bufList {msg: msg} + if b.top != nil { + b.top.prev = &l + l.next = b.top + l.odd = !b.bottom.odd + } + b.top = &l + if b.bottom == nil { + b.bottom = b.top + } +} + +func (b *Buffer) Scroll(amnt int) { + b.scroll.Scroll(amnt) +} + +func (b *Buffer) ScrollBottom() { + b.scroll.ToStart() +} + +func (b *Buffer) AtBottom() bool { + return b.scroll.AtFirst() +} + +func (b *Buffer) AtTop() bool { + return b.scroll.AtLast() +} + +func (b *Buffer) Show() (atTop bool) { + tui.Push(b.id, tui.Box { + Width: tui.Fill, Height: tui.Fill, Dir: tui.Up, Overflow: true, + Scroll: &b.scroll, + }) + defer tui.Pop() + + for m := b.bottom; m != nil; m = m.prev { + var bg int32 = tui.Black + if m.odd { + bg = 236 + } + tui.Push(m.msg.Id(), tui.Box { + Width: tui.Fill, Height: tui.Children, + Style: &tui.Style {Fg: tui.White, Bg: bg}, + }) + m.msg.Show(m.odd) + tui.Pop() + } + + return atTop +} diff --git a/client/client/client.go b/client/client/client.go new file mode 100644 index 0000000..afbafbe --- /dev/null +++ b/client/client/client.go @@ -0,0 +1,135 @@ +package client + +import ( + "citrons.xyz/talk/proto" + "net" + "time" + "bufio" +) + +type MessageHandler interface { + OnConnect() + OnDisconnect(err error) + OnEvent(cmd proto.Command) + OnResponse(requestId string, cmd proto.Command) +} + +type Message struct { + action func(MessageHandler) +} + +type Client struct { + Address string + stop chan struct{} + message chan Message + send chan proto.Line + activeRequests map[string]func(proto.Command) +} + +func New(address string) Client { + return Client { + Address: address, + stop: make(chan struct{}), + message: make(chan Message), + activeRequests: make(map[string]func(proto.Command)), + } +} + +func (c *Client) RunClient() { + sleep := time.Second / 4 + for { + conn, err := net.Dial("tcp", c.Address) + if err != nil { + c.message <- Message {func(mh MessageHandler) { + mh.OnDisconnect(err) + }} + time.Sleep(sleep) + sleep = min(sleep * 2, 30 * time.Second) + continue + } + defer conn.Close() + sleep = time.Second + + c.message <- Message {func(mh MessageHandler) { + mh.OnConnect() + }} + + recv, recvErr := proto.ReadLines(bufio.NewReader(conn)) + send := make(chan proto.Line, 1) + defer close(send) + sendErr := proto.WriteLines(bufio.NewWriter(conn), send) + + c.send = make(chan proto.Line) + go func() { + buf := make([]proto.Line, 0, 8) + for { + line, ok := <-c.send + if !ok { + return + } + buf = append(buf, line) + for len(buf) > 0 { + select { + case line, ok := <-c.send: + if !ok { + return + } + buf = append(buf, line) + case send <- buf[0]: + buf = buf[1:] + } + } + } + }() + defer close(c.send) + + run: for { + select { + case line := <-recv: + switch line.Kind { + case '*': + c.message <- Message {func(mh MessageHandler) { + mh.OnEvent(line.Cmd) + }} + case '!': + cb := c.activeRequests[line.RequestId] + c.message <- Message {func(mh MessageHandler) { + mh.OnResponse(line.RequestId, line.Cmd) + if cb != nil { + cb(line.Cmd) + } + }} + } + case err = <-recvErr: + break run + case err = <-sendErr: + break run + case <-c.stop: + return + } + } + c.message <- Message {func(mh MessageHandler) { + mh.OnDisconnect(err) + }} + time.Sleep(sleep) + sleep = min(sleep * 2, 30 * time.Second) + } +} + +func (c *Client) Stop() { + close(c.stop) +} + +func (c *Client) Messages() <-chan Message { + return c.message +} + +func (c *Client) Request(cmd proto.Command, callback func(proto.Command)) { + id := proto.GenId() + c.send <- proto.Line {'?', id, cmd} + c.activeRequests[id] = callback +} + +func (m Message) Handle(mh MessageHandler) { + m.action(mh) +} diff --git a/client/cmd_buffer.go b/client/cmd_buffer.go new file mode 100644 index 0000000..ae96a0f --- /dev/null +++ b/client/cmd_buffer.go @@ -0,0 +1,77 @@ +package main + +import ( + "citrons.xyz/talk/client/buffer" + "citrons.xyz/talk/tui" + "fmt" +) + +type cmdBuffer struct { + buffer.Buffer +} + +type logMsg struct { + index int + text string + logType logType +} +var lastIndex = 0 + +type logType int +const ( + logInfo = iota + logErr + logCmd +) + +func (m logMsg) Id() string { + return fmt.Sprintf("log.%d", m.index) +} + +func (m logMsg) Show(odd bool) { + var style *tui.Style + switch m.logType { + case logErr: + var bg int32 = tui.Red + if odd { + bg = tui.BrightRed + } + style = &tui.Style {Bg: bg, Fg: tui.Black} + case logCmd: + var bg int32 = tui.Blue + if odd { + bg = tui.BrightBlue + } + style = &tui.Style {Bg: bg, Fg: tui.Black} + default: + } + + tui.Push("", tui.Box { + Width: tui.Fill, Height: tui.Children, Style: style, Dir: tui.Right, + }) + + tui.Push("", tui.Box {Width: tui.TextSize, Height: tui.TextSize}) + tui.Text("* ", nil) + tui.Pop() + + tui.Push("", tui.Box {Width: tui.Fill, Height: tui.TextSize}) + tui.Text(m.text, nil) + tui.Pop() + + tui.Pop() +} + +func (b *cmdBuffer) info(f string, a ...any) { + lastIndex++ + b.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logInfo}) +} + +func (b *cmdBuffer) err(f string, a ...any) { + lastIndex++ + b.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logErr}) +} + +func (b *cmdBuffer) cmd(f string, a ...any) { + lastIndex++ + b.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logCmd}) +} diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..1f46a00 --- /dev/null +++ b/client/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "citrons.xyz/talk/tui" + "time" +) + +func main() { + tui.Start() + + app := newApplication("localhost:27508") + go app.RunClient() + + drawTick := time.Tick(time.Second / 60) + redraw := true + for { + select { + case m := <-app.Messages(): + m.Handle(app) + redraw = true + case e := <-tui.Events(): + app.onInput(e) + redraw = true + case <-drawTick: + if redraw { + app.show() + redraw = false + } + } + } +} diff --git a/client/object/object.go b/client/object/object.go new file mode 100644 index 0000000..f42eaf8 --- /dev/null +++ b/client/object/object.go @@ -0,0 +1,95 @@ +package object + +import ( + "citrons.xyz/talk/proto" +) + +type ObjCache struct { + stream ObjStream + objects map[string]cacheEntry +} + +type cacheEntry struct { + o proto.Object + refCount int + cached bool +} + +type ObjStream interface { + Sub(id string) + GetInfo(id string, callback func(proto.Command)) + Unsub(id string) +} + +func NewCache(stream ObjStream) ObjCache { + return ObjCache {stream, make(map[string]cacheEntry)} +} + +func (oc *ObjCache) Get(id string) *proto.Object { + entry := oc.objects[id] + if !entry.cached { + return nil + } + return &entry.o +} + +func (oc *ObjCache) Fetch(id string, callback func(*proto.Object)) { + entry := oc.objects[id] + if !entry.cached { + oc.stream.GetInfo(id, func(cmd proto.Command) { + if cmd.Kind == "i" && len(cmd.Args) > 0 { + callback(&cmd.Args[0]) + } else { + callback(nil) + } + }) + } + callback(&entry.o) +} + +func (oc *ObjCache) Watch(id string) { + entry, ok := oc.objects[id] + if !ok { + oc.stream.Sub(id) + } + oc.objects[id] = entry +} + +func (oc *ObjCache) Unwatch(id string) { + entry, ok := oc.objects[id] + if ok { + entry.refCount-- + if entry.refCount <= 0 { + oc.stream.Unsub(id) + delete(oc.objects, id) + } else { + oc.objects[id] = entry + } + } +} + +func (oc *ObjCache) Update(id string, new proto.Object) { + entry := oc.objects[id] + if entry.cached && new.Kind == entry.o.Kind { + for k, v := range new.Fields { + entry.o.Fields[k] = v + } + } else { + entry.o = new + } + entry.cached = true + oc.objects[id] = entry +} + +func (oc *ObjCache) Gone(id string) { + entry := oc.objects[id] + if entry.cached { + obj := entry.o + obj.Fields["kind"] = obj.Kind + obj.Kind = "gone" + entry.o = obj + oc.objects[id] = entry + } +} + + diff --git a/tui/style.go b/tui/style.go index dedd287..ac67233 100644 --- a/tui/style.go +++ b/tui/style.go @@ -3,7 +3,6 @@ package tui type Style struct { Fg int32 Bg int32 - Truecolor bool Bold bool Italic bool Underline bool |
