commit 9fb726805a2f9ae34d80ccf8332a09343dbef709
Author: Chris <chris@echoz.io>
Date: Fri, 3 Oct 2025 15:20:41 +0200
feat: init
Diffstat:
12 files changed, 918 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,10 @@
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+insert_final_newline = true
+max_line_length = 72
+trim_trailing_whitespace = true
+
+[*.nix]
+indent_size = 2
diff --git a/.envrc b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+result
+.direnv
+.env
diff --git a/flake.lock b/flake.lock
@@ -0,0 +1,26 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1759381078,
+ "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "ref": "nixos-unstable",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
@@ -0,0 +1,74 @@
+{
+ inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
+
+ outputs =
+ { self, nixpkgs, ... }:
+ let
+ lib = nixpkgs.lib;
+ systems = [
+ "aarch64-linux"
+ "x86_64-linux"
+ ];
+ argsFor = system: {
+ inherit system;
+ pkgs = nixpkgs.legacyPackages.${system};
+ };
+ forAllSystems = f: lib.genAttrs systems (system: f (argsFor system));
+ in
+ {
+ devShells = forAllSystems (
+ { pkgs, ... }:
+ {
+ default = pkgs.mkShell {
+ packages = with pkgs; [
+ bash
+ coreutils
+ curl
+ grimblast
+ inotify-tools
+ jq
+ slurp
+ wf-recorder
+ wl-clipboard
+ ];
+ };
+ }
+ );
+
+ packages = forAllSystems (
+ { pkgs, system, ... }:
+ {
+ default = self.packages.${system}.imsh-clients;
+ imsh-clients = pkgs.symlinkJoin {
+ name = "imsh-clients";
+ paths = with self.packages.${system}; [
+ imsh-shot
+ imsh-cast
+ imsh-cast-monitor
+ ];
+ };
+ imsh-shot = pkgs.callPackage ./imsh-shot { };
+ imsh-cast = pkgs.callPackage ./imsh-cast { };
+ imsh-cast-monitor = pkgs.callPackage ./imsh-cast-monitor { };
+ }
+ );
+
+ homeManagerModules = {
+ default = self.homeManagerModules.imsh-clients;
+ imsh-clients = ./modules/home-manager.nix;
+ };
+
+ formatter = forAllSystems (
+ { pkgs, ... }:
+ pkgs.treefmt.withConfig {
+ settings = {
+ on-unmatched = "info";
+ formatter.nixfmt = {
+ command = lib.getExe pkgs.nixfmt-rfc-style;
+ includes = [ "*.nix" ];
+ };
+ };
+ }
+ );
+ };
+}
diff --git a/imsh-cast-monitor/default.nix b/imsh-cast-monitor/default.nix
@@ -0,0 +1,18 @@
+{
+ writeShellApplication,
+
+ coreutils,
+ inotify-tools,
+ jq,
+ ...
+}:
+writeShellApplication {
+ name = "imsh-cast-monitor";
+ runtimeInputs = [
+ coreutils
+ inotify-tools
+ jq
+ ];
+ bashOptions = [ ];
+ text = builtins.readFile ./imsh-cast-monitor.sh;
+}
diff --git a/imsh-cast-monitor/imsh-cast-monitor.sh b/imsh-cast-monitor/imsh-cast-monitor.sh
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+
+argv0="imsh-cast-monitor"
+imshCastArgv0="imsh-cast"
+wfRecorderArgv0="wf-recorder"
+
+usage() {
+cat <<EOF
+$argv0 - Display a flashing circle while imsh-cast is recording.
+
+Usage: $argv0 [pid-file] [options...]
+
+Options:
+ pid-file
+ PID file to watch for an active process.
+ Defaults to: \$XDG_RUNTIME_DIR/$imshCastArgv0-0.pid
+
+ -h, --help
+ Display this message.
+EOF
+}
+
+fatalUser() {
+cat >&2 <<EOF
+Error: $1
+
+See $argv0 --help for more information.
+EOF
+exit 2
+}
+
+warn() {
+ printf "Warning: %s\n" "$1" >&2
+}
+
+status() {
+ text="$1"; shift
+ class="$1"; shift
+ jq -nc \
+ --arg text "$text" \
+ --arg class "$class" \
+ '{"text": $text, "class": [ "imsh-cast-monitor", $class]}'
+}
+
+pidFile="$XDG_RUNTIME_DIR/$imshCastArgv0-0.pid"
+positional=()
+ignoreOpts=
+while [[ $# -gt 0 ]]; do
+ case "$ignoreOpts$1" in
+ -h|--help) usage; exit 0;;
+ --) ignoreOpts=1;;
+ -*) fatalUser "invalid option: $1";;
+ *) positional+=("$1");;
+ esac
+done
+set -- "${positional[@]}"
+
+pidFile="${1:-$pidFile}"; shift
+
+if [[ $# -gt 0 ]]; then
+ fatalUser "extraneous positional arguments ($*)"
+elif [[ -z "$pidFile" ]]; then
+ fatalUser "missing pid-file argument"
+fi
+
+state=
+while true; do
+ text=
+ class=
+ [[ -e "$pidFile" ]] && pid="$(tr -d '[:space:]' <"$pidFile")" || pid=
+ [[ "$pid" -gt 0 ]] && [[ -e "/proc/$pid" ]] \
+ && pidArgv0="$(basename "$(tr '\0' '\n' <"/proc/$pid/cmdline" | head -n1)")" \
+ || pidArgv0=
+
+ if [[ "$pidArgv0" = "$wfRecorderArgv0" ]]; then
+ state=$((!state))
+ ((state)) && text=$'\uebb4' || text=$'\uebb5'
+ class="recording"
+ elif [[ -n "$pid" ]] || [[ -e "$pidFile" ]]; then
+ warn "invalid PID file contents, this may have been caused by a previous $imshCastArgv0 crash."
+ state=
+ text=$'\ueabd'
+ class="error"
+ else
+ state=
+ text=""
+ class="inactive"
+ fi
+
+ status "$text" "$class"
+
+ inotifywait "$(dirname "$pidFile")" \
+ --include "$(basename "$pidFile")" \
+ --timeout 1 \
+ --event create \
+ --event delete \
+ --event modify \
+ >/dev/null 2>&1
+done
diff --git a/imsh-cast/default.nix b/imsh-cast/default.nix
@@ -0,0 +1,25 @@
+{
+ writeShellApplication,
+
+ coreutils,
+ curl,
+ jq,
+ slurp,
+ wf-recorder,
+ wl-clipboard,
+ ...
+}:
+writeShellApplication {
+ name = "imsh-cast";
+ runtimeInputs = [
+ coreutils
+ curl
+ jq
+ slurp
+ wf-recorder
+ wl-clipboard
+ ];
+ bashOptions = [ ];
+ excludeShellChecks = [ "SC2194" ];
+ text = builtins.readFile ./imsh-cast.sh;
+}
diff --git a/imsh-cast/imsh-cast.sh b/imsh-cast/imsh-cast.sh
@@ -0,0 +1,333 @@
+#!/usr/bin/env bash
+
+argv0="imsh-cast"
+
+usage() {
+cat <<EOF
+$argv0 - Capture and optionally upload a screencast to an imsh server
+
+Usage: $argv0 [options...]
+
+Options:
+ -u, --upload
+ Uploads the screencast to imsh.
+
+ -a, --api-key=key|@file|%cmd
+ API key for uploading to imsh.
+ Prefix with "@" to read from a file.
+ Prefix with "%" to run a command.
+
+ -e, --endpoint=url
+ The imsh endpoint to upload to.
+ Defaults to https://scrn.is/api/v1/upload
+
+ -o, --output=path-pattern
+ Path to write captured screencast to. Gets passed through date and
+ supports the same format strings.
+
+ -U, --utc
+ Call date with -u to use UTC for added privacy when sharing.
+
+ -C, --copy
+ Copy the absolute filesystem path of the screencast to the
+ clipboard. If uploading, instead copy the returned URL.
+
+ --pid-file=path
+ Specify the path to read/write PID from. If a file is found at this
+ location any other arguments will be ignored, and the PID contained
+ within will be sent a SIGINT.
+ Defaults to: \$XDG_RUNTIME_DIR/$argv0-0.pid
+
+ -t, --filetype=ext
+ Specifies the file extension for the temporary file used when no
+ output is specified. This has no effect when an output is specified.
+ Default: mp4
+
+ -A, --audio
+ Records audio in addition to video. To specify a device to record
+ from use --audio-device instead.
+
+ --audio-device=name
+ Specify the device to record audio from. Implies --audio.
+
+ -c, --codec=codec
+ Specifies the codec of the video. These can be found by using:
+ ffmpeg -encoders
+
+ -r, --framerate=number
+ Specifies a constant framerate.
+
+ -d, --device=name
+ Selects the device to use when encoding the video.
+
+ --no-dmabuf
+ Forces CPU copy while for recording.
+
+ -D, --no-damage
+ Continuously records even when there are no new frames.
+
+ -m, --muxer=name
+ Specifies the output format to a specific muxer instead of detecting
+ it from the filename.
+
+ -x, --pixel-format=name
+ Specifies the output pixel format. These can be found by running:
+ ffmpeg -pix_fmts
+
+ -O, --display-output
+ Specifies the display output to record.
+
+ -p, --codec-param=option=value
+ Change video codec parameters.
+
+ -F, --filter=option=value
+ Specifies the ffmpeg filter string to use.
+
+ -b, --bframes=number
+ Specifies the max number of b-frames to be used. If b-frames are not
+ supported by your hardware, set this to 0.
+
+ -B, --buffrate=number
+ Specifies the buffer's expected framerate.
+
+ --audio-backend=name
+ Specifies the audio bakcned to use, e.g. pipewire.
+
+ --audio-codec=codec
+ Specifies the audio codec. These can be found by running:
+ ffmpeg -encoders
+
+ -X, --sample-format=fmt
+ Specifies the audio sample format. These can be found by running:
+ ffmpeg -sample_fmts
+
+ -R, --sample-rate=number
+ Specifies the audio sample rate in Hz.
+ Default: 48000
+
+ -P, --audio-codec-param=option=value
+ Change audio codec parameters.
+
+ -y, --overwrite
+ Force overwriting the output file without prompting.
+
+ -h, --help
+ Display this message.
+EOF
+}
+
+info() {
+ printf "Info: %s\n" "$1" >&2
+}
+
+warn() {
+ printf "Warning: %s\n" "$1" >&2
+}
+
+fatal() {
+ printf "Error: %s\n" "$1" >&2
+ exit 1
+}
+
+fatalUser() {
+cat >&2 <<EOF
+Error: $1
+
+See $argv0 --help for more information.
+EOF
+exit 2
+}
+
+checkInput() {
+ err=()
+ while [[ $# -gt 0 ]]; do
+ msg="$1"; shift
+ cond="$1"; shift
+ value="$1"; shift
+ [[ "$cond" -ne 0 ]] && [[ -z "$value" ]] && err+=("$msg" ", ")
+ done
+ if [[ ${#err[@]} -gt 0 ]]; then
+ IFS=""
+ fatalUser "${err[*]::${#err[@]}-1}" >&2
+ fi
+}
+
+optsWithArgs=(
+ -a --api-key
+ -e --endpoint
+ -o --output
+ --audio-device
+ -c --codec
+ -r --framerate
+ -d --device
+ -t --filetype
+ -m --muxer
+ -x --pixel-format
+ -p --codec-param
+ -F --filter
+ -b --bframes
+ -B --buffrate
+ --audio-backend
+ --audio-codec
+ -X --sample-format
+ -R --sample-rate
+ -P --audio-codec-param
+)
+
+# Preprocess options:
+# Expand -fff to -f -f -f.
+# Expand -kv to -k v.
+# Expand --key=value to --key value.
+# Stop preprocessing once -- is observed.
+argv=()
+ignoreOpts=
+while [[ $# -gt 0 ]]; do
+ case "$ignoreOpts$1" in
+ --) argv+=("$1"); ignoreOpts=1;;
+ --*=*)
+ argv+=("${1%%=*}" "${1#*=}")
+ (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|${1%%=*}"* ]]) \
+ || fatalUser "invalid option: $1"
+ ;;
+ --*) argv+=("$1");;
+ -*)
+ for ((i=1; i < ${#1}; ++i)); do
+ argv+=("-${1:i:1}");
+ if (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|-${1:i:1}"* ]]) && [[ -n "${1:i+1}" ]]; then
+ argv+=("${1:i+1}")
+ break
+ fi
+ done;;
+ *) argv+=("$1");;
+ esac
+ shift
+done
+set -- "${argv[@]}"
+
+upload=
+apiKey=
+endpoint=https://scrn.is/api/v1/upload
+out=
+save=
+copy=
+pidFile="$XDG_RUNTIME_DIR/$argv0-0.pid"
+filetype=mp4
+dateArgs=()
+wfRecorderArgs=()
+
+positional=()
+ignoreOpts=
+while [[ $# -gt 0 ]]; do
+ case "$ignoreOpts$1" in
+ -u|--upload) upload=1;;
+ -a|--api-key) apiKey="$2"; shift;;
+ -e|--endpoint) endpoint="$2"; shift;;
+ -o|--output) out="$2"; save=1; shift;;
+ -U|--utc) dateArgs+=(-u);;
+ -C|--copy) copy=1;;
+ --pid-file) pidFile="$2"; shift;;
+ -t|--filetype) filetype="$2"; shift;;
+ -A|--audio) wfRecorderArgs+=(--audio);;
+ --audio-device) wfRecorderArgs+=("--audio=$2"); shift;;
+ -c|--codec) wfRecorderArgs+=(--codec "$2"); shift;;
+ -r|--framerate) wfRecorderArgs+=(--framerate "$2"); shift;;
+ -d|--device) wfRecorderArgs+=(--device "$2"); shift;;
+ --no-dmabuf) wfRecorderArgs+=(--no-dmabuf);;
+ -D|--no-damage) wfRecorderArgs+=(--no-damage);;
+ -m|--muxer) wfRecorderArgs+=(--muxer "$2"); shift;;
+ -x|--pixel-format) wfRecorderArgs+=(--pixel-format "$2"); shift;;
+ -O|--display-output) wfRecorderArgs+=(--output "$2"); shift;;
+ -p|--codec-param) wfRecorderArgs+=(--codec-param "$2"); shift;;
+ -F|--filter) wfRecorderArgs+=(--filter "$2"); shift;;
+ -b|--bframes) wfRecorderArgs+=(--bframes "$2"); shift;;
+ -B|--buffrate) wfRecorderArgs+=(--buffrate "$2"); shift;;
+ --audio-backend) wfRecorderArgs+=(--audio-backend "$2"); shift;;
+ --audio-codec) wfRecorderArgs+=(--audio-codec "$2"); shift;;
+ -X|--sample-format) wfRecorderArgs+=(--sample-format "$2"); shift;;
+ -R|--sample-rate) wfRecorderArgs+=(--sample-rate "$2"); shift;;
+ -P|--audio-codec-param) wfRecorderArgs+=(--audio-codec-param "$2"); shift;;
+ -y|--overwrite) wfRecorderArgs+=(--overwrite);;
+ -h|--help) usage; exit 0;;
+ --) ignoreOpts=1;;
+ -*) fatalUser "invalid option: $1";;
+ *) positional+=("$1");;
+ esac
+ shift
+done
+set -- "${positional[@]}"
+
+if [[ -e "$pidFile" ]]; then
+ set -e
+ info "found PID file: $pidFile"
+ pid="$(tr -d '[:space:]' <"$pidFile")"
+ if [[ "$pid" -gt 0 ]]; then
+ info "sending SIGINT to PID $pid"
+ kill -INT "$pid"
+ else
+ warn "PID file empty or malformed: $pid"
+ fi
+ info "deleting PID file: $pidFile"
+ rm -f "$pidFile"
+ exit 0
+fi
+
+checkInput \
+ "extraneous positional arguments ($*)" $# ""\
+ "missing one of --output --upload" 1 "$save$out$upload" \
+ "missing value for --api-key" "$upload" "$apiKey" \
+ "missing value for --output" "$save" "$out" \
+
+if [[ $((!save && upload)) -ne 0 ]]; then
+ out="$(mktemp --suffix ".$filetype" "$(printf 'X%.0s' {1..8})" --tmpdir)"
+ wfRecorderArgs+=(--overwrite)
+ trap "$(trap -P EXIT)"$'\nrm -f "$out"' EXIT
+else
+ out="$(date "${dateArgs[@]}" "+${out/#\~/$HOME}")" \
+ || fatal "date returned non-zero status"
+fi
+wfRecorderArgs+=(-f "$out")
+
+if [[ "$upload" -ne 0 ]]; then
+ case "${apiKey:0:1}" in
+ @|%) apiKey=${apiKey:1};;&
+ @)
+ apiKeyRaw="$(<"${apiKey/#\~/$HOME}")" \
+ || fatal "could not read API key from file: ${apiKey}"
+ ;;
+ %)
+ apiKeyRaw="$(bash -c "${apiKey}")" \
+ || fatal "API key command returned non-zero status: ${apiKey}"
+ ;;
+ *) apiKeyRaw="$apiKey";;
+ esac
+ apiKey="$(tr -d '[:space:]' <<<"$apiKeyRaw")"
+fi
+
+set -eo pipefail
+
+area="$(slurp)"
+wfRecorderArgs+=(--geometry "$area")
+
+mkdir -p "$(dirname "$out")"
+wf-recorder "${wfRecorderArgs[@]}" >/dev/null &
+pid=$!
+trap "$(trap -P EXIT)"$'\nfor pid in $(jobs -p); do kill $pid; done' EXIT
+printf "%d" $pid >"$pidFile" \
+ || fatal "wl-recorder unable to write PID: $pidFile"
+wait $pid \
+ || fatal "wl-recorder returned non-zero status"
+
+if [[ "$upload" -ne 0 ]]; then
+ response="$(curl -sS --fail-with-body \
+ "$endpoint" \
+ -F "image=@$out" \
+ -H "Authorization: Bearer $apiKey")"
+ url="$(jq -r .url <<<"$response")"
+ printf "%s" "$url"
+ if [[ "$copy" -ne 0 ]]; then
+ tr -d '[:space:]' <<<"$url" | wl-copy
+ fi
+elif [[ "$copy" -ne 0 ]]; then
+ printf "%s" "$out" | wl-copy
+fi
+
diff --git a/imsh-shot/default.nix b/imsh-shot/default.nix
@@ -0,0 +1,23 @@
+{
+ writeShellApplication,
+
+ coreutils,
+ curl,
+ grimblast,
+ jq,
+ wl-clipboard,
+ ...
+}:
+writeShellApplication {
+ name = "imsh-shot";
+ runtimeInputs = [
+ coreutils
+ curl
+ grimblast
+ jq
+ wl-clipboard
+ ];
+ bashOptions = [ ];
+ excludeShellChecks = [ "SC2194" ];
+ text = builtins.readFile ./imsh-shot.sh;
+}
diff --git a/imsh-shot/imsh-shot.sh b/imsh-shot/imsh-shot.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+
+argv0="imsh-shot"
+
+usage() {
+cat <<EOF
+$argv0 - Capture and optionally upload a screenshot to an imsh server
+
+Usage: $argv0 target [options...]
+
+Targets:
+ active
+ Currently active window.
+
+ screen
+ All visible outputs.
+
+ output
+ Currently active output.
+
+ area
+ Manually select a region or window.
+
+Options:
+ -u, --upload
+ Uploads the screenshot to imsh.
+
+ -a, --api-key=key|@file|%cmd
+ API key for uploading to imsh.
+ Prefix with "@" to read from a file.
+ Prefix with "%" to run a command.
+
+ -e, --endpoint=url
+ The imsh endpoint to upload to.
+ Defaults to https://scrn.is/api/v1/upload
+
+ -o, --output=path-pattern
+ Path to write captured screenshot to. Gets passed through date and
+ supports the same format strings.
+
+ -U, --utc
+ Call date with -u to use UTC for added privacy when sharing.
+
+ -C, --copy
+ Copy the captured screenshot to the clipboard. If uploading, instead
+ copy the returned URL.
+
+ -f, --freeze
+ Freezes the screen before area selection.
+ Only works when target is area.
+
+ -w, --wait=n
+ Wait for n seconds before taking a screenshot.
+
+ -s, --scale=n
+ Passes the -s argument to grim.
+
+ -c, --cursor
+ Include cursors in the screenshot.
+
+ -t, --filetype=type
+ Output filetype. Supports png, ppm, jpeg.
+ Only png works with the -C option.
+ Default: png
+
+ -h, --help
+ Display this message.
+EOF
+}
+
+fatalUser() {
+cat <<EOF
+Error: $1
+
+See $argv0 --help for more information.
+EOF
+exit 2
+}
+
+fatal() {
+ printf "Error: %s\n" "$1" >&2
+ exit 1
+}
+
+check() {
+ err=()
+ while [[ $# -gt 0 ]]; do
+ msg="$1"; shift
+ cond="$1"; shift
+ value="$1"; shift
+ [[ "$cond" -ne 0 ]] && [[ -z "$value" ]] && err+=("$msg" ", ")
+ done
+ if [[ ${#err[@]} -gt 0 ]]; then
+ IFS=""
+ fatalUser "${err[*]::${#err[@]}-1}" >&2
+ fi
+}
+
+optsWithArgs=(
+ -a --api-key
+ -e --endpoint
+ -o --output
+ -w --wait
+ -s --scale
+ -t --filetype
+)
+
+# Preprocess options:
+# Expand -fff to -f -f -f.
+# Expand -kv to -k v.
+# Expand --key=value to --key value.
+# Stop preprocessing once -- is observed.
+argv=()
+ignoreOpts=
+while [[ $# -gt 0 ]]; do
+ case "$ignoreOpts$1" in
+ --) argv+=("$1"); ignoreOpts=1;;
+ --*=*)
+ argv+=("${1%%=*}" "${1#*=}")
+ (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|${1%%=*}"* ]]) \
+ || fatalUser "invalid option: $1"
+ ;;
+ --*) argv+=("$1");;
+ -*)
+ for ((i=1; i < ${#1}; ++i)); do
+ argv+=("-${1:i:1}");
+ if (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|-${1:i:1}"* ]]) && [[ -n "${1:i+1}" ]]; then
+ argv+=("${1:i+1}")
+ break
+ fi
+ done;;
+ *) argv+=("$1");;
+ esac
+ shift
+done
+set -- "${argv[@]}"
+
+upload=
+apiKey=
+endpoint=https://scrn.is/api/v1/upload
+out=
+save=
+copy=
+filetype=png
+dateArgs=()
+grimblastArgs=()
+
+positional=()
+ignoreOpts=
+while [[ $# -gt 0 ]]; do
+ case "$ignoreOpts$1" in
+ -u|--upload) upload=1;;
+ -a|--api-key) apiKey="$2"; shift;;
+ -e|--endpoint) endpoint="$2"; shift;;
+ -o|--output) out="$2"; save=1; shift;;
+ -U|--utc) dateArgs+=(-u);;
+ -C|--copy) copy=1;;
+ -f|--freeze) grimblastArgs+=(--freeze);;
+ -w|--wait) grimblastArgs+=(--wait "$2"); shift;;
+ -s|--scale) grimblastArgs+=(--scale "$2"); shift;;
+ -c|--cursor) grimblastArgs+=(--cursor);;
+ -t|--filetype) grimblastArgs+=(--filetype "$2"); filetype="$2"; shift;;
+ -h|--help) usage; exit 0;;
+ --) ignoreOpts=1;;
+ -*) fatalUser "invalid option $1";;
+ *) positional+=("$1");;
+ esac
+ shift
+done
+set -- "${positional[@]}"
+
+target="$1"; shift
+action=
+case 1 in
+ $((copy && !upload))) action+="copy";;&
+ $((save || upload))) action+="save";;
+esac
+grimblastArgs+=("$action" "$target")
+
+check \
+ "extraneous positional arguments ($*)" $# ""\
+ "missing target" 1 "$target" \
+ "missing one of --copy --output --upload" 1 "$action" \
+ "missing value for --api-key" "$upload" "$apiKey" \
+ "missing value for --output" "$save" "$out" \
+
+if [[ $((!save && upload)) -ne 0 ]]; then
+ out="$(mktemp --suffix ".$filetype" "$(printf 'X%.0s' {1..8})")"
+ trap "$(trap -P EXIT)"$'\n rm -f "$out"' EXIT
+elif [[ "$save" -ne 0 ]]; then
+ out="$(date "${dateArgs[@]}" "+${out/#\~/$HOME}")" \
+ || fatal "date returned non-zero status"
+fi
+[[ -n "$out" ]] && grimblastArgs+=("$out")
+
+if [[ "$upload" -ne 0 ]]; then
+ case "${apiKey:0:1}" in
+ @|%) apiKey=${apiKey:1};;&
+ @)
+ apiKeyRaw="$(<"${apiKey/#\~/$HOME}")" \
+ || fatal "could not read API key from file: ${apiKey}"
+ ;;
+ %)
+ apiKeyRaw="$(bash -c "${apiKey}")" \
+ || fatal "API key command returned non-zero status: ${apiKey}"
+ ;;
+ *) apiKeyRaw="$apiKey";;
+ esac
+ apiKey="$(tr -d '[:space:]' <<<"$apiKeyRaw")"
+fi
+
+set -eo pipefail
+mkdir -p "$(dirname "$out")"
+grimblast "${grimblastArgs[@]}" >/dev/null
+
+if [[ "$upload" -ne 0 ]]; then
+ response="$(curl -sS --fail-with-body \
+ "$endpoint" \
+ -F "image=@$out" \
+ -H "Authorization: Bearer $apiKey")"
+ url="$(jq -r .url <<<"$response")"
+ printf "%s" "$url"
+ if [[ "$copy" -ne 0 ]]; then
+ tr -d '[:space:]' <<<"$url" | wl-copy
+ fi
+fi
diff --git a/modules/home-manager.nix b/modules/home-manager.nix
@@ -0,0 +1,80 @@
+{
+ lib,
+ config,
+ pkgs,
+ ...
+}:
+let
+ cfg = config.programs.imsh-clients;
+in
+{
+ options.programs.imsh-clients = {
+ enable = lib.mkEnableOption "imsh-clients";
+
+ imsh-shot = {
+ enable = lib.mkEnableOption "imsh-shot";
+ package = lib.mkPackageOption pkgs "imsh-shot" { };
+ };
+
+ imsh-cast = {
+ enable = lib.mkEnableOption "imsh-cast";
+ package = lib.mkPackageOption pkgs "imsh-cast" { };
+ };
+
+ imsh-cast-monitor = {
+ enable = lib.mkEnableOption "imsh-cast-monitor";
+ package = lib.mkPackageOption pkgs "imsh-cast-monitor" { };
+
+ waybar = {
+ enable = lib.mkEnableOption "custom waybar module added to mainBar";
+
+ module = lib.mkOption {
+ type = lib.types.raw;
+ };
+ };
+ };
+ };
+
+ config = {
+ nixpkgs.overlays = lib.singleton (
+ final: prev: {
+ imsh-shot = final.callPackage ../imsh-shot { };
+ imsh-cast = final.callPackage ../imsh-cast { };
+ imsh-cast-monitor = final.callPackage ../imsh-cast-monitor { };
+ }
+ );
+
+ programs.imsh-clients = {
+ imsh-shot.enable = lib.mkDefault cfg.enable;
+ imsh-cast.enable = lib.mkDefault cfg.enable;
+
+ imsh-cast-monitor.waybar.module = {
+ format = "{text}";
+ exec = lib.getExe cfg.imsh-cast-monitor.package;
+ return-type = "json";
+ hide-empty-text = true;
+ };
+ };
+
+ home.packages =
+ (with cfg.imsh-shot; lib.optional enable package)
+ ++ (with cfg.imsh-cast; lib.optional enable package)
+ ++ (with cfg.imsh-cast-monitor; lib.optional enable package);
+
+ programs.waybar = lib.mkIf cfg.imsh-cast-monitor.waybar.enable {
+ settings.mainBar = {
+ modules-center = lib.mkBefore [ "custom/imsh-cast-monitor" ];
+ "custom/imsh-cast-monitor" = cfg.imsh-cast-monitor.waybar.module;
+ };
+ style = ''
+ .imsh-cast-monitor.recording {
+ color: #f00;
+ }
+
+ .imsh-cast-monitor.error {
+ color: #ff0;
+ }
+ '';
+ };
+ };
+}