handle offline status correctly and other changes

This commit is contained in:
Lobo 2025-10-29 12:50:59 -03:00
parent dcc018efec
commit 6c66b32396
7 changed files with 93 additions and 81 deletions

View file

@ -1,45 +1,30 @@
# nanite # nanite
`nanite` is a terminal [Nanochat] client. `nanite` is a terminal client for the [Nanochat] protocol.
> Note: I've moved this to my personal Forgejo instance. The upstream URL Requires Go 1.24.4 or higher. Build using `make build`.
> is now https://git.rhzm.org/lobo/nanite :)
![](./assets/screenshot.png) > The upstream URL for this repository is <https://git.rhzm.org/lobo/nanite>.
> The repositories hosted on GitHub and Codeberg are mirrors.
## build # keybindings
```
$ go build .
```
## usage
```
$ ./nanite
usage: ./nanite host [port]
$ ./nanite very.real-server.com
```
keybindings:
- `Ctrl+C`: quit - `Ctrl+C`: quit
- `Ctrl+L`: refresh screen - `Ctrl+L`: refresh screen
- `Ctrl+P`: poll - `Ctrl+P`: poll
- Emacs-like bindings for text editing
commands: # commands
- `/dial hostname`: connect to server - `/clear`: clear message log
- `/hangup`: disconnect - `/dial host [port]`: connect to server
- `/q`, `/quit`: quit - `/hangup`: disconnect from server
- `/nick [nickname]`: change nick, if no arguments, show current nick - `/help`: see command list
- `/me`: IRC `/me` alike - `/me ...`: send IRC-style `/me` message
- `/poll [seconds]`: change polling interval, if no arguments, poll manually - `/nick [nickname]`: change nickname or see current nickname if no arguments given
- `/poll [time]`: set polling rate to time or poll manually if no arguments given
## won't support (yet) - `/quit`: self-explanatory (aliased to `/q`)
- `/script [name]`: run script or see script list if no arguments given (aliased to `/.`)
- sixel (tried, it seems to be complicated to get it to work with Vaxis' pager - `/send ...`: send raw message
widget)
[Nanochat]: https://git.phial.org/d6/nanochat [Nanochat]: https://git.phial.org/d6/nanochat
[Vaxis]: https://git.sr.ht/~rockorager/vaxis

View file

@ -2,8 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"maps"
"os" "os"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -12,15 +14,13 @@ import (
type Command func(*App, string) type Command func(*App, string)
var CommandMap map[string]Command var CommandMap map[string]Command
var AliasMap map[string]string
func initCommandMap() { func initCommandMap() {
CommandMap = map[string]Command{ CommandMap = map[string]Command{
"help": func(app *App, rest string) { "help": func(app *App, rest string) {
var s strings.Builder var s strings.Builder
for name, _ := range CommandMap { for _, name := range slices.Sorted(maps.Keys(CommandMap)) {
if name == "q" {
continue
}
s.WriteString(name) s.WriteString(name)
s.WriteRune(' ') s.WriteRune(' ')
} }
@ -28,26 +28,28 @@ func initCommandMap() {
}, },
"script": func(app *App, rest string) { "script": func(app *App, rest string) {
if rest == "" { if rest == "" {
if err := app.RefreshScripts(); err != nil {
app.AppendSystemMessage("failed to refresh scripts")
}
var s strings.Builder var s strings.Builder
for _, script := range app.scripts { for _, script := range slices.Sorted(maps.Keys(app.scripts)) {
s.WriteString(script) s.WriteString(script)
s.WriteRune(' ') s.WriteRune(' ')
} }
app.AppendSystemMessage("scripts: %s", s.String()) app.AppendSystemMessage("scripts: %s", s.String())
} else { } else {
if err := app.LoadScript(rest); err != nil { if err := app.LoadScript(rest); err != nil {
app.AppendSystemMessage("error loading script `%s`: %s", rest, err) app.AppendSystemMessage("error loading script \"%s\": %s", rest, err)
} }
} }
}, },
"send": func(app *App, rest string) { "send": func(app *App, rest string) {
app.AppendMessage(rest)
app.outgoing <- MessageEvent(rest) app.outgoing <- MessageEvent(rest)
}, },
"dial": func(app *App, rest string) { "dial": func(app *App, rest string) {
args := strings.Fields(rest) args := strings.Fields(rest)
if len(args) < 1 || len(args) > 2 { if len(args) < 1 || len(args) > 2 {
app.AppendSystemMessage("usage: /connect host [port]") app.AppendSystemMessage("usage: /dial host [port]")
} }
host := args[0] host := args[0]
port := "44322" port := "44322"
@ -61,12 +63,12 @@ func initCommandMap() {
}, },
"nick": func(app *App, rest string) { "nick": func(app *App, rest string) {
if rest == "" { if rest == "" {
app.AppendSystemMessage("nick: your nickname is %s", app.nick) app.AppendSystemMessage("your nickname is %s", app.nick)
} else { } else {
app.SetNick(rest) app.SetNick(rest)
app.AppendSystemMessage("nick: your nickname is now %s", app.nick) app.AppendSystemMessage("your nickname is now %s", app.nick)
if err := os.WriteFile(path.Join(app.cfgHome, "nick"), []byte(rest), 0o600); err != nil { if err := os.WriteFile(path.Join(app.cfgHome, "nick"), []byte(rest), 0o600); err != nil {
app.AppendSystemMessage("nick: failed to persist nickname: %s", err) app.AppendSystemMessage("failed to persist nickname: %s", err)
} }
} }
}, },
@ -74,27 +76,29 @@ func initCommandMap() {
if rest == "" { if rest == "" {
app.outgoing <- ManualPollEvent(app.last) app.outgoing <- ManualPollEvent(app.last)
} else { } else {
if app.conn == nil {
app.AppendSystemMessage("not connected to any server")
return
}
num, err := strconv.Atoi(rest) num, err := strconv.Atoi(rest)
if err != nil { if err != nil {
app.AppendSystemMessage("poll: invalid number %s", rest) app.AppendSystemMessage("invalid number \"%s\"", rest)
} else { } else {
if num == 0 { if num == 0 {
app.conn.ticker.Stop() app.conn.ticker.Stop()
app.conn.rate = 0 app.conn.rate = 0
app.AppendSystemMessage("poll: disabled automatic polling") app.AppendSystemMessage("disabled automatic polling")
} else { } else {
app.conn.rate = time.Second * time.Duration(num) app.conn.rate = time.Second * time.Duration(num)
app.conn.ticker.Stop() app.conn.ticker.Stop()
app.conn.ticker = time.NewTicker(app.conn.rate) app.conn.ticker = time.NewTicker(app.conn.rate)
app.AppendSystemMessage("poll: polling every %s", app.conn.rate.String()) app.AppendSystemMessage("polling rate set to %s", app.conn.rate.String())
} }
} }
} }
}, },
"me": func(app *App, rest string) { "me": func(app *App, rest string) {
msg := fmt.Sprintf("%s %s", app.nick, rest) app.outgoing <- MessageEvent(fmt.Sprintf("%s %s", app.nick, rest))
app.AppendMessage(msg)
app.outgoing <- MessageEvent(msg)
}, },
"clear": func(app *App, rest string) { "clear": func(app *App, rest string) {
clear(app.pager.Segments) clear(app.pager.Segments)
@ -106,7 +110,8 @@ func initCommandMap() {
}, },
} }
// aliases AliasMap = map[string]string {
CommandMap["q"] = CommandMap["quit"] "q": "quit",
CommandMap["."] = CommandMap["script"] ".": "script",
}
} }

View file

@ -87,13 +87,14 @@ func (app *App) submitTextInput() {
name, rest, _ := strings.Cut(app.input.String()[1:], " ") name, rest, _ := strings.Cut(app.input.String()[1:], " ")
if cmd, ok := CommandMap[name]; ok { if cmd, ok := CommandMap[name]; ok {
cmd(app, rest) cmd(app, rest)
} else if alias, ok := AliasMap[name]; ok {
cmd := CommandMap[alias]
cmd(app, rest)
} else { } else {
app.AppendSystemMessage("unknown command \"%s\"", name) app.AppendSystemMessage("unknown command \"%s\"", name)
} }
} else { } else {
message := fmt.Sprintf("%s: %s", app.nick, app.input.String()) app.outgoing <- MessageEvent(fmt.Sprintf("%s: %s", app.nick, app.input.String()))
app.AppendMessage(message)
app.outgoing <- MessageEvent(message)
app.outgoing <- StatEvent("") app.outgoing <- StatEvent("")
} }

View file

@ -118,6 +118,7 @@ func (ev MessageEvent) HandleIncoming(app *App) {
} }
func (ev MessageEvent) HandleOutgoing(app *App) error { func (ev MessageEvent) HandleOutgoing(app *App) error {
app.incoming <- ev
num, err := app.Send(string(ev)) num, err := app.Send(string(ev))
if err != nil { if err != nil {
return err return err
@ -178,9 +179,9 @@ func (ev ManualPollEvent) HandleOutgoing(app *App) error {
func (ev ManualPollEvent) HandleIncoming(app *App) { func (ev ManualPollEvent) HandleIncoming(app *App) {
if int(ev) == 0 { if int(ev) == 0 {
app.AppendSystemMessage("poll: no new messages") app.AppendSystemMessage("no new messages")
} else { } else {
app.AppendSystemMessage("poll: retrieving %d messages", ev) app.AppendSystemMessage("retrieving %d messages", ev)
} }
} }

17
main.go
View file

@ -31,7 +31,7 @@ type App struct {
outgoing chan OutgoingEvent outgoing chan OutgoingEvent
error chan error error chan error
scripts []string scripts map[string]string
vx *vaxis.Vaxis vx *vaxis.Vaxis
pager *pager.Model pager *pager.Model
@ -121,9 +121,14 @@ func NewApp() *App {
for { for {
select { select {
case ev := <-app.outgoing: case ev := <-app.outgoing:
if err := ev.HandleOutgoing(app); err != nil { _, dial := ev.(DialEvent)
app.error <- err if dial || app.conn != nil {
return if err := ev.HandleOutgoing(app); err != nil {
app.error <- err
return
}
} else {
app.incoming <- SystemMessageEvent("not connected to any server")
} }
case <-app.ctx.Done(): case <-app.ctx.Done():
return return
@ -155,7 +160,9 @@ func (app *App) Loop() error {
func (app *App) Finish() { func (app *App) Finish() {
app.stop() app.stop()
app.FinishUI() app.FinishUI()
HangupEvent{}.HandleOutgoing(app) if app.conn != nil {
HangupEvent{}.HandleOutgoing(app)
}
} }
func init() { func init() {

9
makefile Normal file
View file

@ -0,0 +1,9 @@
.PHONY: build install
PREFIX=$(HOME)/.local
build:
go build .
install: build
install -Dd $(PREFIX)/bin
install nanite $(PREFIX)/bin/nanite

View file

@ -2,6 +2,7 @@ package main
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@ -10,39 +11,42 @@ import (
) )
func (app *App) RefreshScripts() error { func (app *App) RefreshScripts() error {
app.scripts = make(map[string]string)
scriptDir := path.Join(app.cfgHome, "scripts") scriptDir := path.Join(app.cfgHome, "scripts")
if _, err := os.Stat(scriptDir); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(scriptDir); errors.Is(err, fs.ErrNotExist) {
return nil return nil
} }
return filepath.Walk(scriptDir, func(p string, i os.FileInfo, err error) error { return filepath.Walk(scriptDir, func(p string, i os.FileInfo, err error) error {
if err != nil { switch {
case err != nil:
return err return err
} case i.IsDir():
if i.IsDir() {
return nil return nil
default:
if data, err := os.ReadFile(p); err != nil {
return err
} else {
app.scripts[path.Base(p)] = string(data)
return nil
}
} }
app.scripts = append(app.scripts, path.Base(p))
return nil
}) })
} }
func (app *App) LoadScript(name string) error { func (app *App) LoadScript(name string) error {
scriptPath := path.Join(app.cfgHome, "scripts", name) if script, ok := app.scripts[name]; ok {
data, err := os.ReadFile(scriptPath) for line := range strings.Lines(script) {
if err != nil { cmdName, rest, _ := strings.Cut(strings.TrimSpace(line), " ")
return err if cmd, ok := CommandMap[cmdName]; ok {
} cmd(app, rest)
} else {
for line := range strings.Lines(string(data)) { return fmt.Errorf("unknown command \"%s\"", cmdName)
app.AppendSystemMessage("/%s", line) }
name, rest, _ := strings.Cut(line, " ")
if cmd, ok := CommandMap[name]; ok {
cmd(app, rest)
} else {
app.AppendSystemMessage("unknown command \"%s\"", name)
} }
} else {
return fmt.Errorf("not found")
} }
return nil return nil
} }