handle offline status correctly and other changes
This commit is contained in:
parent
dcc018efec
commit
6c66b32396
7 changed files with 93 additions and 81 deletions
49
README.md
49
README.md
|
|
@ -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 :)
|
|
||||||
|
|
||||||

|
> 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
|
|
||||||
|
|
|
||||||
45
command.go
45
command.go
|
|
@ -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",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
event.go
5
event.go
|
|
@ -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
17
main.go
|
|
@ -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
9
makefile
Normal 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
|
||||||
42
script.go
42
script.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue