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