From 6c66b3239638d7e89cbf4276013e4f717a9f8c17 Mon Sep 17 00:00:00 2001 From: Lobo Torres Date: Wed, 29 Oct 2025 12:50:59 -0300 Subject: [PATCH] handle offline status correctly and other changes --- README.md | 49 +++++++++++++++++-------------------------------- command.go | 45 +++++++++++++++++++++++++-------------------- display.go | 7 ++++--- event.go | 5 +++-- main.go | 17 ++++++++++++----- makefile | 9 +++++++++ script.go | 42 +++++++++++++++++++++++------------------- 7 files changed, 93 insertions(+), 81 deletions(-) create mode 100644 makefile diff --git a/README.md b/README.md index c95d084..9b4cb3c 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,30 @@ # 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 -> is now https://git.rhzm.org/lobo/nanite :) +Requires Go 1.24.4 or higher. Build using `make build`. -![](./assets/screenshot.png) +> The upstream URL for this repository is . +> The repositories hosted on GitHub and Codeberg are mirrors. -## build - -``` -$ go build . -``` - -## usage - -``` -$ ./nanite -usage: ./nanite host [port] -$ ./nanite very.real-server.com -``` - -keybindings: +# keybindings - `Ctrl+C`: quit - `Ctrl+L`: refresh screen - `Ctrl+P`: poll +- Emacs-like bindings for text editing -commands: +# commands -- `/dial hostname`: connect to server -- `/hangup`: disconnect -- `/q`, `/quit`: quit -- `/nick [nickname]`: change nick, if no arguments, show current nick -- `/me`: IRC `/me` alike -- `/poll [seconds]`: change polling interval, if no arguments, poll manually - -## won't support (yet) - -- sixel (tried, it seems to be complicated to get it to work with Vaxis' pager - widget) +- `/clear`: clear message log +- `/dial host [port]`: connect to server +- `/hangup`: disconnect from server +- `/help`: see command list +- `/me ...`: send IRC-style `/me` message +- `/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 +- `/quit`: self-explanatory (aliased to `/q`) +- `/script [name]`: run script or see script list if no arguments given (aliased to `/.`) +- `/send ...`: send raw message [Nanochat]: https://git.phial.org/d6/nanochat -[Vaxis]: https://git.sr.ht/~rockorager/vaxis diff --git a/command.go b/command.go index 727285c..99f3996 100644 --- a/command.go +++ b/command.go @@ -2,8 +2,10 @@ package main import ( "fmt" + "maps" "os" "path" + "slices" "strconv" "strings" "time" @@ -12,15 +14,13 @@ import ( type Command func(*App, string) var CommandMap map[string]Command +var AliasMap map[string]string func initCommandMap() { CommandMap = map[string]Command{ "help": func(app *App, rest string) { var s strings.Builder - for name, _ := range CommandMap { - if name == "q" { - continue - } + for _, name := range slices.Sorted(maps.Keys(CommandMap)) { s.WriteString(name) s.WriteRune(' ') } @@ -28,26 +28,28 @@ func initCommandMap() { }, "script": func(app *App, rest string) { if rest == "" { + if err := app.RefreshScripts(); err != nil { + app.AppendSystemMessage("failed to refresh scripts") + } var s strings.Builder - for _, script := range app.scripts { + for _, script := range slices.Sorted(maps.Keys(app.scripts)) { s.WriteString(script) s.WriteRune(' ') } app.AppendSystemMessage("scripts: %s", s.String()) } else { 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) { - app.AppendMessage(rest) app.outgoing <- MessageEvent(rest) }, "dial": func(app *App, rest string) { args := strings.Fields(rest) if len(args) < 1 || len(args) > 2 { - app.AppendSystemMessage("usage: /connect host [port]") + app.AppendSystemMessage("usage: /dial host [port]") } host := args[0] port := "44322" @@ -61,12 +63,12 @@ func initCommandMap() { }, "nick": func(app *App, rest string) { if rest == "" { - app.AppendSystemMessage("nick: your nickname is %s", app.nick) + app.AppendSystemMessage("your nickname is %s", app.nick) } else { 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 { - 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 == "" { app.outgoing <- ManualPollEvent(app.last) } else { + if app.conn == nil { + app.AppendSystemMessage("not connected to any server") + return + } num, err := strconv.Atoi(rest) if err != nil { - app.AppendSystemMessage("poll: invalid number %s", rest) + app.AppendSystemMessage("invalid number \"%s\"", rest) } else { if num == 0 { app.conn.ticker.Stop() app.conn.rate = 0 - app.AppendSystemMessage("poll: disabled automatic polling") + app.AppendSystemMessage("disabled automatic polling") } else { app.conn.rate = time.Second * time.Duration(num) app.conn.ticker.Stop() 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) { - msg := fmt.Sprintf("%s %s", app.nick, rest) - app.AppendMessage(msg) - app.outgoing <- MessageEvent(msg) + app.outgoing <- MessageEvent(fmt.Sprintf("%s %s", app.nick, rest)) }, "clear": func(app *App, rest string) { clear(app.pager.Segments) @@ -106,7 +110,8 @@ func initCommandMap() { }, } - // aliases - CommandMap["q"] = CommandMap["quit"] - CommandMap["."] = CommandMap["script"] + AliasMap = map[string]string { + "q": "quit", + ".": "script", + } } diff --git a/display.go b/display.go index a685d97..75f72a4 100644 --- a/display.go +++ b/display.go @@ -87,13 +87,14 @@ func (app *App) submitTextInput() { name, rest, _ := strings.Cut(app.input.String()[1:], " ") if cmd, ok := CommandMap[name]; ok { cmd(app, rest) + } else if alias, ok := AliasMap[name]; ok { + cmd := CommandMap[alias] + cmd(app, rest) } else { app.AppendSystemMessage("unknown command \"%s\"", name) } } else { - message := fmt.Sprintf("%s: %s", app.nick, app.input.String()) - app.AppendMessage(message) - app.outgoing <- MessageEvent(message) + app.outgoing <- MessageEvent(fmt.Sprintf("%s: %s", app.nick, app.input.String())) app.outgoing <- StatEvent("") } diff --git a/event.go b/event.go index 358d5fa..41766c1 100644 --- a/event.go +++ b/event.go @@ -118,6 +118,7 @@ func (ev MessageEvent) HandleIncoming(app *App) { } func (ev MessageEvent) HandleOutgoing(app *App) error { + app.incoming <- ev num, err := app.Send(string(ev)) if err != nil { return err @@ -178,9 +179,9 @@ func (ev ManualPollEvent) HandleOutgoing(app *App) error { func (ev ManualPollEvent) HandleIncoming(app *App) { if int(ev) == 0 { - app.AppendSystemMessage("poll: no new messages") + app.AppendSystemMessage("no new messages") } else { - app.AppendSystemMessage("poll: retrieving %d messages", ev) + app.AppendSystemMessage("retrieving %d messages", ev) } } diff --git a/main.go b/main.go index 5197cd0..eb66498 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,7 @@ type App struct { outgoing chan OutgoingEvent error chan error - scripts []string + scripts map[string]string vx *vaxis.Vaxis pager *pager.Model @@ -121,9 +121,14 @@ func NewApp() *App { for { select { case ev := <-app.outgoing: - if err := ev.HandleOutgoing(app); err != nil { - app.error <- err - return + _, dial := ev.(DialEvent) + if dial || app.conn != nil { + if err := ev.HandleOutgoing(app); err != nil { + app.error <- err + return + } + } else { + app.incoming <- SystemMessageEvent("not connected to any server") } case <-app.ctx.Done(): return @@ -155,7 +160,9 @@ func (app *App) Loop() error { func (app *App) Finish() { app.stop() app.FinishUI() - HangupEvent{}.HandleOutgoing(app) + if app.conn != nil { + HangupEvent{}.HandleOutgoing(app) + } } func init() { diff --git a/makefile b/makefile new file mode 100644 index 0000000..c2c6fd9 --- /dev/null +++ b/makefile @@ -0,0 +1,9 @@ +.PHONY: build install +PREFIX=$(HOME)/.local + +build: + go build . + +install: build + install -Dd $(PREFIX)/bin + install nanite $(PREFIX)/bin/nanite diff --git a/script.go b/script.go index 039838c..10c5d3b 100644 --- a/script.go +++ b/script.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "io/fs" "os" "path" @@ -10,39 +11,42 @@ import ( ) func (app *App) RefreshScripts() error { + app.scripts = make(map[string]string) + scriptDir := path.Join(app.cfgHome, "scripts") if _, err := os.Stat(scriptDir); errors.Is(err, fs.ErrNotExist) { return nil } return filepath.Walk(scriptDir, func(p string, i os.FileInfo, err error) error { - if err != nil { + switch { + case err != nil: return err - } - if i.IsDir() { + case i.IsDir(): 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 { - scriptPath := path.Join(app.cfgHome, "scripts", name) - data, err := os.ReadFile(scriptPath) - if err != nil { - return err - } - - for line := range strings.Lines(string(data)) { - 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) + if script, ok := app.scripts[name]; ok { + for line := range strings.Lines(script) { + cmdName, rest, _ := strings.Cut(strings.TrimSpace(line), " ") + if cmd, ok := CommandMap[cmdName]; ok { + cmd(app, rest) + } else { + return fmt.Errorf("unknown command \"%s\"", cmdName) + } } + } else { + return fmt.Errorf("not found") } - return nil }