TBR: Update gocui & polish UI
Update to use a recent github.com/jroimartin/gocui library, and polish
the UI a little bit.
Change-Id: Id507e4307088aa7eb5b8c89c00c98cb76cfba436
diff --git a/diagrams/screenshot.png b/diagrams/screenshot.png
index 98fc3ef..2792d58 100644
--- a/diagrams/screenshot.png
+++ b/diagrams/screenshot.png
Binary files differ
diff --git a/internal/server.go b/internal/server.go
index 1ad0b8a..c488da8 100644
--- a/internal/server.go
+++ b/internal/server.go
@@ -43,6 +43,7 @@
}
type Node struct {
+ id string
server rpc.Server
ps *PubSub
pms []*peerManager
@@ -50,6 +51,10 @@
cancel func()
}
+func (n *Node) Id() string {
+ return n.id
+}
+
func (n *Node) Server() rpc.Server {
return n.server
}
@@ -189,7 +194,7 @@
}
}
- return &Node{server, ps, pms, counters, func() {
+ return &Node{adId.String(), server, ps, pms, counters, func() {
cancel()
<-ps.done
for _, done := range dones {
diff --git a/vmsg/chat.go b/vmsg/chat.go
index 0f18f86..a07ab6e 100644
--- a/vmsg/chat.go
+++ b/vmsg/chat.go
@@ -8,21 +8,19 @@
package main
import (
- "bytes"
"fmt"
"io"
"io/ioutil"
- "math/rand"
"mime/multipart"
"os"
"path/filepath"
+ "sort"
"strconv"
"strings"
"time"
"github.com/fatih/color"
- "github.com/kr/text"
- "github.com/nlacasse/gocui"
+ "github.com/jroimartin/gocui"
"v.io/v23"
"v.io/v23/context"
@@ -31,6 +29,7 @@
lsec "v.io/x/ref/lib/security"
"v.io/x/ref/lib/v23cmd"
+ "messenger/ifc"
"messenger/internal"
)
@@ -64,188 +63,252 @@
}
defer node.Stop()
+ return runChatApp(ctx, node, params.Store)
+}
+
+func runChatApp(ctx *context.T, node *internal.Node, store internal.MessengerStorage) error {
g := gocui.NewGui()
if err := g.Init(); err != nil {
return err
}
defer g.Close()
- g.ShowCursor = true
+ g.Cursor = true
+ g.BgColor = gocui.ColorBlue
+ g.FgColor = gocui.ColorWhite
+ app := app{ctx, g, node, store}
+
g.SetLayout(func(g *gocui.Gui) error {
maxX, maxY := g.Size()
- messageInputViewHeight := 3
- if _, err := g.SetView("history", -1, -1, maxX, maxY-messageInputViewHeight); err != nil {
- if err != gocui.ErrorUnkView {
+ commandInputViewHeight := 3
+ if v, err := g.SetView("history", -1, 0, maxX, maxY-commandInputViewHeight); err != nil {
+ if err != gocui.ErrUnknownView {
return err
}
+ v.Title = " Vanadium Peer to Peer Chat "
+ v.BgColor = gocui.ColorBlack
+ v.Autoscroll = true
+ v.Wrap = true
}
- if messageInputView, err := g.SetView("messageInput", -1, maxY-messageInputViewHeight, maxX, maxY-1); err != nil {
- if err != gocui.ErrorUnkView {
+ if v, err := g.SetView("commandInput", 0, maxY-commandInputViewHeight, maxX-1, maxY-1); err != nil {
+ if err != gocui.ErrUnknownView {
return err
}
- messageInputView.Editable = true
+ v.Title = " Enter text or command. Type /help for help "
+ v.BgColor = gocui.ColorBlue
+ v.FgColor = gocui.ColorYellow
+ v.Editable = true
}
- if err := g.SetCurrentView("messageInput"); err != nil {
+ if err := g.SetCurrentView("commandInput"); err != nil {
return err
}
return nil
})
- g.Flush()
-
- historyView, err := g.View("history")
- if err != nil {
- return err
- }
-
- clear := func() {
- historyView.Clear()
- historyView.SetOrigin(0, 0)
- g.Flush()
- }
-
- print := func(s ...string) {
- width, height := historyView.Size()
- for _, t := range s {
- historyView.Write(text.WrapBytes([]byte(t), width))
- }
- numLines := historyView.NumberOfLines()
- if numLines > height {
- historyView.SetOrigin(0, numLines-height)
- }
- g.Flush()
- }
-
- printf := func(format string, args ...interface{}) {
- print(fmt.Sprintf(format, args...))
- }
-
- debug := func() {
- printf("### %s", node.DebugString())
- }
-
- help := func() {
- print("*** Welcome to Vanadium Peer to Peer Chat ***")
- print("***")
- print(color.RedString("*** This is a demo application."))
- print("***")
- if encryptionKey == defaultEncryptionKey {
- print(color.RedString("*** Messages are encrypted with the default key. They are NOT private."))
- }
- print("***")
- print("*** Messages are stored and relayed peer-to-peer for 15 minutes after they are")
- print("*** created. New peers will see up to 15 minutes of history when they join.")
- print("***")
- print("*** Available commands are:")
- print("*** /debug to show the local node's debug information")
- print("*** /help to see this help message")
- print("*** /ping to send a ping")
- print("*** /quit to exit")
- print("*** /share <filename> to share a file")
- print("***")
- print("*** Type /quit or press Ctrl-C to exit.")
- print("***")
- }
if err := g.SetKeybinding("", gocui.KeyCtrlC, 0,
func(g *gocui.Gui, v *gocui.View) error {
- return gocui.Quit
+ return gocui.ErrQuit
},
); err != nil {
return err
}
- pingId := fmt.Sprintf("%08x", rand.Uint32())
-
- if err := g.SetKeybinding("messageInput", gocui.KeyEnter, 0,
+ if err := g.SetKeybinding("commandInput", gocui.KeyEnter, 0,
func(g *gocui.Gui, v *gocui.View) error {
- mtxt := strings.TrimSpace(v.Buffer())
+ txt := strings.TrimSpace(v.Buffer())
v.Clear()
- if mtxt == "" {
- return nil
- }
- fname := ""
- switch {
- case mtxt == "/debug":
- debug()
- return nil
- case mtxt == "/clear":
- clear()
- return nil
- case mtxt == "/help":
- help()
- return nil
- case mtxt == "/ping":
- mtxt = fmt.Sprintf("\x01PING %s %d", pingId, time.Now().UnixNano())
- case mtxt == "/quit":
- return gocui.Quit
- case strings.HasPrefix(mtxt, "/share"):
- fname = strings.TrimSpace(mtxt[5:])
- printf("### Sharing %s", fname)
- mtxt = ""
- case strings.HasPrefix(mtxt, "/"):
- printf("### Unknown command %s", mtxt)
- return nil
- }
- if err := sendMessage(ctx, node.PubSub(), params.Store, mtxt, fname); err != nil {
- printf("## sendMessage failed: %v\n", err)
- }
- return nil
+ v.SetOrigin(0, 0)
+ return app.handleInput(txt)
},
); err != nil {
return err
}
- help()
+ go app.loop()
- go func() {
- for msg := range node.PubSub().Sub() {
- _, r, err := params.Store.OpenRead(ctx, msg.Id)
- if err != nil {
- continue
- }
- msgText, filename, err := decryptChatMessage(msg.Id, r, incomingDir)
- r.Close()
- if err != nil {
- printf("## decryptChatMessage failed: %v\n", err)
- continue
- }
-
- delta := time.Since(msg.CreationTime).Seconds()
- hops := len(msg.Hops)
- var buf bytes.Buffer
- fmt.Fprintf(&buf, "%s %2d %5.2fs ", msg.CreationTime.Local().Format("15:04:05"), hops, delta)
- if msgText != "" {
- if strings.HasPrefix(msgText, "\x01PING") {
- fmt.Fprintf(&buf, "PING from %s", msg.SenderBlessings)
- reply := "\x01PONG" + msgText[5:]
- if err := sendMessage(ctx, node.PubSub(), params.Store, reply, ""); err != nil {
- ctx.Errorf("sendMessage failed: %v", err)
- }
- } else if strings.HasPrefix(msgText, "\x01PONG ") {
- p := strings.Split(msgText, " ")
- if len(p) != 3 || p[1] != pingId {
- continue
- }
- if i, err := strconv.ParseInt(p[2], 10, 64); err == nil {
- t := time.Unix(0, i)
- fmt.Fprintf(&buf, "PING reply from %s: %s", msg.SenderBlessings, time.Since(t))
- }
- } else {
- fmt.Fprintf(&buf, "<%s> %s", msg.SenderBlessings, msgText)
- }
- }
- if filename != "" {
- fmt.Fprintf(&buf, "Received shared file from %s: %s", msg.SenderBlessings, filename)
- }
- print(buf.String())
- }
- }()
-
- if err := g.MainLoop(); err != nil && err != gocui.Quit {
+ if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
return err
}
return nil
}
+type app struct {
+ ctx *context.T
+ g *gocui.Gui
+ node *internal.Node
+ store internal.MessengerStorage
+}
+
+func (a *app) flush() {
+ // AFAICT, this is the only way to update the UI.
+ a.g.Execute(func(g *gocui.Gui) error { return nil })
+}
+
+func (a *app) print(s string) {
+ if view, err := a.g.View("history"); err == nil {
+ view.Write([]byte(s))
+ }
+ a.flush()
+}
+
+func (a *app) printf(format string, args ...interface{}) {
+ a.print(fmt.Sprintf(format, args...))
+}
+
+func (a *app) printErrorf(format string, args ...interface{}) {
+ a.print(color.RedString(fmt.Sprintf(format, args...)))
+}
+
+func (a *app) clear() {
+ view, err := a.g.View("history")
+ if err != nil {
+ return
+ }
+ view.Clear()
+ view.SetOrigin(0, 0)
+ a.flush()
+}
+
+func (a *app) debug() {
+ a.printf("*** %s\n", a.node.DebugString())
+}
+
+func (a *app) help() {
+ a.print("***\n")
+ a.print("*** Welcome to Vanadium Peer to Peer Chat\n")
+ a.print("*** https://github.com/vanadium/messenger\n")
+ a.print("***\n")
+ a.printf("*** %s\n", color.RedString("This is a demo application."))
+ a.print("***\n")
+ if encryptionKey == defaultEncryptionKey {
+ a.printf("*** %s\n", color.RedString("Messages are encrypted with the default key. They are NOT private."))
+ }
+ a.print("***\n")
+ a.print("*** Messages are stored and relayed peer-to-peer for 15 minutes after they are\n")
+ a.print("*** created. New peers will see up to 15 minutes of history when they join.\n")
+ a.print("***\n")
+ a.print("*** Available commands are:\n")
+ a.print("*** /clear to clear this window\n")
+ a.print("*** /debug to show the local node's debug information\n")
+ a.print("*** /help to show this help message\n")
+ a.print("*** /history to show the message history\n")
+ a.print("*** /ping to send a ping\n")
+ a.print("*** /quit to exit\n")
+ a.print("*** /share <filename> to share a file\n")
+ a.print("***\n")
+ a.print("*** Type /quit or press Ctrl-C to exit.\n")
+ a.print("***\n")
+}
+
+func (a *app) history() {
+ ch, err := a.store.Manifest(a.ctx)
+ if err != nil {
+ return
+ }
+ msgs := messages{}
+ for msg := range ch {
+ msgs = append(msgs, msg)
+ }
+ sort.Sort(msgs)
+ for _, msg := range msgs {
+ a.handleMessage(msg)
+ }
+}
+
+func (a *app) handleInput(m string) error {
+ if m == "" {
+ return nil
+ }
+ fname := ""
+ switch {
+ case m == "/debug":
+ a.debug()
+ return nil
+ case m == "/clear":
+ a.clear()
+ return nil
+ case m == "/help":
+ a.help()
+ return nil
+ case m == "/history":
+ a.history()
+ return nil
+ case m == "/ping":
+ m = fmt.Sprintf("\x01PING %s %d", a.node.Id(), time.Now().UnixNano())
+ case m == "/quit":
+ return gocui.ErrQuit
+ case strings.HasPrefix(m, "/share"):
+ fname = strings.TrimSpace(m[6:])
+ a.printf("*** Sharing %s\n", fname)
+ m = ""
+ case strings.HasPrefix(m, "/"):
+ a.printErrorf("*** Unknown command %s\n", m)
+ return nil
+ }
+ if err := sendMessage(a.ctx, a.node.PubSub(), a.store, m, fname); err != nil {
+ a.printErrorf("*** Unable to send message: %v\n", err)
+ }
+ return nil
+}
+
+func (a *app) handleMessage(msg *ifc.Message) {
+ _, r, err := a.store.OpenRead(a.ctx, msg.Id)
+ if err != nil {
+ return
+ }
+ msgText, filename, err := decryptChatMessage(msg.Id, r, incomingDir)
+ r.Close()
+ if err != nil {
+ a.printErrorf("*** Unable to handle message: %v\n", err)
+ return
+ }
+
+ sender := color.GreenString("%s", msg.SenderBlessings)
+ prefix := color.WhiteString("%s %2d %5.2fs",
+ msg.CreationTime.Local().Format("15:04:05"),
+ len(msg.Hops),
+ time.Since(msg.CreationTime).Seconds())
+ if msgText != "" {
+ ping := color.GreenString("PING")
+ switch {
+ case strings.HasPrefix(msgText, "\x01PING"):
+ a.printf("%s %s from %s\n", prefix, ping, sender)
+ reply := "\x01PONG" + msgText[5:]
+ if err := sendMessage(a.ctx, a.node.PubSub(), a.store, reply, ""); err != nil {
+ a.printErrorf("*** Unable to send ping reply: %v", err)
+ }
+ case strings.HasPrefix(msgText, "\x01PONG "):
+ p := strings.Split(msgText, " ")
+ if len(p) != 3 || p[1] != a.node.Id() {
+ return
+ }
+ if i, err := strconv.ParseInt(p[2], 10, 64); err == nil {
+ t := time.Unix(0, i)
+ a.printf("%s %s reply from %s: %.2fs\n", prefix, ping, sender, time.Since(t).Seconds())
+ }
+ default:
+ a.printf("%s <%s> %s\n", prefix, sender, color.YellowString(msgText))
+ }
+ }
+ if filename != "" {
+ a.printf("%s Received shared file from %s: %s\n", prefix, sender, color.YellowString(filename))
+ }
+}
+
+func (a *app) loop() {
+ a.help()
+ ch := a.node.PubSub().Sub()
+ a.history()
+ for msg := range ch {
+ a.handleMessage(msg)
+ }
+}
+
+type messages []*ifc.Message
+
+func (m messages) Len() int { return len(m) }
+func (m messages) Less(i, j int) bool { return m[i].CreationTime.Before(m[j].CreationTime) }
+func (m messages) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
+
func sendMessage(ctx *context.T, ps *internal.PubSub, store internal.MessengerStorage, txt, fname string) error {
msgId := internal.NewMessageId()
encryptedFile, err := encryptChatMessage(msgId, txt, fname)