File: ./check/check.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package check
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "net"
  33     "os"
  34     "strconv"
  35     "time"
  36 )
  37 
  38 const info = `
  39 check [options...] [timeout...] [domains...]
  40 
  41 Check if the domains given can be connected to via TCP. An optional leading
  42 number (of seconds) lets you override the default timeout of 5 seconds.
  43 `
  44 
  45 func Main() {
  46     args := os.Args[1:]
  47     timeout := 5 * time.Second
  48 
  49     if len(args) > 0 {
  50         switch args[0] {
  51         case `-h`, `--h`, `-help`, `--help`:
  52             os.Stdout.WriteString(info[1:])
  53             return
  54         }
  55     }
  56 
  57     if len(args) > 0 {
  58         f, err := strconv.ParseFloat(args[0], 64)
  59         if err == nil && f > 0 && !math.IsNaN(f) && !math.IsInf(f, 0) {
  60             timeout = time.Duration(f * float64(time.Second))
  61             args = args[1:]
  62         }
  63     }
  64 
  65     if len(args) > 0 && args[0] == `--` {
  66         args = args[1:]
  67     }
  68 
  69     if len(args) == 0 {
  70         os.Stderr.WriteString(info[1:])
  71         os.Exit(1)
  72         return
  73     }
  74 
  75     ok, err := run(os.Stdout, args, timeout)
  76 
  77     if err != nil && err != io.EOF {
  78         os.Stderr.WriteString(err.Error())
  79         os.Stderr.WriteString("\n")
  80         os.Exit(1)
  81         return
  82     }
  83 
  84     if !ok {
  85         os.Exit(1)
  86         return
  87     }
  88 }
  89 
  90 func run(w io.Writer, domains []string, timeout time.Duration) (ok bool, err error) {
  91     bw := bufio.NewWriterSize(w, 32*1024)
  92     defer bw.Flush()
  93 
  94     allOK := true
  95 
  96     for _, s := range domains {
  97         ok, err := check(bw, s, timeout)
  98         if err != nil {
  99             bw.Flush()
 100         }
 101         if !ok {
 102             allOK = false
 103         }
 104     }
 105 
 106     return allOK, nil
 107 }
 108 
 109 func check(w *bufio.Writer, domain string, timeout time.Duration) (ok bool, err error) {
 110     conn, err := net.DialTimeout(`tcp`, domain, timeout)
 111     if conn != nil {
 112         defer conn.Close()
 113     }
 114 
 115     if err == nil {
 116         w.WriteString(domain)
 117         w.WriteString("\tOK\n")
 118         if w.Flush() != nil {
 119             return true, io.EOF
 120         }
 121         return true, nil
 122     }
 123 
 124     fmt.Fprintf(w, "%s\tFAILED\t%s\n", domain, err.Error())
 125     if w.Flush() != nil {
 126         return false, io.EOF
 127     }
 128     return false, nil
 129 }
     File: ./dict/commands.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dict
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "net"
  32     "regexp"
  33     "strings"
  34     "time"
  35 )
  36 
  37 // https://www.rfc-editor.org/rfc/rfc2229
  38 
  39 var (
  40     okLines  = regexp.MustCompile(`^[12][0-9]{2} `)
  41     errLines = regexp.MustCompile(`^5[0-9]{2} `)
  42 )
  43 
  44 const maxBufSize = 8 * 1024 * 1024 * 1024
  45 
  46 func lookup(w io.Writer, dict string, words []string) (ok bool, err error) {
  47     ok = true
  48     dict = strings.ToLower(dict)
  49     if strings.Contains(dict, `-`) && !strings.HasPrefix(dict, `fd-`) {
  50         dict = `fd-` + dict
  51     }
  52 
  53     for _, s := range words {
  54         cmd := fmt.Sprintf(`define %s %s`, dict, s)
  55         // color the command in blue
  56         // fmt.Fprintf(w, "\x1b[38;2;0;95;215m%s\x1b[0m\n", cmd)
  57 
  58         // highlight the word being looked-up
  59         fmt.Fprintf(w, "\x1b[7m%-80s\x1b[0m\n", s)
  60 
  61         good, err := runCommand(w, cmd)
  62         if !good {
  63             ok = false
  64         }
  65         if err != nil {
  66             return ok, err
  67         }
  68     }
  69     return ok, nil
  70 }
  71 
  72 func runCommand(w io.Writer, cmd string) (ok bool, err error) {
  73     conn, err := net.Dial(`tcp`, `dict.org:2628`)
  74     if err != nil {
  75         return false, err
  76     }
  77     defer conn.Close()
  78 
  79     conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
  80     fmt.Fprintf(conn, "%s\r\n", cmd)
  81 
  82     sc := bufio.NewScanner(conn)
  83     sc.Buffer(nil, maxBufSize)
  84 
  85     ok = true
  86 
  87     for sc.Scan() {
  88         line := sc.Text()
  89         if okLines.MatchString(line) {
  90             // ignore success lines
  91             continue
  92         }
  93         if errLines.MatchString(line) {
  94             ok = false
  95             // color errors in red
  96             fmt.Fprintf(w, "\x1b[38;2;204;0;0m%s\x1b[0m\n", line)
  97             continue
  98         }
  99 
 100         if line == `.` {
 101             // ignore last line
 102             break
 103         }
 104 
 105         fmt.Fprintln(w, line)
 106     }
 107 
 108     return ok, sc.Err()
 109 }
     File: ./dict/info.txt
   1 dict [options...] [dictionary...] [word...]
   2 
   3 Look up words from the free online dictionary (dict://dict.org). The help
   4 option also shows you which dictionaries are available.
   5 
   6 If no specific dictionary is given (using 1 or 2 leading dashes), `WordNet`
   7 (wn) is used as the default.
     File: ./dict/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dict
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "os"
  32     "strings"
  33 
  34     _ "embed"
  35 )
  36 
  37 //go:embed info.txt
  38 var info string
  39 
  40 func Main() {
  41     dict := `wn`
  42     args := os.Args[1:]
  43 
  44     for len(args) > 0 {
  45         if args[0] == `--` {
  46             args = args[1:]
  47             break
  48         }
  49 
  50         switch args[0] {
  51         case `-h`, `--h`, `-help`, `--help`:
  52             showHelp()
  53             return
  54         }
  55 
  56         if strings.HasPrefix(args[0], `-`) {
  57             s := args[0]
  58             for len(s) > 0 && s[0] == '-' {
  59                 s = s[1:]
  60             }
  61             dict = s
  62             continue
  63         }
  64 
  65         break
  66     }
  67 
  68     if len(args) == 0 {
  69         showHelp()
  70         os.Exit(1)
  71         return
  72     }
  73 
  74     ok, err := run(os.Stdout, dict, args)
  75     if err != nil {
  76         fmt.Fprintf(os.Stderr, "%s\n", err.Error())
  77         os.Exit(1)
  78         return
  79     }
  80     if !ok {
  81         os.Exit(1)
  82         return
  83     }
  84 }
  85 
  86 func run(w io.Writer, dict string, args []string) (ok bool, err error) {
  87     bw := bufio.NewWriterSize(w, 16*1024)
  88     defer bw.Flush()
  89     ok, err = lookup(bw, dict, args)
  90     return ok, err
  91 }
  92 
  93 func showHelp() {
  94     fmt.Fprint(os.Stderr, info)
  95     fmt.Fprintln(os.Stderr)
  96     fmt.Fprintln(os.Stderr, `Dictionaries available from dict.org`)
  97     fmt.Fprintln(os.Stderr)
  98 
  99     ok, err := runCommand(os.Stderr, `show db`)
 100     if err != nil {
 101         fmt.Fprintf(os.Stderr, "%s\n", err.Error())
 102         os.Exit(1)
 103         return
 104     }
 105     if !ok {
 106         os.Exit(1)
 107         return
 108     }
 109 }
     File: ./fetch/fetch.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fetch
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "net/http"
  31     "os"
  32     "strings"
  33 )
  34 
  35 const info = `
  36 fetch [options...] [URIs...]
  37 
  38 Load/get data from the HTTP/HTTPS URIs given, auto-completing the protocol to
  39 HTTPS, when missing.
  40 `
  41 
  42 func Main() {
  43     args := os.Args[1:]
  44 
  45     if len(args) > 0 {
  46         switch args[0] {
  47         case `-h`, `--h`, `-help`, `--help`:
  48             os.Stdout.WriteString(info[1:])
  49             return
  50         }
  51     }
  52 
  53     if len(args) > 0 && args[0] == `--` {
  54         args = args[1:]
  55     }
  56 
  57     if len(args) == 0 {
  58         os.Stderr.WriteString(info[1:])
  59         os.Exit(1)
  60         return
  61     }
  62 
  63     if err := run(os.Stdout, args); err != nil && err != io.EOF {
  64         os.Stderr.WriteString(err.Error())
  65         os.Stderr.WriteString("\n")
  66         os.Exit(1)
  67         return
  68     }
  69 }
  70 
  71 func run(w io.Writer, args []string) error {
  72     bw := bufio.NewWriterSize(w, 32*1024)
  73     defer bw.Flush()
  74 
  75     for _, s := range args {
  76         if err := fetch(bw, s); err != nil {
  77             return err
  78         }
  79     }
  80     return nil
  81 }
  82 
  83 func fetch(w *bufio.Writer, uri string) error {
  84     if !hasAnyPrefix(uri, `https://`, `http://`) {
  85         uri = `https://` + uri
  86     }
  87 
  88     resp, err := http.Get(uri)
  89     if err != nil {
  90         return err
  91     }
  92     defer resp.Body.Close()
  93 
  94     _, err = io.Copy(w, resp.Body)
  95     return err
  96 }
  97 
  98 func hasAnyPrefix(s string, prefixes ...string) bool {
  99     for _, p := range prefixes {
 100         if strings.HasPrefix(s, p) {
 101             return true
 102         }
 103     }
 104     return false
 105 }
     File: ./get/get.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package get
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "net/http"
  31     "os"
  32 )
  33 
  34 const info = `
  35 get [options...] [URIs...]
  36 
  37 Load/get data from the HTTP/HTTPS URIs given.
  38 `
  39 
  40 func Main() {
  41     args := os.Args[1:]
  42 
  43     if len(args) > 0 {
  44         switch args[0] {
  45         case `-h`, `--h`, `-help`, `--help`:
  46             os.Stdout.WriteString(info[1:])
  47             return
  48         }
  49     }
  50 
  51     if len(args) > 0 && args[0] == `--` {
  52         args = args[1:]
  53     }
  54 
  55     if len(args) == 0 {
  56         os.Stderr.WriteString(info[1:])
  57         os.Exit(1)
  58         return
  59     }
  60 
  61     if err := run(os.Stdout, args); err != nil && err != io.EOF {
  62         os.Stderr.WriteString(err.Error())
  63         os.Stderr.WriteString("\n")
  64         os.Exit(1)
  65         return
  66     }
  67 }
  68 
  69 func run(w io.Writer, args []string) error {
  70     bw := bufio.NewWriterSize(w, 32*1024)
  71     defer bw.Flush()
  72 
  73     for _, s := range args {
  74         if err := get(bw, s); err != nil {
  75             return err
  76         }
  77     }
  78     return nil
  79 }
  80 
  81 func get(w *bufio.Writer, uri string) error {
  82     resp, err := http.Get(uri)
  83     if err != nil {
  84         return err
  85     }
  86     defer resp.Body.Close()
  87 
  88     _, err = io.Copy(w, resp.Body)
  89     return err
  90 }
     File: ./info.txt
   1 netbox [options...] [tool] [arguments...]
   2 
   3 This is a collection of many specialized networking-related app-like tools,
   4 similar to "busybox".
   5 
   6 You can either run it with the tool name as its first argument, or run a link
   7 to it whose name is one of those same tools, avoiding the tool-name argument
   8 in that case.
   9 
  10 Tool "help" shows you all tools available, as well as all their aliases, and
  11 tool "tools" merely lists all main tool-names.
     File: ./main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29     go build -ldflags "-s -w" -trimpath
  30 
  31 If you have `tinygo`, you can do even better by instead using:
  32 
  33     tinygo build -no-debug -opt=2 netbox
  34 */
  35 
  36 package main
  37 
  38 import (
  39     "fmt"
  40     "io"
  41     "os"
  42     "path"
  43     "sort"
  44     "strings"
  45     "unicode/utf8"
  46 
  47     "./check"
  48     "./dict"
  49     "./fetch"
  50     "./get"
  51     "./podfeed"
  52     "./wports"
  53 
  54     _ "embed"
  55 )
  56 
  57 //go:embed info.txt
  58 var info string
  59 
  60 // mains has some entries starting as nil to avoid circular-dependency errors
  61 var mains = map[string]func(){
  62     `check`:   check.Main,
  63     `dict`:    dict.Main,
  64     `fetch`:   fetch.Main,
  65     `get`:     get.Main,
  66     `podfeed`: podfeed.Main,
  67     `wports`:  wports.Main,
  68 }
  69 
  70 var aliases = map[string]string{}
  71 
  72 var blurbs = map[string]string{
  73     `check`:   `check if the domains given can be connected to via TCP`,
  74     `dict`:    `dictionary-define the words given, via dict://dict.org`,
  75     `get`:     `try a get request over HTTP/HTTPS; no protocol autocomplete`,
  76     `fetch`:   `fetch over HTTP/HTTPS, with protocol auto-completed to HTTPS`,
  77     `help`:    `show the help message for "netbox"`,
  78     `podfeed`: `link to podcast episodes in a self-contained web-page`,
  79     `tools`:   `list all tools available`,
  80     `wports`:  `find which localhost TCP ports are currently in use`,
  81 }
  82 
  83 func main() {
  84     // try to use the app's `name`, in case it's being called from a file-link
  85     // named after one of the tools
  86     if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
  87         tool()
  88         return
  89     }
  90 
  91     // try normal tool-lookup using the first command-line argument
  92     if len(os.Args) >= 2 {
  93         name := os.Args[1]
  94 
  95         if tool, ok := lookupTool(name); ok {
  96             os.Args = os.Args[1:]
  97             tool()
  98             return
  99         }
 100 
 101         switch name {
 102         case `-h`, `--h`, `-help`, `--help`, `help`:
 103             showHelp(os.Stdout)
 104             return
 105 
 106         case `-l`, `--l`, `-list`, `--list`:
 107             tools()
 108             return
 109 
 110         case `-links`, `--links`:
 111             showLinks(os.Stdout)
 112             return
 113 
 114         case `-t`, `--t`, `-tools`, `--tools`, `tools`:
 115             tools()
 116             return
 117         }
 118 
 119         const fs = "netbox: tool/alias named %q not found\n"
 120         fmt.Fprintf(os.Stderr, fs, name)
 121         os.Exit(1)
 122         return
 123     }
 124 
 125     showHelp(os.Stderr)
 126     fmt.Fprintln(os.Stderr, ``)
 127     fmt.Fprintln(os.Stderr, `netbox: no tool name given`)
 128     os.Exit(1)
 129     return
 130 }
 131 
 132 // dealias tries to lookup a string to the aliases given, returning the name
 133 // given if the lookup fails
 134 func dealias(aliases map[string]string, name string) string {
 135     if s, ok := aliases[name]; ok {
 136         return s
 137     }
 138     return name
 139 }
 140 
 141 func help() {
 142     showHelp(os.Stdout)
 143 }
 144 
 145 func lookupTool(name string) (tool func(), ok bool) {
 146     if name == `help` {
 147         return help, true
 148     }
 149     if name == `tools` {
 150         return tools, true
 151     }
 152 
 153     name = strings.ReplaceAll(name, `-`, ``)
 154 
 155     if tool, ok := mains[dealias(aliases, name)]; ok {
 156         return tool, ok
 157     }
 158     return tool, ok
 159 }
 160 
 161 // showHelp has a parameter to write either to stdout or stderr
 162 func showHelp(w io.Writer) {
 163     fmt.Fprintln(w, info)
 164     fmt.Fprintln(w, ``)
 165     fmt.Fprintln(w, `Tools Available`)
 166 
 167     maxlen := 0
 168     names := make([]string, 0, max(len(mains), len(aliases)))
 169     for k := range mains {
 170         names = append(names, k)
 171         maxlen = max(maxlen, utf8.RuneCountInString(k))
 172     }
 173 
 174     sort.Strings(names)
 175 
 176     for _, s := range names {
 177         fmt.Fprintf(w, "  - %-*s  %s\n", maxlen, s, blurbs[s])
 178     }
 179 
 180     fmt.Fprintln(w, ``)
 181     fmt.Fprintln(w, `Aliases Available`)
 182 
 183     maxlen = 0
 184     names = names[:0]
 185     for k := range aliases {
 186         names = append(names, k)
 187         maxlen = max(maxlen, utf8.RuneCountInString(k))
 188     }
 189 
 190     sort.Strings(names)
 191 
 192     for _, k := range names {
 193         fmt.Fprintf(w, "  - %-*s -> %s\n", maxlen, k, aliases[k])
 194     }
 195 }
 196 
 197 // showLinks has a parameter to write either to stdout or stderr
 198 func showLinks(w io.Writer) {
 199     names := make([]string, 0, len(mains))
 200     for k := range mains {
 201         names = append(names, k)
 202     }
 203 
 204     sort.Strings(names)
 205 
 206     for _, s := range names {
 207         fmt.Fprintf(w, "ln -s \"$(which netbox)\" ./%s\n", s)
 208     }
 209 }
 210 
 211 func tools() {
 212     names := make([]string, 0, len(mains))
 213     for k := range mains {
 214         names = append(names, k)
 215     }
 216 
 217     sort.Strings(names)
 218 
 219     for _, s := range names {
 220         fmt.Fprintln(os.Stdout, s)
 221     }
 222 }
     File: ./main_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package main
  26 
  27 import "testing"
  28 
  29 func TestAliases(t *testing.T) {
  30     for alias, name := range aliases {
  31         if _, ok := mains[name]; ok {
  32             continue
  33         }
  34 
  35         t.Errorf("alias %q leads nowhere", alias)
  36     }
  37 }
  38 
  39 func TestBlurbs(t *testing.T) {
  40     for name := range mains {
  41         if blurbs[name] != `` {
  42             continue
  43         }
  44         t.Errorf("no description/blurb for tool %q", name)
  45     }
  46 }
     File: ./mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright (c) 2026 pacman64
   4 
   5 Permission is hereby granted, free of charge, to any person obtaining a copy of
   6 this software and associated documentation files (the "Software"), to deal
   7 in the Software without restriction, including without limitation the rights to
   8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
   9 of the Software, and to permit persons to whom the Software is furnished to do
  10 so, subject to the following conditions:
  11 
  12 The above copyright notice and this permission notice shall be included in all
  13 copies or substantial portions of the Software.
  14 
  15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 SOFTWARE.
     File: ./podfeed/atom.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "bytes"
  29     "encoding/xml"
  30 )
  31 
  32 // atomFeed is an atom-format XML/RSS document: all its useful info is in the
  33 // Channels field, which is an array with usually only 1 item
  34 type atomFeed struct {
  35     Atom     string        `xml:"atom"`
  36     CC       string        `xml:"cc"`
  37     Channels []atomChannel `xml:"channel"`
  38     Content  string        `xml:"content"`
  39     Media    string        `xml:"media"`
  40     Version  int           `xml:"version"`
  41 }
  42 
  43 // atomChannel has all the channel tags in an atom-format document: its most
  44 // useful info is in its Items array field
  45 type atomChannel struct {
  46     Author          string     `xml:"author"`
  47     Description     string     `xml:"description"`
  48     Docs            string     `xml:"docs"`
  49     Explicit        string     `xml:"explicit"`
  50     Image           atomImage  `xml:"image"`
  51     Items           []atomItem `xml:"item"`
  52     Language        string     `xml:"language"`
  53     Link            string     `xml:"link"`
  54     PublicationDate string     `xml:"pubDate"`
  55     Summary         string     `xml:"summary"`
  56     Title           string     `xml:"title"`
  57     Subtitle        string     `xml:"subtitle"`
  58 
  59     // Copyright string `xml:"copyright"`
  60     // Generator string `xml:"generator"`
  61     // Categories     []string
  62     // Image          []string
  63     // Owner          []string
  64     // ManagingEditor string
  65     // LastBuildDate  string
  66     // Type           string
  67 }
  68 
  69 // atomImage is a channel's thumbnail image/logo
  70 type atomImage struct {
  71     Title string `xml:"title"`
  72     URL   string `xml:"url"`
  73 }
  74 
  75 // atomItem is a link to a podcast episode or to an article
  76 type atomItem struct {
  77     Author      string `xml:"author"`
  78     Description string `xml:"description"`
  79     Duration    string `xml:"duration"` // media-duration as hh:mm:ss
  80 
  81     Enclosures []atomEnclosure `xml:"enclosure"`
  82 
  83     Episode         int    `xml:"episode"`
  84     Explicit        string `xml:"explicit"`
  85     PublicationDate string `xml:"pubDate"`
  86     Summary         string `xml:"summary"`
  87     Title           string `xml:"title"`
  88 
  89     // Keywords []string // not sure these array items are strings
  90 }
  91 
  92 // atomEnclosure is an item's link, along with some useful metadata
  93 type atomEnclosure struct {
  94     Length int    `xml:"length"` // seems to be the media filesize
  95     Type   string `xml:"type"`   // MIME type for the media file
  96     URL    string `xml:"url"`    // the URL for the media file
  97 
  98     // special iTunes attributes
  99     AttrLength   int    `xml:"length,attr"`
 100     AttrType     string `xml:"type,attr"`
 101     AttrURL      string `xml:"url,attr"`
 102     AttrDuration string `xml:"duration,attr"`
 103 }
 104 
 105 // parseAtom decodes podcast/feed info from the bytes given
 106 func parseAtom(b []byte) (atomFeed, error) {
 107     var wrap atomFeed
 108     if !bytes.Contains(b, []byte(`itunes:`)) {
 109         err := xml.Unmarshal(b, &wrap)
 110         return wrap, err
 111     }
 112 
 113     b = bytes.ReplaceAll(b, []byte(`<itunes:`), []byte{'<'})
 114     b = bytes.ReplaceAll(b, []byte(`</itunes:`), []byte{'<', '/'})
 115     err := xml.Unmarshal(b, &wrap)
 116     return wrap, err
 117 }
     File: ./podfeed/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "bufio"
  29     "flag"
  30     "fmt"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const (
  37     titleUsage      = `title for the HTML result`
  38     itemLimitUsage  = `max items shown per feed, starting from latest; negative to disable`
  39     thumbnailsUsage = `show channel/podcast thumbnails`
  40     inlineUsage     = `inline/embed thumbnails as base64 data`
  41 )
  42 
  43 // config has all the cmd-line options: each has its own default value, but
  44 // can be explicitly set via one of the cmd-line flags
  45 type config struct {
  46     Feeds      []string
  47     Title      string
  48     ItemLimit  int
  49     Thumbnails bool
  50     Inline     bool
  51 }
  52 
  53 func parseFlags(usage string) config {
  54     cfg := config{
  55         Title:      `Latest Podcast Episodes`,
  56         ItemLimit:  -1,
  57         Thumbnails: true,
  58         Inline:     true,
  59     }
  60 
  61     flag.Usage = func() {
  62         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  63         flag.PrintDefaults()
  64     }
  65     flag.StringVar(&cfg.Title, `title`, cfg.Title, titleUsage)
  66     flag.IntVar(&cfg.ItemLimit, `max`, cfg.ItemLimit, itemLimitUsage)
  67     flag.BoolVar(&cfg.Thumbnails, `thumbs`, cfg.Thumbnails, thumbnailsUsage)
  68     flag.BoolVar(&cfg.Inline, `inline`, cfg.Inline, inlineUsage)
  69     flag.Parse()
  70 
  71     for _, a := range flag.Args() {
  72         if strings.HasPrefix(a, `https://`) || strings.HasPrefix(a, `http://`) {
  73             // it's a URI feed
  74             cfg.Feeds = append(cfg.Feeds, a)
  75             continue
  76         }
  77 
  78         // it's a text file with feed URIs, one per line
  79         lines, err := slurpFileLines(a)
  80         if err != nil {
  81             fmt.Fprintln(os.Stderr, err.Error())
  82             continue
  83         }
  84         cfg.Feeds = append(cfg.Feeds, lines...)
  85     }
  86 
  87     // if not given any filenames/URIs, read URIs from stdin
  88     if flag.NArg() == 0 {
  89         lines, err := slurpLines(os.Stdin)
  90         if err == nil {
  91             cfg.Feeds = append(cfg.Feeds, lines...)
  92         } else {
  93             fmt.Fprintln(os.Stderr, err.Error())
  94         }
  95     }
  96 
  97     return cfg
  98 }
  99 
 100 func slurpFileLines(fname string) ([]string, error) {
 101     f, err := os.Open(fname)
 102     if err != nil {
 103         return nil, err
 104     }
 105     defer f.Close()
 106     return slurpLines(f)
 107 }
 108 
 109 func slurpLines(r io.Reader) ([]string, error) {
 110     var lines []string
 111     const maxbufsize = 8 * 1024 * 1024 * 1024
 112     sc := bufio.NewScanner(r)
 113     sc.Buffer(nil, maxbufsize)
 114 
 115     for sc.Scan() {
 116         err := sc.Err()
 117         if err != nil {
 118             return lines, err
 119         }
 120 
 121         s := strings.TrimSpace(sc.Text())
 122         // ignore empty lines and comment lines
 123         if s == `` || strings.HasPrefix(s, `#`) {
 124             continue
 125         }
 126 
 127         lines = append(lines, s)
 128     }
 129 
 130     return lines, nil
 131 }
     File: ./podfeed/feeds.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "io"
  31     "strconv"
  32     "strings"
  33     "time"
  34 )
  35 
  36 // parseFeed takes raw RSS-string bytes and makes a feed object out of them
  37 func parseFeed(b []byte) (feed, error) {
  38     atom, err := parseAtom(b)
  39     if err != io.EOF && err != nil {
  40         return feed{}, err
  41     }
  42 
  43     if len(atom.Channels) == 0 {
  44         return feed{}, errors.New(`feed has no channels`)
  45     }
  46     if len(atom.Channels) > 1 {
  47         const msg = `multiple channels in a single feed aren't supported`
  48         return feed{}, errors.New(msg)
  49     }
  50 
  51     var feed feed
  52     ch := atom.Channels[0]
  53     feed.Title = ch.Title
  54     feed.Link = strings.Replace(ch.Link, `http://`, `https://`, 1)
  55     feed.ImageLink = ch.Image.URL
  56     feed.Description = clean(ch.Description)
  57 
  58     for _, v := range ch.Items {
  59         if len(v.Enclosures) == 0 {
  60             continue
  61         }
  62         feed.Items = append(feed.Items, adaptItem(v))
  63     }
  64     return feed, nil
  65 }
  66 
  67 // feed is a template-friendly representation of a parsed podcast feed
  68 type feed struct {
  69     Title       string
  70     Link        string
  71     ImageLink   string
  72     Description string
  73 
  74     Items []item
  75 }
  76 
  77 // item is a template-friendly representation of a podcast episode
  78 type item struct {
  79     Title       string
  80     Link        string
  81     Tooltip     string
  82     Description string
  83 }
  84 
  85 // adaptItem makes a podcast episodes's info more template-friendly
  86 func adaptItem(v atomItem) item {
  87     tooltip := ``
  88     duration := v.Duration
  89     // if duration is in seconds, turn it into the hh:mm:ss format
  90     if !strings.Contains(duration, `:`) {
  91         n, err := strconv.Atoi(duration)
  92         if err == nil && n > 0 {
  93             duration = (time.Duration(n) * time.Second).String()
  94         }
  95     }
  96 
  97     if duration != `` && v.PublicationDate != `` {
  98         const fs = `published: %s | duration: %s`
  99         tooltip = fmt.Sprintf(fs, v.PublicationDate, duration)
 100     }
 101     if duration == `` && v.PublicationDate != `` {
 102         tooltip = fmt.Sprintf(`published: %s`, v.PublicationDate)
 103     }
 104     if duration != `` && v.PublicationDate == `` {
 105         tooltip = fmt.Sprintf(`duration: %s`, v.PublicationDate)
 106     }
 107 
 108     enc := v.Enclosures[0]
 109     return item{
 110         Title:       v.Title,
 111         Link:        notEmptyOr(enc.URL, enc.AttrURL),
 112         Tooltip:     tooltip,
 113         Description: clean(v.Description),
 114     }
 115 }
     File: ./podfeed/fetch.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "io"
  29     "net/http"
  30     "runtime"
  31     "strings"
  32     "sync"
  33 )
  34 
  35 // Result is the payload/error combo resulting from trying to fetch a feed.
  36 type Result struct {
  37     Index int
  38     URI   string
  39 
  40     Feed    feed
  41     Problem error
  42 }
  43 
  44 // fetch tries to fetch all podcast feeds concurrently, to save time
  45 func fetch(cfg config) []Result {
  46     var wg sync.WaitGroup
  47     wg.Add(len(cfg.Feeds))
  48 
  49     // start rate-limiter up to the # of CPUs
  50     tickets := make(chan int, runtime.NumCPU())
  51     go func() {
  52         for i := range cfg.Feeds {
  53             tickets <- i
  54         }
  55 
  56         // wait until fetcher loop below has finished dispatching all tasks
  57         wg.Wait()
  58         close(tickets) // quit the fetcher loop
  59     }()
  60 
  61     // setup parameters and final results array
  62     res := make([]Result, len(cfg.Feeds))
  63     for i, uri := range cfg.Feeds {
  64         res[i] = Result{Index: i, URI: uri, Feed: feed{}, Problem: nil}
  65     }
  66 
  67     // concurrently fetch feeds
  68     for i := range tickets {
  69         go fetchItem(&res[i], &wg, cfg)
  70     }
  71     return res
  72 }
  73 
  74 // fetchItem is concurrently called/dispatched to try to fetch and decode a
  75 // single podcast feed: any error along the way is remembered as part of the
  76 // result, so the user can later be told about it
  77 func fetchItem(r *Result, wg *sync.WaitGroup, cfg config) {
  78     defer wg.Done()
  79 
  80     // read RSS feed
  81     b, err := slurp(r.URI)
  82     if err != nil {
  83         r.Problem = err
  84         return
  85     }
  86 
  87     // extract most important RSS info
  88     f, err := parseFeed(b)
  89     if err != nil {
  90         r.Problem = err
  91         return
  92     }
  93     // r.Feed = newFeed(f)
  94     r.Feed = f
  95 
  96     if !cfg.Thumbnails {
  97         // to hide thumbnails, use a no-data URI
  98         r.Feed.ImageLink = `data,`
  99         return
 100     }
 101 
 102     if !cfg.Inline {
 103         // if asked to, keep images as externally-linked resources
 104         return
 105     }
 106 
 107     // read image thumbnail
 108     b, err = slurp(f.ImageLink)
 109     if err != nil {
 110         r.Problem = err
 111         return
 112     }
 113 
 114     mime := `image/jpeg`
 115     if strings.Contains(f.ImageLink, `.png`) {
 116         mime = `image/png`
 117     }
 118 
 119     // data-URI-encode thumbnail, so it's part of the resulting webpage
 120     s, err := makeDataURI(b, mime)
 121     if err != nil {
 122         r.Problem = err
 123         return
 124     }
 125     r.Feed.ImageLink = s
 126 }
 127 
 128 func slurp(uri string) ([]byte, error) {
 129     resp, err := http.Get(uri)
 130     if err != nil {
 131         return nil, err
 132     }
 133     defer resp.Body.Close()
 134     return io.ReadAll(resp.Body)
 135 }
     File: ./podfeed/info.txt
   1 podfeed [URIs/filenames...]
   2 
   3 Keep track of what's on multiple podcasts/RSS feeds with auto-popup links and
   4 collapsible descriptions.
   5 
   6 After fetching all RSS feeds, this program emits script-free HTML code for a
   7 standalone webpage with links to all feed items, each having expandable
   8 descriptions.
   9 
  10 The cmd-line arguments can be a mix of direct URIs to podcast/RSS feeds and
  11 filenames: in any files given, each line is taken as a URI to check, unless
  12 the line is empty or starts with #, which marks it as a comment line.
     File: ./podfeed/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "html/template"
  31     "os"
  32 
  33     _ "embed"
  34 )
  35 
  36 //go:embed template.html
  37 var src string
  38 
  39 //go:embed info.txt
  40 var usage string
  41 
  42 // enable inlining/embedding thumbnails into page
  43 var funcs = template.FuncMap{
  44     `url`: func(s string) template.URL {
  45         return template.URL(s)
  46     },
  47 }
  48 
  49 var pageTemplate = template.Must(template.New(`main`).Funcs(funcs).Parse(src))
  50 
  51 // result is the payload given to the page template
  52 type result struct {
  53     Title string
  54     Feeds []feed
  55 }
  56 
  57 func Main() {
  58     cfg := parseFlags(usage)
  59 
  60     // fetch feeds concurrently
  61     res := fetch(cfg)
  62 
  63     // show which podcasts/feeds caused problems, and keep only the ones
  64     // which were loaded successfully
  65     page := result{Title: cfg.Title}
  66     for _, v := range res {
  67         if v.Problem != nil {
  68             fmt.Fprintln(os.Stderr, v.Problem.Error())
  69             continue
  70         }
  71 
  72         // limit feed's item-length, unless length-limiting was disabled via
  73         // a negative value
  74         if cfg.ItemLimit >= 0 && len(v.Feed.Items) > cfg.ItemLimit {
  75             v.Feed.Items = v.Feed.Items[:cfg.ItemLimit]
  76         }
  77         page.Feeds = append(page.Feeds, v.Feed)
  78     }
  79 
  80     // render HTML result to standard output
  81     w := bufio.NewWriter(os.Stdout)
  82     defer w.Flush()
  83     pageTemplate.Execute(w, page)
  84 }
     File: ./podfeed/strings.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package podfeed
  26 
  27 import (
  28     "encoding/base64"
  29     "fmt"
  30     "math"
  31     "regexp"
  32     "strings"
  33 )
  34 
  35 // note: the tag matcher can't rid anchor tags of inner tags in its content
  36 const tagRE = `</?[a-z][a-z1-6]*( +[a-z]+ *= *"[a-z A-Z0-9-]*")*( /)?>`
  37 
  38 // regex to match opening/closing HTML tags, used in function `clean`; the
  39 // first letter explicitly excludes a, to avoid matching/replacing anchor tags
  40 var tagMatcher = regexp.MustCompile(tagRE)
  41 
  42 // regex to match ampersand escapes, used in function `clean`
  43 var ampersandMatcher = regexp.MustCompile(`&[a-zA-Z]+;`)
  44 
  45 var ampersandEscapes = map[string]string{
  46     `&nbsp`: ` `,
  47     `&amp`:  `&`,
  48     `&lt`:   `<`,
  49     `&gt`:   `>`,
  50 }
  51 
  52 // clean improves the content of descriptions, by removing typical markup
  53 // junk often found in RSS feeds
  54 func clean(s string) string {
  55     s = tagMatcher.ReplaceAllStringFunc(s, func(s string) string {
  56         if strings.HasPrefix(s, `<a `) {
  57             return s
  58         }
  59 
  60         switch s {
  61         case `</a>`:
  62             return `</a>`
  63         case `<br/>`, `<br />`:
  64             return "\n"
  65         default:
  66             return ``
  67         }
  68     })
  69 
  70     s = ampersandMatcher.ReplaceAllStringFunc(s, func(s string) string {
  71         sub, ok := ampersandEscapes[s]
  72         if ok {
  73             return sub
  74         }
  75         return s
  76     })
  77 
  78     return s
  79 }
  80 
  81 // makeDataURI encodes the bytes given into a MIME-typed base64-encoded URI
  82 func makeDataURI(b []byte, mime string) (string, error) {
  83     var buf strings.Builder
  84     base64len := int(math.Ceil(4 * float64(len(b)) / 3))
  85     buf.Grow(len(`data:`) + len(mime) + len(`;base64,`) + base64len)
  86     fmt.Fprintf(&buf, `data:%s;base64,`, mime)
  87 
  88     enc := base64.NewEncoder(base64.StdEncoding, &buf)
  89     defer enc.Close()
  90 
  91     _, err := enc.Write(b)
  92     if err != nil {
  93         return ``, err
  94     }
  95     return buf.String(), nil
  96 }
  97 
  98 // notEmptyOr simplifies control flow around this app
  99 func notEmptyOr(s, fallback string) string {
 100     if len(s) > 0 {
 101         return s
 102     }
 103     return fallback
 104 }
     File: ./podfeed/template.html
   1 <!DOCTYPE html>
   2 <html lang="en">
   3 
   4 <head>
   5     <meta charset="UTF-8">
   6     <meta name="viewport" content="width=device-width, initial-scale=1.0">
   7     <link rel="icon" href="data:,">
   8     <title>{{ .Title }}</title>
   9     <style>
  10         body {
  11             font-size: 0.9rem;
  12             margin: 0 0 2rem 0;
  13             font-family: system-ui, -apple-system, sans-serif;
  14         }
  15 
  16         main {
  17             margin: auto;
  18             display: flex;
  19             width: fit-content;
  20         }
  21 
  22         h1 {
  23             top: 0;
  24             position: sticky;
  25             font-size: 0.9rem;
  26             text-align: center;
  27             background-color: white;
  28         }
  29 
  30         img {
  31             margin: auto;
  32             margin-bottom: 1rem;
  33             display: block;
  34             max-width: 15ch;
  35         }
  36 
  37         section {
  38             width: 48ch;
  39             padding: 0.3rem;
  40             margin: 0 0.1rem;
  41         }
  42 
  43         section:nth-child(2n+1) {
  44             background-color: #eee;
  45         }
  46 
  47         a {
  48             color: steelblue;
  49             text-decoration: none;
  50         }
  51 
  52         details p {
  53             line-height: 1.3rem;
  54         }
  55     </style>
  56 </head>
  57 
  58 <body>
  59     <main>
  60         {{- range .Feeds }}
  61         <article>
  62             <h1>
  63                 <a target="_blank" rel="noreferrer" href="{{ .Link }}">{{ .Title }}</a>
  64             </h1>
  65             <img src="{{ .ImageLink | url }}">
  66             {{- range .Items }}
  67             <section>
  68                 <details>
  69                     <summary title="{{ .Tooltip }}">
  70                         <a target="_blank" rel="noreferrer" href="{{ .Link }}">{{ .Title }}</a>
  71                     </summary>
  72                     <p>{{ .Description }}</p>
  73                 </details>
  74             </section>
  75             {{- end }}
  76         </article>
  77         {{- end }}
  78     </main>
  79 </body>
  80 
  81 </html>
     File: ./wports/wports.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package wports
  26 
  27 import (
  28     "net"
  29     "os"
  30     "runtime"
  31     "strconv"
  32     "sync"
  33     "time"
  34 )
  35 
  36 const info = `
  37 wports [options...]
  38 
  39 Which PORTS finds all localhost TCP ports currently in use.
  40 
  41 The only option available is to show this help message, using any of
  42 "-h", "--h", "-help", or "--help", without the quotes.
  43 `
  44 
  45 const (
  46     // timeout gives plenty of waiting-time to check localhost ports
  47     timeout = 500 * time.Millisecond
  48 
  49     // lastPort = int(^uint16(0))
  50 
  51     lastPort = 1<<16 - 1
  52 )
  53 
  54 // result values report which ports are currently being used
  55 type result struct {
  56     // When is the date/time the port was checked
  57     When time.Time
  58 
  59     // Port is the network-port number checked
  60     Port int
  61 }
  62 
  63 func Main() {
  64     args := os.Args[1:]
  65 
  66     if len(args) > 0 {
  67         switch args[0] {
  68         case `-h`, `--h`, `-help`, `--help`:
  69             os.Stdout.WriteString(info[1:])
  70             return
  71 
  72         case `--`:
  73             args = args[1:]
  74         }
  75     }
  76 
  77     _, err := os.Stdout.WriteString("when\tport\n")
  78     if err != nil {
  79         return
  80     }
  81 
  82     results := make(chan result)
  83     go run(results)
  84 
  85     // buf is a buffer big enough for any output line
  86     var buf [32]byte
  87 
  88     for r := range results {
  89         line := buf[:0]
  90         line = r.When.AppendFormat(line, `2006-01-02 15:04:05`)
  91         line = append(line, '\t')
  92         line = strconv.AppendInt(line, int64(r.Port), 10)
  93         line = append(line, '\n')
  94 
  95         _, err := os.Stdout.Write(line)
  96         if err != nil {
  97             return
  98         }
  99     }
 100 }
 101 
 102 // run asynchronously dispatches tasks to check all ports
 103 func run(results chan<- result) {
 104     defer close(results) // allow the main app to end
 105 
 106     var tasks sync.WaitGroup
 107     // the number of tasks is always known in advance
 108     tasks.Add(lastPort)
 109 
 110     // permissions is buffered to limit concurrency to the core-count
 111     permissions := make(chan struct{}, runtime.NumCPU())
 112     defer close(permissions)
 113 
 114     // avoid checking port 0, as it's the `anything-available` port
 115     for port := 1; port <= lastPort; port++ {
 116         // wait until some concurrency-room is available, before proceeding
 117         permissions <- struct{}{}
 118         go check(port, &tasks, permissions, results)
 119     }
 120 
 121     // wait for all checks to finish, before closing the `results` channel,
 122     // which in turn would quit the whole app right away
 123     tasks.Wait()
 124 }
 125 
 126 // check figures out if the TCP port given is being used
 127 func check(port int, tasks *sync.WaitGroup, turn <-chan struct{}, res chan<- result) {
 128     when := time.Now()
 129     defer tasks.Done()
 130 
 131     var buf [24]byte
 132     addr := strconv.AppendInt(append(buf[:0], ':'), int64(port), 10)
 133     conn, err := net.DialTimeout(`tcp`, string(addr), timeout)
 134     if err != nil {
 135         <-turn
 136         return
 137     }
 138     conn.Close()
 139 
 140     <-turn
 141     // only report ports being used
 142     res <- result{When: when, Port: port}
 143 }