imsh-clients

Clients for imsh screenshot/screencast sharing service
git clone https://git.echoz.io/imsh-clients.git
Log | Files | Refs

commit 9fb726805a2f9ae34d80ccf8332a09343dbef709
Author: Chris <chris@echoz.io>
Date:   Fri,  3 Oct 2025 15:20:41 +0200

feat: init

Diffstat:
A.editorconfig | 10++++++++++
A.envrc | 1+
A.gitignore | 3+++
Aflake.lock | 26++++++++++++++++++++++++++
Aflake.nix | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aimsh-cast-monitor/default.nix | 18++++++++++++++++++
Aimsh-cast-monitor/imsh-cast-monitor.sh | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aimsh-cast/default.nix | 25+++++++++++++++++++++++++
Aimsh-cast/imsh-cast.sh | 333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aimsh-shot/default.nix | 23+++++++++++++++++++++++
Aimsh-shot/imsh-shot.sh | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home-manager.nix | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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; + } + ''; + }; + }; +}