diff options
| author | Martin Fischer <martin@push-f.com> | 2026-01-06 20:02:15 +0100 |
|---|---|---|
| committer | Martin Fischer <martin@push-f.com> | 2026-01-06 22:39:30 +0100 |
| commit | 222b4dc884892d5927dabbab2b2b6e64d3244002 (patch) | |
| tree | 18b6ce665dfcc99996411b553631484cd3a88cad | |
initial commit
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | LICENSE | 19 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | default.nix | 20 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | hashes.nix | 3 | ||||
| -rw-r--r-- | programs/open/main.go | 121 | ||||
| -rw-r--r-- | programs/serve/main.go | 30 | ||||
| -rwxr-xr-x | scripts/reset-bg | 4 | ||||
| -rwxr-xr-x | scripts/set-bg | 5 | ||||
| -rwxr-xr-x | scripts/sreplace | 17 |
12 files changed, 245 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4a847d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/result @@ -0,0 +1,19 @@ +Copyright (c) 2026 Martin Fischer <martin@push-f.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..68116c6 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# CMD 50 + +CMD 50 is a collection of perhaps someday 50 command-line tools. + +* open - open the given URI or file with your configured application +* reset-bg - reset background of [foot] terminal +* serve - serve the current directory over HTTP +* set-bg - set background of [foot] terminal +* sreplace - OpenSSH secure replace a directory + +TODO: write man pages + +## Other recommendations + +* wl-copy and wl-paste from [wl-clipboard] + +[foot]: https://codeberg.org/dnkl/foot +[wl-clipboard]: https://github.com/bugaevc/wl-clipboard diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..407458a --- /dev/null +++ b/default.nix @@ -0,0 +1,20 @@ +{ pkgs ? import <nixpkgs> {} }: + +let + go_module = pkgs.buildGoModule { + name = "cmd50"; + src = pkgs.lib.cleanSource ./.; + vendorHash = (import ./hashes.nix).go; + }; +in +pkgs.stdenv.mkDerivation { + name = "cmd50"; + + src = ./scripts; + + installPhase = '' + mkdir -p $out/bin + cp $src/* $out/bin/ + cp ${go_module}/bin/* $out/bin/ + ''; +} @@ -0,0 +1,5 @@ +module push-f.com/cmd50 + +go 1.25.5 + +require github.com/BurntSushi/toml v1.6.0 @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= diff --git a/hashes.nix b/hashes.nix new file mode 100644 index 0000000..3196db1 --- /dev/null +++ b/hashes.nix @@ -0,0 +1,3 @@ +{ + go = "sha256-pbA/AlBz3cQYRTMnQ/qBPcinYOKokrBLNhkbRTq54gE="; +} diff --git a/programs/open/main.go b/programs/open/main.go new file mode 100644 index 0000000..415f62a --- /dev/null +++ b/programs/open/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "log" + "mime" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" +) + +type Config struct { + MimeTypes map[string][]string `toml:"mime-types"` + UriSchemes map[string][]string `toml:"uri-schemes"` +} + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: %s <file-or-uri>", os.Args[0]) + os.Exit(1) + } + + arg := os.Args[1] + + configPath, err := xdgConfigPath("open") + if err != nil { + fatal("failed to find config path: %v", err) + } + + var cfg Config + meta, err := toml.DecodeFile(configPath, &cfg) + if err != nil { + fatal("failed to decode config: %v", err) + } + if len(meta.Undecoded()) != 0 { + fatal("unknown config keys: %v", meta.Undecoded()) + } + + for scheme, cmd := range cfg.UriSchemes { + if strings.HasPrefix(arg, scheme+":") { + cmd = append(cmd, arg) + err := run(cmd) + if err != nil { + log.Fatal(err) + } + return + } + } + + stat, err := os.Stat(arg) + if err != nil { + if os.IsNotExist(err) && strings.Contains(arg, ":") { + fatal("file not found or URI scheme not registered") + } + fatal("stat failed: %v", err) + } + + var mimeType string + + if stat.IsDir() { + mimeType = "inode/directory" + } else { + ext := path.Ext(arg) + mimeType = mime.TypeByExtension(ext) + if mimeType == "" { + fatal("no associated mime type for file extension %s", ext) + } + // strip e.g. `; charset=utf-8` + mimeType, _, _ = strings.Cut(mimeType, ";") + } + + for mimeTypePat, cmd := range cfg.MimeTypes { + matched, err := path.Match(mimeTypePat, mimeType) + if err != nil { + log.Fatal(err) + } + if matched { + cmd = append(cmd, arg) + err := run(cmd) + if err != nil { + log.Fatal(err) + } + return + } + } + + fatal("no opener configured for mime %s", mimeType) +} + +func fatal(msg string, v ...any) { + fmt.Fprintf(os.Stderr, "fatal: "+msg+"\n", v...) + os.Exit(1) +} + +func run(args []string) error { + cmd := exec.Command(args[0], args[1:]...) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func xdgConfigPath(appName string) (string, error) { + xdgHome := os.Getenv("XDG_CONFIG_HOME") + + if xdgHome == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + xdgHome = filepath.Join(home, ".config") + } + + return filepath.Join(xdgHome, appName, "config.toml"), nil +} diff --git a/programs/serve/main.go b/programs/serve/main.go new file mode 100644 index 0000000..c0de682 --- /dev/null +++ b/programs/serve/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + "fmt" + "log" + "log/slog" + "net/http" + "os" +) + +var port = flag.Int("port", 8000, "port") + +func main() { + flag.Parse() + + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + slog.SetDefault(logger) + + slog.Info(fmt.Sprintf("listening on http://localhost:%d/", *port)) + err := http.ListenAndServe(fmt.Sprintf(":%d", *port), logRequest(http.FileServer(http.Dir(".")))) + log.Fatal(err) +} + +func logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("request", "addr", r.RemoteAddr, "method", r.Method, "url", r.URL) + handler.ServeHTTP(w, r) + }) +} diff --git a/scripts/reset-bg b/scripts/reset-bg new file mode 100755 index 0000000..049954d --- /dev/null +++ b/scripts/reset-bg @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Resets the background color of the current foot terminal to the default. + +echo -en "\033]111\007" diff --git a/scripts/set-bg b/scripts/set-bg new file mode 100755 index 0000000..2ed0a78 --- /dev/null +++ b/scripts/set-bg @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Sets the background color of the current foot terminal to the given hex color. + +rgb_with_slashes=$(echo "$1" | sed -r 's,(..)(..),\1/\2/,') +echo -en "\033]11;rgb:$rgb_with_slashes\007" diff --git a/scripts/sreplace b/scripts/sreplace new file mode 100755 index 0000000..f90a4ae --- /dev/null +++ b/scripts/sreplace @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Replaces the contents of a remote directory with the contents of a local directory. + +if [ "$#" -ne 3 ]; then + echo "usage: $0 <local_dir> <ssh_dest> <remote_dir>" + exit 1 +fi +printf -v remote_dir %q $3 + +cd $1 +tar cf - . | ssh $2 "set -xe +TEMP_DIR=\$(mktemp -d) +cd \$TEMP_DIR +tar xvf - +rm -rf $remote_dir +mv \$TEMP_DIR $remote_dir +" |
