Compare commits
10 commits
f126ba7cce
...
4990c6d26a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4990c6d26a | |||
| 5701062c7f | |||
| b091174e5a | |||
| f7e904c6a4 | |||
| 6c66b32396 | |||
| dcc018efec | |||
| 70d95cdcce | |||
| 3412dd7900 | |||
| d3b3a62f28 | |||
| 77b1a93b30 |
11 changed files with 378 additions and 255 deletions
46
README.md
46
README.md
|
|
@ -1,42 +1,30 @@
|
|||
# nanite
|
||||
|
||||
`nanite` is a terminal [Nanochat] client.
|
||||
`nanite` is a terminal client for the [Nanochat] protocol.
|
||||
|
||||

|
||||
Requires Go 1.24.4 or higher. Build using `make build`.
|
||||
|
||||
## build
|
||||
> The upstream URL for this repository is <https://git.rhzm.org/lobo/nanite>.
|
||||
> The repositories hosted on GitHub and Codeberg are mirrors.
|
||||
|
||||
```
|
||||
$ 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
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 731 KiB |
217
command.go
217
command.go
|
|
@ -2,129 +2,116 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (app *App) Stat() (res string, err error) {
|
||||
if _, err := app.conn.Write([]byte("STAT\n")); err != nil {
|
||||
return "", err
|
||||
}
|
||||
type Command func(*App, string)
|
||||
|
||||
var str strings.Builder
|
||||
for range 3 {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return "", app.conn.Scanner.Err()
|
||||
}
|
||||
str.Write(app.conn.Scanner.Bytes())
|
||||
str.WriteRune(' ')
|
||||
}
|
||||
var CommandMap map[string]Command
|
||||
var AliasMap map[string]string
|
||||
|
||||
return str.String(), nil
|
||||
}
|
||||
|
||||
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.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
numRaw := app.conn.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.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
numRaw := app.conn.Scanner.Text()
|
||||
num, err = strconv.Atoi(numRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func (app *App) Skip(since int) (num int, err error) {
|
||||
if _, err := fmt.Fprintf(app.conn, "SKIP %d\n", since); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
num, err = strconv.Atoi(app.conn.Scanner.Text())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for range num {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- MessageEvent(app.conn.Scanner.Text())
|
||||
}
|
||||
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
last, err := strconv.Atoi(app.conn.Scanner.Text())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- SetLastEvent(last)
|
||||
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func (app *App) Last(n int) (num int, err error) {
|
||||
if n == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(app.conn, "LAST %d\n", n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var nsrv int
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
nsrvRaw := app.conn.Scanner.Text()
|
||||
nsrv, err = strconv.Atoi(nsrvRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if nsrv != 0 {
|
||||
for range nsrv {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
func initCommandMap() {
|
||||
CommandMap = map[string]Command{
|
||||
"help": func(app *App, rest string) {
|
||||
var s strings.Builder
|
||||
for _, name := range slices.Sorted(maps.Keys(CommandMap)) {
|
||||
s.WriteString(name)
|
||||
s.WriteRune(' ')
|
||||
}
|
||||
app.incoming <- MessageEvent(app.conn.Scanner.Text())
|
||||
}
|
||||
app.AppendSystemMessage("commands: %s", s.String())
|
||||
},
|
||||
"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 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
"send": func(app *App, rest string) {
|
||||
app.outgoing <- MessageEvent(rest)
|
||||
},
|
||||
"dial": func(app *App, rest string) {
|
||||
args := strings.Fields(rest)
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
app.AppendSystemMessage("usage: /dial host [port]")
|
||||
}
|
||||
host := args[0]
|
||||
port := "44322"
|
||||
if len(args) == 2 {
|
||||
port = args[1]
|
||||
}
|
||||
app.outgoing <- DialEvent{host, port}
|
||||
},
|
||||
"hangup": func(app *App, rest string) {
|
||||
app.outgoing <- HangupEvent{}
|
||||
},
|
||||
"nick": func(app *App, rest string) {
|
||||
if rest == "" {
|
||||
app.AppendSystemMessage("your nickname is %s", app.nick)
|
||||
} else {
|
||||
app.SetNick(rest)
|
||||
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("failed to persist nickname: %s", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
"poll": func(app *App, rest string) {
|
||||
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("invalid number \"%s\"", rest)
|
||||
} else {
|
||||
if num == 0 {
|
||||
app.conn.ticker.Stop()
|
||||
app.conn.rate = 0
|
||||
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("polling rate set to %s", app.conn.rate.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"me": func(app *App, rest string) {
|
||||
app.outgoing <- MessageEvent(fmt.Sprintf("%s %s", app.nick, rest))
|
||||
},
|
||||
"clear": func(app *App, rest string) {
|
||||
clear(app.pager.Segments)
|
||||
app.pager.Layout()
|
||||
app.AppendSystemMessage("cleared message history")
|
||||
},
|
||||
"quit": func(app *App, rest string) {
|
||||
app.stop()
|
||||
},
|
||||
}
|
||||
|
||||
var last int
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
AliasMap = map[string]string {
|
||||
"q": "quit",
|
||||
".": "script",
|
||||
}
|
||||
lastRaw := app.conn.Scanner.Text()
|
||||
last, err = strconv.Atoi(lastRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- SetLastEvent(last)
|
||||
|
||||
return nsrv, nil
|
||||
}
|
||||
|
|
|
|||
7
default.nix
Normal file
7
default.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.buildGoModule {
|
||||
pname = "nanite";
|
||||
version = "0.1.0+5701062c7f";
|
||||
src = ./.;
|
||||
vendorHash = "sha256-10zAs/SEJp3ImoaJ3/uJ23jjsnzOceLvizrvrWYI97k=";
|
||||
}
|
||||
14
display.go
14
display.go
|
|
@ -5,7 +5,7 @@ import (
|
|||
"math"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/lobo/nanite/widgets/pager"
|
||||
"git.rhzm.org/lobo/nanite/widgets/pager"
|
||||
"git.sr.ht/~rockorager/vaxis"
|
||||
"git.sr.ht/~rockorager/vaxis/widgets/textinput"
|
||||
)
|
||||
|
|
@ -87,14 +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 <- StatEvent("")
|
||||
app.outgoing <- MessageEvent(fmt.Sprintf("%s: %s", app.nick, app.input.String()))
|
||||
}
|
||||
|
||||
app.input.SetContent("")
|
||||
|
|
@ -108,6 +108,10 @@ func (app *App) HandleTerminalEvent(ev vaxis.Event) {
|
|||
app.pager.ScrollUp()
|
||||
case "Down":
|
||||
app.pager.ScrollDown()
|
||||
case "Page_Up":
|
||||
app.pager.ScrollUpN(app.w.log.Height * 2 / 3)
|
||||
case "Page_Down":
|
||||
app.pager.ScrollDownN(app.w.log.Height * 2 / 3)
|
||||
case "Enter":
|
||||
app.submitTextInput()
|
||||
case "Ctrl+p":
|
||||
|
|
|
|||
25
event.go
25
event.go
|
|
@ -11,6 +11,7 @@ import (
|
|||
type IncomingEvent interface {
|
||||
HandleIncoming(*App)
|
||||
}
|
||||
|
||||
type OutgoingEvent interface {
|
||||
HandleOutgoing(*App) error
|
||||
}
|
||||
|
|
@ -27,7 +28,9 @@ func (ev DialEvent) HandleOutgoing(app *App) error {
|
|||
var err error
|
||||
|
||||
if app.conn != nil {
|
||||
app.incoming <- SystemMessageEvent(fmt.Sprintf("already connected to %s:%s", app.conn.host, app.conn.port))
|
||||
app.incoming <- SystemMessageEvent(
|
||||
fmt.Sprintf("already connected to %s:%s", app.conn.host, app.conn.port),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort(ev.Host, ev.Port))
|
||||
|
|
@ -51,8 +54,8 @@ func (ev DialEvent) HandleOutgoing(app *App) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delta := time.Since(latStart).Round(time.Second)
|
||||
delta = min(max(time.Second, delta*3/2), 5*time.Second)
|
||||
delta := time.Since(latStart)
|
||||
delta = min(max(time.Second, delta*3/2), 5*time.Second).Round(time.Second)
|
||||
|
||||
app.conn.rate = delta
|
||||
app.conn.ticker = time.NewTicker(delta)
|
||||
|
|
@ -75,9 +78,13 @@ func (ev DialEvent) HandleOutgoing(app *App) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type HangupEvent struct{}
|
||||
type HangupEvent struct{ host, port string }
|
||||
|
||||
func (ev HangupEvent) HandleOutgoing(app *App) error {
|
||||
func (ev HangupEvent) HandleIncoming(app *App) {
|
||||
app.AppendSystemMessage("disconnected from %s:%s", ev.host, ev.port)
|
||||
}
|
||||
|
||||
func (_ HangupEvent) HandleOutgoing(app *App) error {
|
||||
if app.conn == nil {
|
||||
app.incoming <- SystemMessageEvent("not connected to any server")
|
||||
return nil
|
||||
|
|
@ -92,7 +99,7 @@ func (ev HangupEvent) HandleOutgoing(app *App) error {
|
|||
app.conn.ticker.Stop()
|
||||
app.conn.ticker = nil
|
||||
app.conn = nil
|
||||
app.incoming <- SystemMessageEvent(fmt.Sprintf("disconnected from %s:%s", host, port))
|
||||
app.incoming <- HangupEvent{host, port}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -111,11 +118,13 @@ 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
|
||||
}
|
||||
app.incoming <- SetLastEvent(num)
|
||||
app.outgoing <- StatEvent("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -171,9 +180,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,4 +1,4 @@
|
|||
module codeberg.org/lobo/nanite
|
||||
module git.rhzm.org/lobo/nanite
|
||||
|
||||
go 1.24.4
|
||||
|
||||
|
|
|
|||
134
main.go
134
main.go
|
|
@ -8,97 +8,15 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/lobo/nanite/widgets/pager"
|
||||
"git.rhzm.org/lobo/nanite/widgets/pager"
|
||||
|
||||
"git.sr.ht/~rockorager/vaxis"
|
||||
"git.sr.ht/~rockorager/vaxis/widgets/textinput"
|
||||
)
|
||||
|
||||
var CommandMap map[string]func(*App, string)
|
||||
|
||||
func init() {
|
||||
CommandMap = map[string]func(*App, string){
|
||||
"help": func(app *App, rest string) {
|
||||
var s strings.Builder
|
||||
for name, _ := range CommandMap {
|
||||
if name == "q" {
|
||||
continue
|
||||
}
|
||||
s.WriteString(name)
|
||||
s.WriteRune(' ')
|
||||
}
|
||||
app.AppendSystemMessage("commands: %s", s.String())
|
||||
},
|
||||
"dial": func(app *App, rest string) {
|
||||
args := strings.Fields(rest)
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
app.AppendSystemMessage("usage: /connect host [port]")
|
||||
}
|
||||
host := args[0]
|
||||
port := "44322"
|
||||
if len(args) == 2 {
|
||||
port = args[1]
|
||||
}
|
||||
app.outgoing <- DialEvent{host, port}
|
||||
},
|
||||
"hangup": func(app *App, rest string) {
|
||||
app.outgoing <- HangupEvent{}
|
||||
},
|
||||
"nick": func(app *App, rest string) {
|
||||
if rest == "" {
|
||||
app.AppendSystemMessage("nick: your nickname is %s", app.nick)
|
||||
} else {
|
||||
app.SetNick(rest)
|
||||
app.AppendSystemMessage("nick: your nickname is now %s", app.nick)
|
||||
if err := os.WriteFile(path.Join(app.cfgHome, "nick"), []byte(rest), 0o700); err != nil {
|
||||
app.AppendSystemMessage("nick: failed to persist nickname: %s", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
"poll": func(app *App, rest string) {
|
||||
if rest == "" {
|
||||
app.outgoing <- ManualPollEvent(app.last)
|
||||
} else {
|
||||
num, err := strconv.Atoi(rest)
|
||||
if err != nil {
|
||||
app.AppendSystemMessage("poll: invalid number %s", rest)
|
||||
} else {
|
||||
if num == 0 {
|
||||
app.conn.ticker.Stop()
|
||||
app.conn.rate = 0
|
||||
app.AppendSystemMessage("poll: 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"me": func(app *App, rest string) {
|
||||
msg := fmt.Sprintf("%s %s", app.nick, rest)
|
||||
app.AppendMessage(msg)
|
||||
app.outgoing <- MessageEvent(msg)
|
||||
},
|
||||
"clear": func(app *App, rest string) {
|
||||
app.pager.Segments = []vaxis.Segment{}
|
||||
app.pager.Layout()
|
||||
app.AppendSystemMessage("cleared message history")
|
||||
},
|
||||
"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
|
||||
|
|
@ -113,12 +31,15 @@ type App struct {
|
|||
outgoing chan OutgoingEvent
|
||||
error chan error
|
||||
|
||||
vx *vaxis.Vaxis
|
||||
w struct {
|
||||
scripts map[string]string
|
||||
|
||||
vx *vaxis.Vaxis
|
||||
pager *pager.Model
|
||||
input *textinput.Model
|
||||
w struct {
|
||||
log, title, input vaxis.Window
|
||||
}
|
||||
pager *pager.Model
|
||||
input *textinput.Model
|
||||
|
||||
cfgHome string
|
||||
}
|
||||
|
||||
|
|
@ -133,12 +54,15 @@ type Conn struct {
|
|||
}
|
||||
|
||||
func (app *App) AppendMessage(data string) {
|
||||
// TODO: make messages without a nick italic
|
||||
nick, _, found := strings.Cut(data, ": ")
|
||||
style := vaxis.Style{}
|
||||
if !found || strings.TrimSpace(nick) != nick {
|
||||
style.Attribute = vaxis.AttrItalic
|
||||
}
|
||||
app.pager.Segments = append(app.pager.Segments,
|
||||
vaxis.Segment{Text: data},
|
||||
vaxis.Segment{Text: data, Style: style},
|
||||
vaxis.Segment{Text: "\n"},
|
||||
)
|
||||
app.last += 1
|
||||
app.pager.Offset = math.MaxInt
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +78,7 @@ func (app *App) AppendSystemMessage(format string, args ...any) {
|
|||
|
||||
func (app *App) SetNick(nick string) {
|
||||
nick = strings.TrimSpace(nick)
|
||||
app.input.SetPrompt(fmt.Sprintf("%s: ", nick))
|
||||
app.input.SetPrompt(fmt.Sprintf("[%s] ", nick))
|
||||
app.input.Prompt.Attribute = vaxis.AttrBold
|
||||
app.nick = nick
|
||||
}
|
||||
|
|
@ -167,7 +91,7 @@ func (app *App) EnsureConfigDir() error {
|
|||
app.cfgHome = path.Join(userCfg, "nanite")
|
||||
stat, err := os.Stat(app.cfgHome)
|
||||
if err != nil {
|
||||
if err := os.Mkdir(app.cfgHome, 0o700); err != nil {
|
||||
if err := os.MkdirAll(app.cfgHome, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
|
@ -186,16 +110,25 @@ func NewApp() *App {
|
|||
app.error = make(chan error)
|
||||
|
||||
if err := app.EnsureConfigDir(); err != nil {
|
||||
app.error <- err
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := app.RefreshScripts(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
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
|
||||
|
|
@ -227,7 +160,13 @@ 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() {
|
||||
initCommandMap()
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -253,6 +192,7 @@ func main() {
|
|||
app.AppendSystemMessage("welcome to nanite! :3")
|
||||
|
||||
if err := app.Loop(); err != nil {
|
||||
app.FinishUI()
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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
|
||||
127
protocol.go
Normal file
127
protocol.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *App) Stat() (res string, err error) {
|
||||
if _, err := app.conn.Write([]byte("STAT\n")); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var str strings.Builder
|
||||
for range 3 {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return "", app.conn.Scanner.Err()
|
||||
}
|
||||
str.Write(app.conn.Scanner.Bytes())
|
||||
str.WriteRune(' ')
|
||||
}
|
||||
|
||||
return str.String(), nil
|
||||
}
|
||||
|
||||
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.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
numRaw := app.conn.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.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
numRaw := app.conn.Scanner.Text()
|
||||
num, err = strconv.Atoi(numRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func (app *App) Skip(since int) (num int, err error) {
|
||||
if _, err := fmt.Fprintf(app.conn, "SKIP %d\n", since); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
num, err = strconv.Atoi(app.conn.Scanner.Text())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for range num {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- MessageEvent(app.conn.Scanner.Text())
|
||||
}
|
||||
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
last, err := strconv.Atoi(app.conn.Scanner.Text())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- SetLastEvent(last)
|
||||
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func (app *App) Last(n int) (num int, err error) {
|
||||
if n == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(app.conn, "LAST %d\n", n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
numRaw := app.conn.Scanner.Text()
|
||||
num, err = strconv.Atoi(numRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for range num {
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
app.incoming <- MessageEvent(app.conn.Scanner.Text())
|
||||
}
|
||||
|
||||
var last int
|
||||
if !app.conn.Scanner.Scan() {
|
||||
return 0, app.conn.Scanner.Err()
|
||||
}
|
||||
lastRaw := app.conn.Scanner.Text()
|
||||
last, err = strconv.Atoi(lastRaw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
app.incoming <- SetLastEvent(last)
|
||||
|
||||
return num, nil
|
||||
}
|
||||
52
script.go
Normal file
52
script.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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 {
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) LoadScript(name string) error {
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue