first working commit
This commit is contained in:
commit
68525224d5
9 changed files with 633 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/chat.log
|
||||||
|
/nanite
|
||||||
14
README.md
Normal file
14
README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# nanite
|
||||||
|
|
||||||
|
it ony a [nanochat] client!
|
||||||
|
wrote this while simultaneously learning how to use [vaxis] and it sure was an experience
|
||||||
|
|
||||||
|
## use it
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go build .
|
||||||
|
$ ./nanite very.real-server.com
|
||||||
|
```
|
||||||
|
|
||||||
|
[nanochat]: https://git.phial.org/d6/nanochat
|
||||||
|
[vaxis]: https://git.sr.ht/~rockorager/vaxis
|
||||||
74
command.go
Normal file
74
command.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) Send(data string) (num int, err error) {
|
||||||
|
if _, err := fmt.Fprintf(app.conn, "SEND %s\n", data); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.scanner.Scan() {
|
||||||
|
return 0, app.scanner.Err()
|
||||||
|
}
|
||||||
|
numRaw := app.scanner.Text()
|
||||||
|
num, err = strconv.Atoi(numRaw)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Poll(since int) (num int, err error) {
|
||||||
|
if _, err := fmt.Fprintf(app.conn, "POLL %d\n", since); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.scanner.Scan() {
|
||||||
|
return 0, app.scanner.Err()
|
||||||
|
}
|
||||||
|
numRaw := app.scanner.Text()
|
||||||
|
num, err = strconv.Atoi(numRaw)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Last(n int) (err error) {
|
||||||
|
if _, err := fmt.Fprintf(app.conn, "LAST %d\n", n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsrv int
|
||||||
|
if !app.scanner.Scan() {
|
||||||
|
return app.scanner.Err()
|
||||||
|
}
|
||||||
|
nsrvRaw := app.scanner.Text()
|
||||||
|
nsrv, err = strconv.Atoi(nsrvRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for range nsrv {
|
||||||
|
if !app.scanner.Scan() {
|
||||||
|
return app.scanner.Err()
|
||||||
|
}
|
||||||
|
app.incoming <- Message(app.scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
var last int
|
||||||
|
if !app.scanner.Scan() {
|
||||||
|
return app.scanner.Err()
|
||||||
|
}
|
||||||
|
lastRaw := app.scanner.Text()
|
||||||
|
last, err = strconv.Atoi(lastRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.incoming <- SetLast(last)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
96
display.go
Normal file
96
display.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"codeberg.org/lobo/nanite/widgets/pager"
|
||||||
|
"git.sr.ht/~rockorager/vaxis"
|
||||||
|
"git.sr.ht/~rockorager/vaxis/widgets/textinput"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) InitUI() error {
|
||||||
|
var err error
|
||||||
|
app.vx, err = vaxis.New(vaxis.Options{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.input = textinput.New()
|
||||||
|
app.pager = &pager.Model{
|
||||||
|
WrapMode: pager.WrapWords,
|
||||||
|
}
|
||||||
|
app.resize()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) FinishUI() {
|
||||||
|
app.vx.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) resize() {
|
||||||
|
app.dirty = true
|
||||||
|
|
||||||
|
win := app.vx.Window()
|
||||||
|
app.w.log = win.New(0, 1, win.Width, win.Height-3)
|
||||||
|
app.w.title = win.New(0, 0, win.Width, 1)
|
||||||
|
app.w.status = win.New(0, win.Height-2, win.Width, 1)
|
||||||
|
app.w.input = win.New(0, win.Height-1, win.Width, 1)
|
||||||
|
app.pager.Offset = math.MaxInt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Redraw() {
|
||||||
|
if !app.dirty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.dirty = false
|
||||||
|
|
||||||
|
// set window title and draw titlebar
|
||||||
|
app.w.title.Clear()
|
||||||
|
titlebarStyle := vaxis.Style{Attribute: vaxis.AttrBold}
|
||||||
|
|
||||||
|
if app.conn != nil {
|
||||||
|
app.vx.SetTitle(fmt.Sprintf("nanite (%s:%s)", app.host, app.port))
|
||||||
|
app.w.title.PrintTruncate(0,
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: "• ",
|
||||||
|
Style: titlebarStyle,
|
||||||
|
},
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: app.host,
|
||||||
|
Style: titlebarStyle,
|
||||||
|
},
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: ":",
|
||||||
|
Style: titlebarStyle,
|
||||||
|
},
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: app.port,
|
||||||
|
Style: titlebarStyle,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
app.vx.SetTitle("nanite (disconnected)")
|
||||||
|
app.w.title.PrintTruncate(0,
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: "✖ (disconnected)",
|
||||||
|
Style: titlebarStyle,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw statusbar
|
||||||
|
statusStyle := vaxis.Style{Attribute: vaxis.AttrReverse}
|
||||||
|
app.w.status.Fill(vaxis.Cell{Style: statusStyle})
|
||||||
|
app.w.status.PrintTruncate(0,
|
||||||
|
vaxis.Segment{
|
||||||
|
Text: app.nick,
|
||||||
|
Style: statusStyle,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// let the widgets draw themselves
|
||||||
|
app.pager.Layout()
|
||||||
|
app.pager.Draw(app.w.log)
|
||||||
|
app.input.Draw(app.w.input)
|
||||||
|
app.vx.Render()
|
||||||
|
}
|
||||||
32
event.go
Normal file
32
event.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type IncomingEvent interface {
|
||||||
|
HandleIncoming(*App)
|
||||||
|
}
|
||||||
|
type OutgoingEvent interface {
|
||||||
|
HandleOutgoing(*App)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message string
|
||||||
|
|
||||||
|
func (m Message) HandleIncoming(app *App) {
|
||||||
|
app.AppendMessage(string(m))
|
||||||
|
}
|
||||||
|
func (m Message) HandleOutgoing(app *App) {
|
||||||
|
num, _ := app.Send(string(m))
|
||||||
|
app.incoming <- SetLast(num)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type Poll int
|
||||||
|
|
||||||
|
func (p Poll) HandleOutgoing(app *App) {
|
||||||
|
num, _ := app.Poll(int(p))
|
||||||
|
go app.Last(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetLast int
|
||||||
|
|
||||||
|
func (m SetLast) HandleIncoming(app *App) {
|
||||||
|
app.last = int(m)
|
||||||
|
}
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module codeberg.org/lobo/nanite
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.sr.ht/~rockorager/vaxis v0.15.0
|
||||||
|
github.com/rivo/uniseg v0.4.7
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-sixel v0.0.5 // indirect
|
||||||
|
github.com/soniakeys/quant v1.0.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
|
golang.org/x/image v0.9.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
)
|
||||||
59
go.sum
Normal file
59
go.sum
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
git.sr.ht/~rockorager/vaxis v0.15.0 h1:r9VUcDfKRNe9Rp988konQ1Gy8vbtpQajGSYXNAYx8hI=
|
||||||
|
git.sr.ht/~rockorager/vaxis v0.15.0/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
|
||||||
|
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||||
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8=
|
||||||
|
github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
|
||||||
|
github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
|
||||||
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||||
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
|
golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
|
||||||
|
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
167
main.go
Normal file
167
main.go
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/lobo/nanite/widgets/pager"
|
||||||
|
"git.sr.ht/~rockorager/vaxis"
|
||||||
|
"git.sr.ht/~rockorager/vaxis/widgets/textinput"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlreadyConnected = errors.New("already connected")
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
conn net.Conn
|
||||||
|
scanner *bufio.Scanner
|
||||||
|
host, port string
|
||||||
|
|
||||||
|
nick string
|
||||||
|
last int
|
||||||
|
ticker *time.Ticker
|
||||||
|
|
||||||
|
incoming chan IncomingEvent
|
||||||
|
outgoing chan OutgoingEvent
|
||||||
|
|
||||||
|
vx *vaxis.Vaxis
|
||||||
|
w struct {
|
||||||
|
log, title, status, input vaxis.Window
|
||||||
|
}
|
||||||
|
pager *pager.Model
|
||||||
|
input *textinput.Model
|
||||||
|
dirty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Connect(host, port string) (err error) {
|
||||||
|
if app.conn != nil {
|
||||||
|
return ErrAlreadyConnected
|
||||||
|
}
|
||||||
|
app.host = host
|
||||||
|
app.port = port
|
||||||
|
app.conn, err = net.Dial("tcp", net.JoinHostPort(host, port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.scanner = bufio.NewScanner(app.conn)
|
||||||
|
app.incoming = make(chan IncomingEvent)
|
||||||
|
app.outgoing = make(chan OutgoingEvent)
|
||||||
|
app.ticker = time.NewTicker(2 * time.Second)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev := <-app.outgoing:
|
||||||
|
ev.HandleOutgoing(app)
|
||||||
|
case <-app.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Disconnect() {
|
||||||
|
if app.conn != nil {
|
||||||
|
app.conn.Write([]byte("QUIT\n"))
|
||||||
|
app.conn.Close()
|
||||||
|
app.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) AppendMessage(data string) {
|
||||||
|
app.pager.Segments = append(app.pager.Segments,
|
||||||
|
vaxis.Segment{Text: data},
|
||||||
|
vaxis.Segment{Text: "\n"},
|
||||||
|
)
|
||||||
|
app.last += 1
|
||||||
|
app.pager.Offset = math.MaxInt
|
||||||
|
app.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) HandleTerminalEvent(ev vaxis.Event) {
|
||||||
|
app.dirty = true
|
||||||
|
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case vaxis.Resize:
|
||||||
|
app.resize()
|
||||||
|
case vaxis.Key:
|
||||||
|
if ev.MatchString("ctrl+c") {
|
||||||
|
app.stop()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ev.MatchString("up"):
|
||||||
|
app.pager.ScrollUp()
|
||||||
|
case ev.MatchString("down"):
|
||||||
|
app.pager.ScrollDown()
|
||||||
|
case ev.MatchString("ctrl+l"):
|
||||||
|
app.Redraw()
|
||||||
|
app.vx.Refresh()
|
||||||
|
app.dirty = false
|
||||||
|
case ev.MatchString("enter"):
|
||||||
|
if len(app.input.Characters()) != 0 {
|
||||||
|
message := fmt.Sprintf("%s: %s", app.nick, app.input.String())
|
||||||
|
app.AppendMessage(message)
|
||||||
|
app.outgoing <- Message(message)
|
||||||
|
app.input.SetContent("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.input.Update(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := os.Args
|
||||||
|
if len(args) < 2 || len(args) > 3 {
|
||||||
|
fmt.Printf("usage: %s host [port]\n", args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := "44322"
|
||||||
|
if len(args) == 3 {
|
||||||
|
port = args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
app := App{}
|
||||||
|
app.ctx, app.stop = context.WithCancel(context.Background())
|
||||||
|
defer app.stop()
|
||||||
|
|
||||||
|
if err := app.Connect(args[1], port); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer app.Disconnect()
|
||||||
|
|
||||||
|
app.InitUI()
|
||||||
|
defer app.FinishUI()
|
||||||
|
|
||||||
|
app.nick = "wolfdog"
|
||||||
|
go app.Last(20)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev := <-app.vx.Events():
|
||||||
|
app.HandleTerminalEvent(ev)
|
||||||
|
case ev := <-app.incoming:
|
||||||
|
ev.HandleIncoming(&app)
|
||||||
|
case <-app.ticker.C:
|
||||||
|
app.outgoing <- Poll(app.last)
|
||||||
|
case <-app.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.Redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
171
widgets/pager/pager.go
Normal file
171
widgets/pager/pager.go
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
// This is a "fork" of Vaxis' pager widget that adds Unicode line breaking.
|
||||||
|
// See <https://git.sr.ht/~rockorager/vaxis/tree/v0.15.0/item/widgets/pager/pager.go>.
|
||||||
|
|
||||||
|
package pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~rockorager/vaxis"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WrapFast = iota
|
||||||
|
WrapWords
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultFill = vaxis.Character{Grapheme: " "}
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
Segments []vaxis.Segment
|
||||||
|
lines []*line
|
||||||
|
Fill vaxis.Cell
|
||||||
|
Offset int
|
||||||
|
WrapMode int
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
type line struct {
|
||||||
|
characters []vaxis.Cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *line) append(t vaxis.Cell) {
|
||||||
|
l.characters = append(l.characters, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Draw(win vaxis.Window) {
|
||||||
|
w, h := win.Size()
|
||||||
|
if w != m.width {
|
||||||
|
m.width = w
|
||||||
|
m.Layout()
|
||||||
|
}
|
||||||
|
if len(m.lines)-m.Offset < h {
|
||||||
|
m.Offset = len(m.lines) - h
|
||||||
|
}
|
||||||
|
if m.Offset < 0 {
|
||||||
|
m.Offset = 0
|
||||||
|
}
|
||||||
|
if m.Fill.Grapheme == "" {
|
||||||
|
m.Fill.Character = defaultFill
|
||||||
|
}
|
||||||
|
win.Fill(m.Fill)
|
||||||
|
for row, l := range m.lines {
|
||||||
|
if row < m.Offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (row - m.Offset) >= h {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
col := 0
|
||||||
|
for _, cell := range l.characters {
|
||||||
|
win.SetCell(col, row-m.Offset, cell)
|
||||||
|
col += cell.Width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) layoutFast() {
|
||||||
|
l := &line{}
|
||||||
|
col := 0
|
||||||
|
for _, seg := range m.Segments {
|
||||||
|
for _, char := range vaxis.Characters(seg.Text) {
|
||||||
|
if strings.ContainsRune(char.Grapheme, '\n') {
|
||||||
|
m.lines = append(m.lines, l)
|
||||||
|
l = &line{}
|
||||||
|
col = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cell := vaxis.Cell{
|
||||||
|
Character: char,
|
||||||
|
Style: seg.Style,
|
||||||
|
}
|
||||||
|
l.append(cell)
|
||||||
|
col += char.Width
|
||||||
|
if col >= m.width {
|
||||||
|
m.lines = append(m.lines, l)
|
||||||
|
l = &line{}
|
||||||
|
col = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) layoutSlow() {
|
||||||
|
cols := m.width
|
||||||
|
col := 0
|
||||||
|
l := &line{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
state = -1
|
||||||
|
segment string
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, seg := range m.Segments {
|
||||||
|
rest := seg.Text
|
||||||
|
for len(rest) > 0 {
|
||||||
|
segment, rest, _, state = uniseg.FirstLineSegmentInString(rest, state)
|
||||||
|
chars := vaxis.Characters(segment)
|
||||||
|
total := 0
|
||||||
|
for _, char := range chars {
|
||||||
|
total += char.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case total > cols:
|
||||||
|
case total+col > cols:
|
||||||
|
m.lines = append(m.lines, l)
|
||||||
|
l = &line{}
|
||||||
|
col = 0
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range chars {
|
||||||
|
if uniseg.HasTrailingLineBreakInString(char.Grapheme) {
|
||||||
|
m.lines = append(m.lines, l)
|
||||||
|
l = &line{}
|
||||||
|
col = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cell := vaxis.Cell{
|
||||||
|
Character: char,
|
||||||
|
Style: seg.Style,
|
||||||
|
}
|
||||||
|
l.append(cell)
|
||||||
|
col += char.Width
|
||||||
|
if col >= cols {
|
||||||
|
m.lines = append(m.lines, l)
|
||||||
|
l = &line{}
|
||||||
|
col = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Layout() {
|
||||||
|
m.lines = []*line{}
|
||||||
|
switch m.WrapMode {
|
||||||
|
case WrapFast:
|
||||||
|
m.layoutFast()
|
||||||
|
case WrapWords:
|
||||||
|
m.layoutSlow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolls the pager down n lines, if it can
|
||||||
|
func (m *Model) ScrollDown() {
|
||||||
|
m.Offset += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ScrollUp() {
|
||||||
|
m.Offset -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ScrollDownN(n int) {
|
||||||
|
m.Offset += n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ScrollUpN(n int) {
|
||||||
|
m.Offset -= n
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue