gopher://gopher.someodd.zip:70/1/catalog/applets/Gopher AppletsLittle programs that run in gopherspace.2026-06-09T00:00:00Zgopher.someodd.zipgopher://gopher.someodd.zip:70/1/applets/interlogInterlog: Gopher-style forum.2026-06-09T00:00:00Z2026-06-09T00:00:00ZText BBS/4ch-style forum.Text BBS/4ch-style forum.gopher://gopher.someodd.zip:70/1/applets/README.gophermapREADME.gophermap2026-05-23T00:00:00Z2026-05-23T00:00:00Zgopher://gopher.someodd.zip:70/9/applets/girc.lhs.bak-20260615girc.lhs.bak-202606152026-05-23T00:00:00Z2026-05-23T00:00:00Zgopher://gopher.someodd.zip:70/1/applets/webhole.lhswebhole: browse any website as a gopherhole2026-05-23T00:00:00Z2026-05-23T00:00:00Zpaste a web-page URL and walk it as a gopher menu -- prose becomes ...gopher://gopher.someodd.zip:70/0/applets/curl/README.txtREADME.txt2026-05-23T00:00:00Z2026-05-23T00:00:00ZThe idea of these applets is to evangalize Gopher to people who don...gopher://gopher.someodd.zip:70/1/applets/curl/ask.lhsask: question a local LLM over gopher2026-05-14T00:00:00Z2026-05-23T00:00:00Zask a short question, get a short plain-text answer from a local LL...gopher://gopher.someodd.zip:70/1/applets/whoami.lhswhoami.lhs2026-05-22T00:00:00Z2026-05-22T00:00:00Zgopher://gopher.someodd.zip:70/1/applets/bujo/bujo.lhsbujo: a bullet journal over gopher2026-05-22T00:00:00Z2026-05-22T00:00:00Za dead-simple bullet journal you keep entirely in gopherspace --- o...gopher://gopher.someodd.zip:70/1/applets/curl/atomize.lhsatomize: any web page into an Atom feed2026-05-22T00:00:00Z2026-05-22T00:00:00Zpaste a web-page URL, get back an Atom feed -- deterministically di...gopher://gopher.someodd.zip:70/1/applets/curl/txtify.lhstxtify: any web page into plaintext2026-05-22T00:00:00Z2026-05-22T00:00:00Zpaste a web-page URL, get back the most likely main textual content...gopher://gopher.someodd.zip:70/1/applets/girc.lhsgirc: chat on IRC over gopher2026-05-20T00:00:00Z2026-05-21T00:00:00ZRead #main's scrollback over gopher (the shared oddbot channel log)...gopher://gopher.someodd.zip:70/1/applets/4_ch.lhs4_ch.lhs2026-05-18T00:00:00Z2026-05-18T00:00:00Zgopher://gopher.someodd.zip:70/1/applets/grep.lhsSearch this gopherhole with grep2026-05-18T00:00:00Z2026-05-18T00:00:00Zgopher://gopher.someodd.zip:70/1/applets/search.lhssearch.lhs2026-05-18T00:00:00Z2026-05-18T00:00:00Zgopher://gopher.someodd.zip:70/1/applets/diggingsdiggings: prospect gopherspace2026-05-14T00:00:00Z2026-05-14T00:00:00Zsurf gopherspace through a proxy, strike gold for finding selectors...surf gopherspace through a proxy, strike gold for finding selectors nobody has visited, and leave verified tripcode posts on any page --- the whole protocol as one text boardgopher://gopher.someodd.zip:70/1/applets/qdbqdb: a Gopher <-> IRC quote database2026-05-14T00:00:00Z2026-05-14T00:00:00ZIRC quotes captured in #main with .qdb, kept forever as plain text,...IRC quotes captured in #main with .qdb, kept forever as plain text, browsable in gopherspace --- read as text or as a Microsoft Comic Chat stripgopher://gopher.someodd.zip:70/1/applets/icecastinternet radio over gopher2026-05-10T00:00:00Z2026-05-12T00:00:00Zstream any http/https internet radio over gopher as audio passthrou...stream any http/https internet radio over gopher as audio passthrough or 144p showcqt visualizationgopher://gopher.someodd.zip:70/1/applets/fewbytes/yt.lhsYouTube at modem speeds2026-05-11T00:00:00Z2026-05-11T00:00:00Zstream youtube over gopher as 6 kbps audio, 144p video, or plain-te...gopher://gopher.someodd.zip:70/1/applets/figlet.lhsFiglet ASCII font art generator2026-05-10T00:00:00Z2026-05-10T00:00:00Zcreate ASCII art text in gopherspacegopher://gopher.someodd.zip:70/0/applets/digest.hsdigest.hs2026-05-06T00:00:00Z2026-05-06T00:00:00Z#!/usr/bin/env runghc {-# LANGUAGE OverloadedStrings #-} {-# LANGUA... IO ()
dbg s = lookupEnv "DEBUG" >>= \d ->
case d of Just "1" -> hPutStrLn stderr ("[dbg] " ++ s); _ -> pure ()
trim :: String -> String
trim = dropWhile isSpace . dropWhileEnd isSpace
lower :: String -> String
lower = map toLower
short :: Int -> String -> String
short n s = let t = unwords (words s) in if length t > n then take (n-1) t ++ "…" else t
section :: String -> IO ()
section t = putStrLn $ "\n===== " ++ t ++ " ====="
-- curl / fetch / jq
curlBody :: String -> IO (Maybe String)
curlBody u = do
ik <- lookupEnv "INSECURE"
let k = if ik == Just "1" && ("someodd.zip" `isInfixOf` u) && ("http" `isPrefixOf` u) then ["-k"] else []
a = ["-fsSL","--location","--connect-timeout","3","--max-time","8","-A","someodd-digest/hs","--"] ++ k ++ [u]
dbg $ "curl " ++ unwords a
readProcessWithExitCode "curl" a "" >>= \case
(ExitSuccess, out, _) -> pure (Just out)
_ -> pure Nothing
fetch :: String -> IO (Maybe String)
fetch u
| "gopher://" `isPrefixOf` u = curlBody u
| "https://" `isPrefixOf` u = curlBody u >>= \case { Just x -> pure (Just x); _ -> curlBody ("http://" ++ drop 8 u) }
| otherwise = curlBody u
jq :: String -> String -> IO (Maybe String)
jq e s = readProcessWithExitCode "jq" ["-r", e] s >>= \case
(ExitSuccess, o, _) -> let t = trim o in pure $ if t == "" || t == "null" then Nothing else Just t
_ -> pure Nothing
-- 1) latest toot
toot :: IO ()
toot = do
section "Toot"
fetch "https://fosstodon.org/api/v1/accounts/lookup?acct=someodd" >>= \case
Nothing -> putStrLn "(couldn't resolve account)"
Just js -> jq ".id" js >>= \case
Nothing -> putStrLn "(couldn't resolve account)"
Just aid -> fetch ("https://fosstodon.org/api/v1/accounts/" ++ aid ++ "/statuses?limit=1&exclude_replies=true&exclude_reblogs=true") >>= \case
Nothing -> putStrLn "(unreachable)"
Just s -> jq ".[0] | select(.!=null) | [.created_at, (.content//\"\")] | @tsv" s >>= \case
Nothing -> putStrLn "(none found)"
Just tsv -> let (ts, rest) = span (/= '\t') tsv
body = stripHTML (drop 1 rest)
in putStrLn (ts ++ " — " ++ short 280 body)
where
stripHTML = decode . kill
kill [] = []
kill ('<':xs) = kill (drop 1 (dropWhile (/= '>') xs))
kill (x:xs) = x : kill xs
decode = rep " " " " . rep "&" "&" . rep "<" "<" . rep ">" ">" . rep """ "\"" . rep "'" "'"
rep a b = go where go s | a `isPrefixOf` s = b ++ go (drop (length a) s)
| otherwise = case s of [] -> []; (c:cs) -> c : go cs
-- 2) latest bartleby library accession (via gopher atom feed)
bartleby :: IO ()
bartleby = do
section "Bartleby Library"
fetch "gopher://gopher.someodd.zip:70/0/library/catalog/feed.xml" >>= \case
Nothing -> putStrLn "(unreachable)"
Just feed -> do
mt <- xpath feed "//a:entry[1]/a:title"
ms <- xpath feed "//a:entry[1]/a:summary"
let title = stripMd (fromMaybe "" mt)
summary = fromMaybe "" ms
if null title
then putStrLn "(no accessions)"
else do
putStrLn $ "Latest: " ++ title
case summary of
"" -> pure ()
s | s == title -> pure ()
| otherwise -> putStrLn $ "Summary: " ++ short 280 s
where
stripMd s = if ".md" `isSuffixOf` s then take (length s - 3) s else s
xpath input expr =
let args = ["sel", "-N", "a=http://www.w3.org/2005/Atom", "-t", "-v", expr]
in readProcessWithExitCode "xmlstarlet" args input >>= \case
(ExitSuccess, o, _) ]]>gopher://gopher.someodd.zip:70/0/applets/server.shserver.sh2025-11-13T00:00:00Z2025-11-13T00:00:00Z#!/usr/bin/env bash/dev/null && echo 'UP' || echo 'DOWN'"
# --- 2. Systemd Services ---
# Loops through a list of services and checks if they are active.
echo
echo "--- Services ---"
for service in nginx venusia phorum smbd apcupsd; do
print_check "$service" "systemctl is-active --quiet $service && echo 'UP' || echo 'DOWN'"
done
# special case for mpd because it runs as user
print_check mpd "pgrep -u baudrillard mpd >/dev/null && echo 'UP' || echo 'DOWN'"
# --- 3. Counter-Strike 1.6 ---
# Runs the dedicated stats script and pretty-prints the JSON output.
echo
echo "--- Counter-Strike 1.6 ---"
# Pipe the JSON output to 'jq' to format it and 'sed' to indent it.
/usr/local/bin/cs_stats.sh | jq . | sed 's/^/ /'
# --- 4. Disk Usage ---
echo
echo "--- Storage ---"
print_check "Disk Usage (Root)" "df -h / | awk 'NR==2 {print \$5}'"
print_check "Disk Usage (BackupRAID)" "df -h /media/root/BackupRAID | awk 'NR==2 {print \$5}'"
# --- 5. Restic Backups ---
# Checks the logs for any sign of errors or warnings.
echo
echo "--- Restic Backups ---"
# Check the backup log for errors. If found, print the whole log.
if grep -q -E 'Warning|error|failed' /var/log/restic-backup.log; then
printf "%-25s: %s\n" "Backup Status" "ERRORS FOUND (see log below)"
echo "------------------------- Restic Backup Log -------------------------"
sed 's/^/ /' /var/log/restic-backup.log
echo "-------------------------------------------------------------------"
else
print_check "Backup Status" "echo 'OK'"
fi
# This command will print any error lines from the maintenance log, or 'No errors found' if none exist.
print_check "Maintenance Status" "grep 'errors' /var/log/restic-maintain.log || echo 'No errors found'"
echo
echo "LAST BACKUP LOG, 3 LINES"
echo
tail -n 3 /var/log/restic-backup.log
echo
echo "MAINTENANCE LOG, 3 LINES"
echo
tail -n 3 /var/log/restic-maintain.log
# --- 6. Hardware & System ---
echo
echo "--- Hardware & System ---"
print_check "System Uptime" "uptime -p"
print_check "Last UPS Event" "tail -n 1 /var/log/apcupsd.events"
echo
echo "UPS/BATTERY"
apcaccess status
echo
inxi -SCGIDm -t cm5
echo
echo "--- Report Complete ---"
]]>