summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorcitrons <citrons@mondecitronne.com>2025-06-01 15:38:17 -0500
committercitrons <citrons@mondecitronne.com>2025-06-01 15:38:17 -0500
commite2dc5b6fbb6adf6f379ef0123c214a196211c305 (patch)
treecc976665dd2e0916af100b5c0f7f629cc1936b01 /client
parente740e5478a43358fbbd79636483d000e01f88b7e (diff)
joining channels
Diffstat (limited to 'client')
-rw-r--r--client/application.go159
-rw-r--r--client/buffer/buffer.go31
-rw-r--r--client/channel_window.go189
-rw-r--r--client/cmd_buffer.go77
-rw-r--r--client/cmd_window.go134
-rw-r--r--client/command.go78
-rw-r--r--client/empty_window.go29
-rw-r--r--client/main.go23
-rw-r--r--client/object/object.go1
-rw-r--r--client/ui.go86
-rw-r--r--client/window/window.go38
11 files changed, 713 insertions, 132 deletions
diff --git a/client/application.go b/client/application.go
index 72ec078..b1c5865 100644
--- a/client/application.go
+++ b/client/application.go
@@ -3,45 +3,49 @@ package main
import (
"citrons.xyz/talk/client/client"
"citrons.xyz/talk/client/object"
- "citrons.xyz/talk/client/buffer"
+ "citrons.xyz/talk/client/window"
"citrons.xyz/talk/proto"
- "citrons.xyz/talk/tui"
- "zgo.at/termfo/keys"
- "os"
)
type application struct {
client.Client
+ quit bool
+ connected bool
reconnecting bool
authenticated bool
uid string
cache object.ObjCache
- currentBuffer *buffer.Buffer
- cmdBuffer cmdBuffer
+ windowCache window.WindowCache
+ currentWindow window.Location
+ cmdWindow cmdWindow
}
func newApplication(serverAddress string) *application {
var app application
app.Client = client.New(serverAddress)
+ app.currentWindow = app.cmdWindow.Location()
app.cache = object.NewCache(&app)
- app.currentBuffer = &app.cmdBuffer.Buffer
+ app.windowCache = window.NewCache()
- app.cmdBuffer.info("connecting to %s", app.Client.Address)
+ app.cmdWindow.info("connecting to %s", app.Client.Address)
return &app
}
func (a *application) OnConnect() {
+ a.connected = true
a.reconnecting = false
- a.cmdBuffer.info("connected to %s", a.Client.Address)
-
- a.auth("test user")
+ a.cache = object.NewCache(a)
+ a.windowCache = window.NewCache()
+ a.cmdWindow.info("connected to %s", a.Client.Address)
+ a.cmdWindow.loginMode()
}
func (a *application) OnDisconnect(err error) {
+ a.connected = false
a.authenticated = false
a.uid = ""
if !a.reconnecting {
- a.cmdBuffer.err(
+ a.cmdWindow.err(
"disconnected from %s: %s\nreconnecting...", a.Client.Address, err,
)
a.reconnecting = true
@@ -50,6 +54,11 @@ func (a *application) OnDisconnect(err error) {
func (a *application) OnEvent(cmd proto.Command) {
switch cmd.Kind {
+ case "p":
+ cw := a.windowCache.Get(channelLocation {cmd.Target}).(*channelWindow)
+ if cw != nil && len(cmd.Args) > 0 {
+ cw.put(cmd.Args[0])
+ }
case "update":
if len(cmd.Args) > 0 {
a.cache.Update(cmd.Target, cmd.Args[0])
@@ -63,7 +72,7 @@ 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[""])
+ a.cmdWindow.info("your name is: %s", cmd.Args[0].Fields[""])
}
}
}
@@ -84,19 +93,28 @@ func (a *application) GetInfo(id string, callback func(proto.Command)) {
a.Request(proto.NewCmd("i", id), callback)
}
-func (a *application) auth(name string) {
+func (a *application) auth(name string, authCallback func(success bool)) {
callback := func(response proto.Command) {
switch response.Kind {
case "you-are":
if len(response.Args) == 0 {
+ authCallback(false)
break
}
me := response.Args[0]
a.authenticated = true
a.uid = me.Id
a.cache.Watch(a.uid)
+ if authCallback != nil {
+ authCallback(true)
+ }
case "fail":
- // todo
+ if len(response.Args) != 0 {
+ a.cmdWindow.err(proto.Strfail(response.Args[0]))
+ }
+ if authCallback != nil {
+ authCallback(false)
+ }
}
}
a.Request(proto.NewCmd("auth", "", proto.Object {
@@ -104,39 +122,98 @@ func (a *application) auth(name string) {
}), callback)
}
-func (a *application) onInput(ev tui.Event) {
- tui.Selected = "input"
+func (a *application) sendUpdate(o proto.Object, cb func(proto.Command)) {
+ a.Request(proto.NewCmd("update", o.Id, o), func(response proto.Command) {
+ if response.Kind == "ok" {
+ a.cache.Update(o.Id, o)
+ }
+ cb(response)
+ })
+}
- a.currentBuffer.Scroll(-ev.Mouse.Scroll * 2)
- scroll := tui.Size().Height - 5
- switch ev.Key {
- case keys.PageUp:
- a.currentBuffer.Scroll(scroll)
- case keys.PageDown:
- a.currentBuffer.Scroll(-scroll)
+func (a *application) lookup(
+ name string, kind string, callback func(*proto.Object, *proto.Fail)) {
+ cb := func(response proto.Command) {
+ switch response.Kind {
+ case "i":
+ if len(response.Args) > 0 {
+ callback(&response.Args[0], nil)
+ }
+ case "fail":
+ if len(response.Args) > 0 {
+ f := proto.Fail(response.Args[0])
+ callback(nil, &f)
+ }
+ }
}
-
- a.currentBuffer.TextInput.Update(ev)
+ o := proto.Object {kind, "", map[string]string {"": name}}
+ a.Request(proto.NewCmd("lookup", "", o), cb)
}
-func (a *application) show() {
- tui.Clear()
- s := tui.Size()
- tui.Push("", tui.Box {
- Width: tui.BoxSize(s.Width), Height: tui.BoxSize(s.Height),
+func (a *application) join(channelName string) {
+ a.lookup(channelName, "channel", func(ch *proto.Object, f *proto.Fail) {
+ if f != nil {
+ a.cmdWindow.err(f.Error())
+ return
+ }
+ a.Request(proto.NewCmd("join", ch.Id), func(response proto.Command) {
+ switch response.Kind {
+ case "ok":
+ a.currentWindow = channelLocation {id: ch.Id}
+ case "fail":
+ if len(response.Args) > 0 {
+ f := proto.Fail(response.Args[0])
+ a.cmdWindow.err(f.Error())
+ }
+ }
+ })
})
+}
- a.currentBuffer.Show("buffer")
- tui.Push("status", tui.Box {
- Width: tui.Fill, Height: tui.BoxSize(1), Dir: tui.Right,
- Style: &tui.Style {Bg: tui.White, Fg: tui.Black},
+func (a *application) createChannel(name string) {
+ ch := proto.Object {"channel", "", map[string]string {"": name}}
+ a.Request(proto.NewCmd("create", "", ch), func(response proto.Command) {
+ switch response.Kind {
+ case "create":
+ if len(response.Args) > 0 {
+ ch := response.Args[0]
+ a.currentWindow = channelLocation {id: ch.Id}
+ }
+ case "fail":
+ if len(response.Args) > 0 {
+ f := proto.Fail(response.Args[0])
+ a.cmdWindow.err(f.Error())
+ }
+ }
})
- tui.Pop()
- a.currentBuffer.TextInput.Show("input")
+}
+
+func (a *application) getNick() string {
+ if !a.authenticated {
+ return ""
+ }
+ u := a.cache.Get(a.uid)
+ if u != nil {
+ return u.Fields[""]
+ }
+ return ""
+}
- tui.Pop()
- tui.DrawLayout()
- if tui.Present() != nil {
- os.Exit(-1)
+func (a *application) setNick(newName string) {
+ if !a.authenticated {
+ return
+ }
+ callback := func(response proto.Command) {
+ switch response.Kind {
+ case "fail":
+ if len(response.Args) != 0 {
+ a.cmdWindow.err(proto.Strfail(response.Args[0]))
+ }
+ case "ok":
+ a.cmdWindow.info("your name is: %s", newName)
+ }
}
+ a.sendUpdate(
+ proto.Object {"u", a.uid, map[string]string {"": newName}}, callback,
+ )
}
diff --git a/client/buffer/buffer.go b/client/buffer/buffer.go
index 230fba9..fea88a9 100644
--- a/client/buffer/buffer.go
+++ b/client/buffer/buffer.go
@@ -5,10 +5,9 @@ import (
)
type Buffer struct {
- top *bufList
- bottom *bufList
+ top *List
+ bottom *List
scroll tui.ScrollState
- TextInput tui.TextInput
Closed bool
}
@@ -17,15 +16,31 @@ type Message interface {
Show(odd bool)
}
-type bufList struct {
+type List struct {
msg Message
odd bool
- prev *bufList
- next *bufList
+ prev *List
+ next *List
+}
+
+func (l List) Prev() *List {
+ return l.prev
+}
+
+func (l List) Next() *List {
+ return l.next
+}
+
+func (b *Buffer) Top() *List {
+ return b.top
+}
+
+func (b *Buffer) Bottom() *List {
+ return b.bottom
}
func (b *Buffer) Add(msg Message) {
- l := &bufList {msg: msg}
+ l := &List {msg: msg}
if b.bottom != nil {
b.bottom.next = l
l.prev = b.bottom
@@ -38,7 +53,7 @@ func (b *Buffer) Add(msg Message) {
}
func (b *Buffer) AddTop(msg Message) {
- l := bufList {msg: msg}
+ l := List {msg: msg}
if b.top != nil {
b.top.prev = &l
l.next = b.top
diff --git a/client/channel_window.go b/client/channel_window.go
new file mode 100644
index 0000000..abeaa77
--- /dev/null
+++ b/client/channel_window.go
@@ -0,0 +1,189 @@
+package main
+
+import (
+ "citrons.xyz/talk/client/buffer"
+ "citrons.xyz/talk/client/window"
+ "citrons.xyz/talk/proto"
+ "citrons.xyz/talk/tui"
+ "strconv"
+ "time"
+)
+
+type channelLocation struct {
+ id string
+}
+
+type channelWindow struct {
+ location channelLocation
+ buf buffer.Buffer
+ input tui.TextInput
+ watchedUsers map[string]bool
+ loadingHistory bool
+}
+
+type channelMsg struct {
+ proto.Object
+ window *channelWindow
+}
+
+func (cl channelLocation) CreateWindow() window.Window {
+ if !globalApp.connected {
+ return nil
+ }
+ cw := &channelWindow {
+ location: cl, watchedUsers: make(map[string]bool),
+ }
+ globalApp.cache.Watch(cl.id)
+ return cw
+}
+
+func (cw *channelWindow) getChannel() *proto.Object {
+ return globalApp.cache.Get(cw.location.id)
+}
+
+func (cw *channelWindow) watchUser(uid string) {
+ if !cw.watchedUsers[uid] {
+ globalApp.cache.Watch(uid)
+ cw.watchedUsers[uid] = true
+ }
+}
+
+func (cw *channelWindow) username(uid string) string {
+ cw.watchUser(uid)
+ u := globalApp.cache.Get(uid)
+ if u != nil {
+ return u.Fields[""]
+ }
+ return "..."
+}
+
+func (cw *channelWindow) put(msg proto.Object) {
+ cw.buf.Add(channelMsg {msg, cw})
+}
+
+func (cw *channelWindow) Location() window.Location {
+ return cw.location
+}
+
+func (cw *channelWindow) Kill() {
+ globalApp.cache.Unwatch(cw.location.id)
+ for u := range cw.watchedUsers {
+ globalApp.cache.Watch(u)
+ }
+}
+
+func (cw *channelWindow) Buffer() *buffer.Buffer {
+ return &cw.buf
+}
+
+func (cw *channelWindow) Input() *tui.TextInput {
+ return &cw.input
+}
+
+func (cw *channelWindow) Send(text string) {
+ if !globalApp.connected || !globalApp.authenticated {
+ return
+ }
+ cb := func(response proto.Command) {
+ switch response.Kind {
+ case "p":
+ if len(response.Args) > 0 {
+ cw.put(response.Args[0])
+ }
+ case "fail":
+ if len(response.Args) > 0 {
+ f := proto.Fail(response.Args[0])
+ globalApp.cmdWindow.err(f.Error())
+ }
+ }
+ }
+ msg := proto.Object {"m", "", map[string]string {"": text}}
+ globalApp.Request(proto.NewCmd("p", cw.location.id, msg), cb)
+ cw.input.SetText("")
+}
+
+func (cw *channelWindow) ShowStatusLine() {
+ ch := cw.getChannel()
+ if ch == nil {
+ return
+ }
+ tui.Text(ch.Fields[""], nil)
+ if cw.loadingHistory {
+ tui.Text("...", nil)
+ }
+}
+
+func (m channelMsg) Id() string {
+ return m.Object.Id
+}
+
+func (m channelMsg) showDate(bg int32) {
+ unix, _ := strconv.Atoi(m.Fields["t"])
+ time := time.Unix(int64(unix), 0).Format(time.DateTime)
+ tui.Push("", tui.Box {Width: tui.TextSize, Height: 1, NoWrap: true})
+ tui.Text(" " + time, &tui.Style {Fg: tui.BrightBlack, Bg: bg})
+ tui.Pop()
+ tui.Push("", tui.Box {Width: tui.Fill, Height: 1})
+ tui.Pop()
+}
+
+func (m channelMsg) Show(odd bool) {
+ var bg int32 = tui.Black
+ if odd {
+ bg = 236
+ }
+
+ switch m.Kind {
+ case "join":
+ tui.Push("", tui.Box {
+ Width: tui.Fill, Height: tui.Children, Dir: tui.Left,
+ })
+ m.showDate(bg)
+ tui.Push("", tui.Box {Width: tui.TextSize, Height: tui.TextSize})
+ tui.Text("-> ", &tui.Style {Fg: tui.BrightBlack, Bg: bg})
+ tui.Text("join", &tui.Style {Fg: tui.Blue, Bg: bg, Bold: true})
+ tui.Text(": ", &tui.Style {Fg: tui.BrightBlack, Bg: bg})
+ tui.Text(m.window.username(m.Fields[""]), nil)
+ tui.Pop()
+ tui.Pop()
+ case "leave":
+ tui.Push("", tui.Box {
+ Width: tui.Fill, Height: tui.Children, Dir: tui.Left,
+ })
+ m.showDate(bg)
+ tui.Push("", tui.Box {Width: tui.TextSize, Height: tui.TextSize})
+ tui.Text("<- ", &tui.Style {Fg: tui.BrightBlack, Bg: bg})
+ tui.Text("leave", &tui.Style {Fg: tui.Blue, Bg: bg, Bold: true})
+ tui.Text(": ", &tui.Style {Fg: tui.BrightBlack, Bg: bg})
+ tui.Text(m.window.username(m.Fields[""]), nil)
+ tui.Pop()
+ tui.Pop()
+ case "m":
+ var usernameFg int32 = tui.White
+ if m.Fields["f"] == globalApp.uid {
+ usernameFg = tui.Cyan
+ }
+
+ tui.Push("", tui.Box {Width: tui.Fill, Height: 1, Dir: tui.Left})
+ m.showDate(bg)
+ tui.Push("", tui.Box {Width: tui.TextSize, Height: 1, NoWrap: true})
+ tui.Text(m.window.username(m.Fields["f"]), &tui.Style {
+ Fg: usernameFg, Bg: bg, Bold: true,
+ })
+ tui.Pop()
+ tui.Pop()
+
+ tui.Push("", tui.Box {
+ Width: tui.TextSize, Height: tui.TextSize,
+ Margins: [4]int {1, 0, 0, 0},
+ })
+ tui.Text(m.Fields[""], nil)
+ tui.Pop()
+ default:
+ tui.Push("", tui.Box {Width: tui.Fill, Height: tui.TextSize})
+ tui.Text("[", nil)
+ tui.Text(m.Kind, &tui.Style {Fg: tui.White, Bg: bg, Italic: true})
+ tui.Text("]", nil)
+ tui.Pop()
+ }
+}
diff --git a/client/cmd_buffer.go b/client/cmd_buffer.go
deleted file mode 100644
index ae96a0f..0000000
--- a/client/cmd_buffer.go
+++ /dev/null
@@ -1,77 +0,0 @@
-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/cmd_window.go b/client/cmd_window.go
new file mode 100644
index 0000000..6a69bb9
--- /dev/null
+++ b/client/cmd_window.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "citrons.xyz/talk/client/buffer"
+ "citrons.xyz/talk/client/window"
+ "citrons.xyz/talk/tui"
+ "fmt"
+)
+
+type cmdWindowLocation struct {}
+
+type cmdWindow struct {
+ buf buffer.Buffer
+ input tui.TextInput
+ login bool
+}
+
+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.White}
+ 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 (l cmdWindowLocation) CreateWindow() window.Window {
+ return &globalApp.cmdWindow
+}
+
+func (w *cmdWindow) Location() window.Location {
+ return cmdWindowLocation {}
+}
+
+func (w *cmdWindow) Kill() {}
+
+func (w *cmdWindow) Buffer() *buffer.Buffer {
+ return &w.buf
+}
+
+func (w *cmdWindow) Input() *tui.TextInput {
+ return &w.input
+}
+
+func (w *cmdWindow) Send(text string) {
+ if w.login {
+ w.login = false
+ previousText := w.input.Text()
+ w.input.SetText("")
+ globalApp.auth(text, func(success bool) {
+ if !success {
+ w.loginMode()
+ w.input.SetText(previousText)
+ }
+ })
+ }
+}
+
+func (w *cmdWindow) ShowStatusLine() {
+ if !w.login {
+ tui.Text("command window", &tui.Style {
+ Bg: tui.White, Fg: tui.Black, Italic: true,
+ })
+ } else {
+ tui.Text("[", nil)
+ tui.Text("login", &tui.Style {
+ Bg: tui.White, Fg: tui.Blue, Bold: true,
+ })
+ tui.Text("]", nil)
+ tui.Text(" username:", nil)
+ }
+}
+
+func (w *cmdWindow) loginMode() {
+ w.login = true
+ w.input.SetText("")
+}
+
+func (w *cmdWindow) info(f string, a ...any) {
+ lastIndex++
+ w.buf.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logInfo})
+}
+
+func (w *cmdWindow) err(f string, a ...any) {
+ lastIndex++
+ w.buf.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logErr})
+}
+
+func (w *cmdWindow) cmd(f string, a ...any) {
+ lastIndex++
+ w.buf.Add(logMsg {lastIndex, fmt.Sprintf(f, a...), logCmd})
+}
diff --git a/client/command.go b/client/command.go
new file mode 100644
index 0000000..f505f9a
--- /dev/null
+++ b/client/command.go
@@ -0,0 +1,78 @@
+package main
+
+func isCommand(text string) (bool, string) {
+ if text[0] == '/' {
+ if len(text) > 1 && text[1] == '/' {
+ text = text[1:]
+ return false, text
+ }
+ return true, text
+ }
+ return false, text
+}
+
+func (a *application) processCommand(text string) {
+ text = text[1:]
+ args := []string {""}
+ escaped := false
+ quoted := false
+ for _, c := range text {
+ if escaped {
+ args[len(args) - 1] += string(c)
+ continue
+ }
+ switch c {
+ case '\\':
+ escaped = true
+ case '"':
+ quoted = !quoted
+ case ' ':
+ if !quoted {
+ if args[len(args) - 1] != "" {
+ args = append(args, "")
+ }
+ break
+ }
+ fallthrough
+ default:
+ args[len(args) - 1] += string(c)
+ }
+ }
+ if len(text) > len(args[0]) {
+ text = text[len(args[0]) + 1:]
+ } else {
+ text = ""
+ }
+ a.doCommand(args[0], args[1:], text)
+}
+
+func (a *application) doCommand(command string, args []string, text string) {
+ if !a.connected {
+ return
+ }
+ argN := func(n int) {
+ if len(args) != n {
+ a.cmdWindow.err(
+ "%s: expected %d arguments, was %d", command, n, len(args),
+ )
+ }
+ }
+ switch command {
+ case "nick":
+ a.setNick(text)
+ case "join":
+ argN(1)
+ if a.authenticated {
+ a.join(args[0])
+ }
+ case "create":
+ argN(1)
+ if a.authenticated {
+ a.createChannel(args[0])
+ }
+ case "quit":
+ a.quit = true
+ default:
+ a.cmdWindow.err("unknown command: /" + command)
+ }
+}
diff --git a/client/empty_window.go b/client/empty_window.go
new file mode 100644
index 0000000..e40ab96
--- /dev/null
+++ b/client/empty_window.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "citrons.xyz/talk/client/window"
+ "citrons.xyz/talk/client/buffer"
+ "citrons.xyz/talk/tui"
+)
+
+type emptyWindow struct {
+ location window.Location
+}
+
+func (w emptyWindow) Location() window.Location {
+ return w.location
+}
+
+func (w emptyWindow) Kill() {}
+
+func (w emptyWindow) Buffer() *buffer.Buffer {
+ return &buffer.Buffer {}
+}
+
+func (w emptyWindow) Input() *tui.TextInput {
+ return &tui.TextInput {}
+}
+
+func (w emptyWindow) Send(text string) {}
+
+func (w emptyWindow) ShowStatusLine() {}
diff --git a/client/main.go b/client/main.go
index 4c65752..a9929be 100644
--- a/client/main.go
+++ b/client/main.go
@@ -7,30 +7,41 @@ import (
"os"
)
+var globalApp *application
+
func main() {
err := tui.Start()
if err != nil {
fmt.Fprintln(os.Stderr, "error initializing terminal: ", err)
+ os.Exit(-1)
}
+ defer func() {
+ tui.End()
+ fmt.Println("bye!")
+ }()
- app := newApplication("localhost:27508")
- go app.RunClient()
+ globalApp = newApplication("localhost:27508")
+ go globalApp.RunClient()
+ defer globalApp.Stop()
drawTick := time.Tick(time.Second / 60)
redraw := true
for {
select {
- case m := <-app.Messages():
- m.Handle(app)
+ case m := <-globalApp.Messages():
+ m.Handle(globalApp)
redraw = true
case e := <-tui.Events():
- app.onInput(e)
+ globalApp.onInput(e)
redraw = true
case <-drawTick:
if redraw {
- app.show()
+ globalApp.show()
redraw = false
}
}
+ if globalApp.quit == true {
+ return
+ }
}
}
diff --git a/client/object/object.go b/client/object/object.go
index f42eaf8..5215993 100644
--- a/client/object/object.go
+++ b/client/object/object.go
@@ -52,6 +52,7 @@ func (oc *ObjCache) Watch(id string) {
if !ok {
oc.stream.Sub(id)
}
+ entry.refCount++
oc.objects[id] = entry
}
diff --git a/client/ui.go b/client/ui.go
new file mode 100644
index 0000000..386645e
--- /dev/null
+++ b/client/ui.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "citrons.xyz/talk/client/window"
+ "citrons.xyz/talk/tui"
+ "zgo.at/termfo/keys"
+ "os"
+)
+
+func (a *application) getWin() window.Window {
+ win := a.windowCache.Open(a.currentWindow)
+ if win == nil {
+ return emptyWindow {a.currentWindow}
+ }
+ return win
+}
+
+func (a *application) onInput(ev tui.Event) {
+ tui.Selected = "input"
+
+ win := a.getWin()
+ win.Input().Update(ev)
+
+ buf := win.Buffer()
+ buf.Scroll(-ev.Mouse.Scroll * 2)
+ scroll := tui.Size().Height - 5
+ switch ev.Key {
+ case keys.PageUp:
+ buf.Scroll(scroll)
+ case keys.PageDown:
+ buf.Scroll(-scroll)
+ case keys.Enter:
+ input := win.Input()
+ if !input.IsEmpty() {
+ is, text := isCommand(input.Text())
+ if !is {
+ win.Send(text)
+ } else {
+ a.processCommand(text)
+ input.SetText("")
+ }
+ }
+ case 'h' | keys.Ctrl:
+ a.currentWindow = cmdWindowLocation {}
+ }
+
+}
+
+func (a *application) showNickBox() {
+ tui.Push("username", tui.Box {Width: tui.TextSize, Height: tui.TextSize})
+ tui.Text("[", nil)
+ name := a.getNick()
+ name = string([]rune(name)[:8])
+ tui.Text(name, nil)
+ tui.Text("] ", nil)
+ tui.Pop()
+}
+
+func (a *application) show() {
+ tui.Clear()
+ s := tui.Size()
+ tui.Push("", tui.Box {
+ Width: tui.BoxSize(s.Width), Height: tui.BoxSize(s.Height),
+ })
+
+ a.getWin().Buffer().Show("buffer")
+ tui.Push("status", tui.Box {
+ Width: tui.Fill, Height: tui.BoxSize(1), Dir: tui.Right,
+ Style: &tui.Style {Bg: tui.White, Fg: tui.Black},
+ })
+ a.getWin().ShowStatusLine()
+ tui.Pop()
+
+ tui.Push("input container", tui.Box {
+ Width: tui.Fill, Height: tui.Children, Dir: tui.Right,
+ })
+ a.showNickBox()
+ a.getWin().Input().Show("input")
+ tui.Pop()
+
+ tui.Pop()
+ tui.DrawLayout()
+ if tui.Present() != nil {
+ os.Exit(-1)
+ }
+} \ No newline at end of file
diff --git a/client/window/window.go b/client/window/window.go
new file mode 100644
index 0000000..a877f7b
--- /dev/null
+++ b/client/window/window.go
@@ -0,0 +1,38 @@
+package window
+
+import (
+ "citrons.xyz/talk/client/buffer"
+ "citrons.xyz/talk/tui"
+)
+
+type Location interface {
+ CreateWindow() Window
+}
+
+type Window interface {
+ Location() Location
+ Kill()
+ Buffer() *buffer.Buffer
+ Input() *tui.TextInput
+ Send(text string)
+ ShowStatusLine()
+}
+
+type WindowCache struct {
+ windows map[Location]Window
+}
+
+func NewCache() WindowCache {
+ return WindowCache {make(map[Location]Window)}
+}
+
+func (wc *WindowCache) Open(l Location) Window {
+ if wc.windows[l] == nil {
+ wc.windows[l] = l.CreateWindow()
+ }
+ return wc.windows[l]
+}
+
+func (wc *WindowCache) Get(l Location) Window {
+ return wc.windows[l]
+}