nanite/main.go

235 lines
4.6 KiB
Go

package main
import (
"bufio"
"context"
"errors"
"fmt"
"math"
"net"
"os"
"strconv"
"strings"
"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")
)
var CommandMap = map[string]func(*App, string){
"nick": func(app *App, rest string) {
args := strings.Fields(rest)
switch len(args) {
case 0:
app.AppendSystemMessage("nick: your nickname is %s", app.nick)
default:
app.SetNick(args[0])
app.AppendSystemMessage("nick: your nickname is now %s", app.nick)
}
},
"poll": func(app *App, rest string) {
if rest == "" {
app.outgoing <- ManualPoll(app.last)
} else {
num, err := strconv.Atoi(rest)
if err != nil {
app.AppendSystemMessage("poll: invalid number %s", rest)
} else {
if num == 0 {
app.ticker.Stop()
app.rate = 0
app.AppendSystemMessage("poll: disabled automatic polling")
} else {
app.rate = time.Second * time.Duration(num)
app.ticker.Stop()
app.ticker = time.NewTicker(app.rate)
app.AppendSystemMessage("poll: polling every %s", app.rate.String())
}
}
}
},
"me": func(app *App, rest string) {
msg := fmt.Sprintf("%s %s", app.nick, rest)
app.AppendMessage(msg)
app.outgoing <- Message(msg)
},
"quit": func(app *App, rest string) {
app.stop()
},
"q": func(app *App, rest string) {
app.stop()
},
}
type App struct {
ctx context.Context
stop context.CancelFunc
conn net.Conn
scanner *bufio.Scanner
host, port string
stats string
nick string
last int
rate time.Duration
ticker *time.Ticker
incoming chan IncomingEvent
outgoing chan OutgoingEvent
error chan error
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, 256)
app.error = make(chan error)
// Calculate latency
now := time.Now()
_, err = app.Poll(0)
if err != nil {
app.Disconnect()
return err
}
delta := time.Since(now).Round(time.Second)
delta = min(max(time.Second, delta*3/2), 5*time.Second)
app.rate = delta
app.ticker = time.NewTicker(delta)
go func() {
app.outgoing <- Stat("")
app.Last(20)
for {
select {
case ev := <-app.outgoing:
if err := ev.HandleOutgoing(app); err != nil {
app.error <- err
return
}
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
}
if app.ticker != nil {
app.ticker.Stop()
app.ticker = nil
}
if app.incoming != nil {
close(app.incoming)
app.incoming = nil
}
if app.outgoing != nil {
close(app.outgoing)
app.outgoing = 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) AppendSystemMessage(format string, args ...any) {
st := vaxis.Style{Attribute: vaxis.AttrDim | vaxis.AttrItalic}
app.pager.Segments = append(app.pager.Segments,
vaxis.Segment{Text: "* ", Style: st},
vaxis.Segment{Text: fmt.Sprintf(format, args...), Style: st},
vaxis.Segment{Text: "\n"},
)
app.pager.Offset = math.MaxInt
app.dirty = true
}
func (app *App) SetNick(nick string) {
app.input.SetPrompt(fmt.Sprintf("%s: ", nick))
app.input.Prompt.Attribute = vaxis.AttrBold
app.nick = nick
}
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()
app.InitUI()
app.Redraw()
defer app.FinishUI()
if err := app.Connect(args[1], port); err != nil {
panic(err)
}
defer app.Disconnect()
app.SetNick("wolfdog")
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 err := <-app.error:
app.FinishUI()
panic(err)
case <-app.ctx.Done():
return
}
app.Redraw()
}
}