Add notifications and info page
This commit is contained in:
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -8,7 +8,7 @@
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [],
|
||||
"args": ["-debug-notify"],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
@@ -18,7 +18,7 @@
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": ["-listen", "127.0.0.1:9000"],
|
||||
"args": ["-debug-notify", "-listen", "127.0.0.1:9000"],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
|
||||
17
go.mod
17
go.mod
@@ -2,6 +2,19 @@ module luxtools-client
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/godbus/dbus/v5 v5.2.2
|
||||
require (
|
||||
github.com/gen2brain/beeep v0.11.2
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.27.0 // indirect
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
|
||||
github.com/esiqveland/notify v0.13.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
|
||||
40
go.sum
40
go.sum
@@ -1,4 +1,40 @@
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
|
||||
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
|
||||
github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=
|
||||
github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
BIN
internal/notify/info.png
Executable file
BIN
internal/notify/info.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
16
internal/notify/notify.go
Normal file
16
internal/notify/notify.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/gen2brain/beeep"
|
||||
)
|
||||
|
||||
//go:embed info.png
|
||||
var icon []byte
|
||||
|
||||
// Show shows a desktop notification if possible.
|
||||
// It must never panic and callers should treat it as fire-and-forget.
|
||||
func Show(title, message string) {
|
||||
_ = beeep.Notify(title, message, icon)
|
||||
}
|
||||
127
main.go
127
main.go
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
@@ -12,13 +14,65 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"luxtools-client/internal/installer"
|
||||
"luxtools-client/internal/notify"
|
||||
"luxtools-client/internal/openfolder"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
type endpointDoc struct {
|
||||
Path string
|
||||
Methods string
|
||||
Description string
|
||||
}
|
||||
|
||||
func register(mux *http.ServeMux, docs *[]endpointDoc, path, methods, description string, handler http.HandlerFunc) {
|
||||
mux.HandleFunc(path, handler)
|
||||
*docs = append(*docs, endpointDoc{Path: path, Methods: methods, Description: description})
|
||||
}
|
||||
|
||||
var indexTemplate = template.Must(template.New("index").Parse(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>luxtools-client</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 1.25rem; }
|
||||
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
table { border-collapse: collapse; }
|
||||
th, td { border-bottom: 1px solid #ddd; padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>luxtools-client</h1>
|
||||
|
||||
<h2>Endpoints</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Path</th><th>Methods</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- range .Endpoints }}
|
||||
<tr>
|
||||
<td><a href="{{ .Path }}"><code>{{ .Path }}</code></a></td>
|
||||
<td><code>{{ .Methods }}</code></td>
|
||||
<td>{{ .Description }}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Info</h2>
|
||||
<pre>{{ .InfoJSON }}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
type allowList []string
|
||||
|
||||
func (a *allowList) String() string { return strings.Join(*a, ",") }
|
||||
@@ -40,6 +94,27 @@ type openResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func buildInfoPayload() map[string]any {
|
||||
var deps []map[string]string
|
||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, d := range bi.Deps {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
deps = append(deps, map[string]string{"path": d.Path, "version": d.Version})
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"version": version,
|
||||
"goVersion": runtime.Version(),
|
||||
"deps": deps,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
infoLog := log.New(os.Stdout, "", log.LstdFlags)
|
||||
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
||||
@@ -60,6 +135,7 @@ func main() {
|
||||
}
|
||||
|
||||
listen := flag.String("listen", "127.0.0.1:8765", "listen address (host:port), should be loopback")
|
||||
debugNotify := flag.Bool("debug-notify", false, "debug: show OS notifications on successful actions")
|
||||
var allowed allowList
|
||||
flag.Var(&allowed, "allow", "allowed path prefix (repeatable); if none, any path is allowed")
|
||||
flag.Parse()
|
||||
@@ -69,14 +145,56 @@ func main() {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
var endpointDocs []endpointDoc
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
register(mux, &endpointDocs, "/health", "GET", "Simple health check (JSON)", func(w http.ResponseWriter, r *http.Request) {
|
||||
withCORS(w, r)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "time": time.Now().Format(time.RFC3339)})
|
||||
})
|
||||
|
||||
mux.HandleFunc("/open", func(w http.ResponseWriter, r *http.Request) {
|
||||
register(mux, &endpointDocs, "/info", "GET, OPTIONS", "Detailed client info (JSON)", func(w http.ResponseWriter, r *http.Request) {
|
||||
withCORS(w, r)
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(buildInfoPayload())
|
||||
})
|
||||
|
||||
register(mux, &endpointDocs, "/", "GET, OPTIONS", "Human-friendly status page", func(w http.ResponseWriter, r *http.Request) {
|
||||
withCORS(w, r)
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
payload := buildInfoPayload()
|
||||
payloadJSON, _ := json.MarshalIndent(payload, "", " ")
|
||||
data := struct {
|
||||
Endpoints []endpointDoc
|
||||
InfoJSON template.HTML
|
||||
}{
|
||||
Endpoints: endpointDocs,
|
||||
InfoJSON: template.HTML(html.EscapeString(string(payloadJSON))),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := indexTemplate.Execute(w, data); err != nil {
|
||||
errLog.Printf("/ index-template error=%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
register(mux, &endpointDocs, "/open", "GET, POST, OPTIONS", "Open a folder in the OS file manager", func(w http.ResponseWriter, r *http.Request) {
|
||||
withCORS(w, r)
|
||||
start := time.Now()
|
||||
var rawPath string
|
||||
@@ -114,16 +232,21 @@ func main() {
|
||||
|
||||
if len(allowed) > 0 && !isAllowed(target, allowed) {
|
||||
errLog.Printf("/open forbidden method=%s path=%q normalized=%q dur=%s", r.Method, rawPath, target, time.Since(start))
|
||||
notify.Show("luxtools-client", fmt.Sprintf("Refused to open (not allowed): %s", target))
|
||||
writeJSON(w, http.StatusForbidden, openResponse{OK: false, Message: "path not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := openfolder.OpenLocation(target); err != nil {
|
||||
errLog.Printf("/open open-failed method=%s path=%q normalized=%q err=%v dur=%s", r.Method, rawPath, target, err, time.Since(start))
|
||||
notify.Show("luxtools-client", fmt.Sprintf("Failed to open: %s (%v)", target, err))
|
||||
writeJSON(w, http.StatusInternalServerError, openResponse{OK: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
infoLog.Printf("/open opened method=%s path=%q normalized=%q dur=%s", r.Method, rawPath, target, time.Since(start))
|
||||
if *debugNotify {
|
||||
notify.Show("luxtools-client", fmt.Sprintf("Opened: %s", target))
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
// For GET callers (image-ping), a 204 avoids console noise from non-image responses.
|
||||
|
||||
Reference in New Issue
Block a user