diff --git a/bin/omarchy b/bin/omarchy index 511aeb97..bca7a306 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -55,6 +55,7 @@ GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows" GROUP_DESCRIPTIONS[remove]="Removal workflows" GROUP_DESCRIPTIONS[restart]="Restart Omarchy components" GROUP_DESCRIPTIONS[setup]="Interactive setup wizards" +GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation" GROUP_DESCRIPTIONS[snapshot]="System snapshots" GROUP_DESCRIPTIONS[state]="Persistent Omarchy state" GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers" diff --git a/bin/omarchy-menu b/bin/omarchy-menu index df1c6eaf..891ff1da 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -217,12 +217,36 @@ show_style_menu() { *Font*) show_font_menu ;; *Background*) show_background_menu ;; *Hyprland*) open_in_editor ~/.config/hypr/looknfeel.conf ;; - *Screensaver*) open_in_editor ~/.config/omarchy/branding/screensaver.txt ;; + *Screensaver*) show_screensaver_menu ;; *About*) open_in_editor ~/.config/omarchy/branding/about.txt ;; *) show_main_menu ;; esac } +show_screensaver_menu() { + case $(menu "Screensaver" " Set From Image\n Restore Default\n Edit Text\n󱄄 Preview") in + *Image*) set_screensaver_from_image ;; + *Default*) terminal bash -lc 'omarchy-screensaver-set-logo default; echo; read -n 1 -s -r -p "Press any key to close..."' ;; + *Text*) open_in_editor ~/.config/omarchy/branding/screensaver.txt ;; + *Preview*) omarchy-launch-screensaver force ;; + *) show_style_menu ;; + esac +} + +set_screensaver_from_image() { + terminal bash -lc ' + image=$(fd --type file --ignore-case --extension svg --extension png . "$HOME" 2>/dev/null | fzf --prompt "Logo image> ") + if [[ -n $image ]]; then + if omarchy-screensaver-set-logo "$image"; then + echo + echo "Preview with: omarchy-launch-screensaver force" + fi + echo + read -n 1 -s -r -p "Press any key to close..." + fi + ' +} + show_theme_menu() { omarchy-launch-walker -m menus:omarchythemes --width 800 --minheight 400 } diff --git a/bin/omarchy-screensaver-set-logo b/bin/omarchy-screensaver-set-logo new file mode 100755 index 00000000..d3f21899 --- /dev/null +++ b/bin/omarchy-screensaver-set-logo @@ -0,0 +1,384 @@ +#!/bin/bash + +# omarchy:summary=Set the screensaver logo text from an SVG/PNG or the default +# omarchy:args= [--width ] [--height ] [--mode ] [--threshold ] [--invert] [--stdout] +# omarchy:examples=omarchy screensaver set logo ~/logo.svg | omarchy screensaver set logo default | omarchy screensaver set logo ~/logo.png --width 80 --mode block --stdout + +set -o pipefail + +usage() { + cat <<'EOF' +Usage: omarchy-screensaver-set-logo [options] + +Sets the screensaver logo text from an image. +Pass "default" instead of an image path to restore the Omarchy default. +By default, writes to ~/.config/omarchy/branding/screensaver.txt. + +Options: + -w, --width Maximum output width in terminal columns (default: 80) + -H, --height Maximum output height in terminal rows (default: 26) + -m, --mode Output style (default: braille) + -t, --threshold Pixel threshold from 0-100 (default: 50) + --invert Treat light pixels as the logo instead of dark pixels + --no-trim Preserve surrounding whitespace/background + --stdout Print converted text instead of writing the screensaver file + --help Show this help +EOF +} + +width=80 +height=26 +mode=braille +threshold=50 +invert=false +trim=true +output_path="$HOME/.config/omarchy/branding/screensaver.txt" +image_path="" +reset_default=false + +while (($# > 0)); do + case "$1" in + --help) + usage + exit 0 + ;; + -w | --width) + shift + if [[ -z $1 ]]; then + echo "Missing value for --width" >&2 + exit 1 + fi + width="$1" + ;; + --width=*) + width="${1#*=}" + ;; + -H | --height) + shift + if [[ -z $1 ]]; then + echo "Missing value for --height" >&2 + exit 1 + fi + height="$1" + ;; + --height=*) + height="${1#*=}" + ;; + -m | --mode) + shift + if [[ -z $1 ]]; then + echo "Missing value for --mode" >&2 + exit 1 + fi + mode="$1" + ;; + --mode=*) + mode="${1#*=}" + ;; + --block | --blocks) + mode=block + ;; + --braille) + mode=braille + ;; + -t | --threshold) + shift + if [[ -z $1 ]]; then + echo "Missing value for --threshold" >&2 + exit 1 + fi + threshold="$1" + ;; + --threshold=*) + threshold="${1#*=}" + ;; + --invert) + invert=true + ;; + --no-trim) + trim=false + ;; + --stdout) + output_path="-" + ;; + --) + shift + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + if [[ $1 == "default" && -z $image_path ]]; then + reset_default=true + elif [[ -z $image_path ]]; then + image_path="$1" + else + echo "Unexpected argument: $1" >&2 + usage >&2 + exit 1 + fi + ;; + esac + shift +done + +if (($# > 0)); then + while (($# > 0)); do + if [[ $1 == "default" && -z $image_path ]]; then + reset_default=true + elif [[ -z $image_path ]]; then + image_path="$1" + else + echo "Unexpected argument: $1" >&2 + usage >&2 + exit 1 + fi + shift + done +fi + +if [[ $reset_default == "true" ]]; then + if [[ -n $image_path ]]; then + echo "default does not accept an image path" >&2 + exit 1 + fi + + default_path="${OMARCHY_PATH:-$HOME/.local/share/omarchy}/logo.txt" + if [[ ! -f $default_path ]]; then + echo "Default screensaver logo text not found: $default_path" >&2 + exit 1 + fi + + if [[ $output_path == "-" ]]; then + cat "$default_path" + else + mkdir -p "$(dirname "$output_path")" + cp "$default_path" "$output_path" + echo "Restored default screensaver logo text to $output_path" + fi + exit 0 +fi + +if [[ -z $image_path ]]; then + usage >&2 + exit 1 +fi + +if ! [[ $width =~ ^[0-9]+$ ]] || (( width < 1 )); then + echo "Invalid width: $width" >&2 + exit 1 +fi + +if ! [[ $height =~ ^[0-9]+$ ]] || (( height < 1 )); then + echo "Invalid height: $height" >&2 + exit 1 +fi + +if [[ $mode != "braille" && $mode != "block" ]]; then + echo "Invalid mode: $mode (expected braille or block)" >&2 + exit 1 +fi + +if ! [[ $threshold =~ ^[0-9]+$ ]] || (( threshold < 0 || threshold > 100 )); then + echo "Invalid threshold: $threshold (expected 0-100)" >&2 + exit 1 +fi + +if [[ ! -f $image_path ]]; then + echo "Logo file not found: $image_path" >&2 + exit 1 +fi + +if omarchy-cmd-missing magick; then + echo "ImageMagick is required to convert logo images" >&2 + exit 1 +fi + +case "$mode" in +braille) + pixel_width=$((width * 2)) + pixel_height=$((height * 4)) + ;; +block) + pixel_width=$width + pixel_height=$((height * 2)) + ;; +esac + +alpha_min=$(magick -background none "$image_path" -alpha extract -format '%[fx:minima]' info: 2>/dev/null) || { + echo "Unable to read logo image: $image_path" >&2 + exit 1 +} +alpha_max=$(magick -background none "$image_path" -alpha extract -format '%[fx:maxima]' info: 2>/dev/null) || { + echo "Unable to read logo image: $image_path" >&2 + exit 1 +} + +use_alpha=false +if awk -v min="$alpha_min" -v max="$alpha_max" 'BEGIN { exit !(min < 0.999 && max > min) }'; then + use_alpha=true +fi + +magick_args=(-background none "$image_path" -auto-orient) + +if [[ $use_alpha == "true" ]]; then + magick_args+=(-alpha extract -alpha off) + if [[ $invert == "true" ]]; then + magick_args+=(-negate) + fi +else + magick_args+=(-alpha remove -alpha off -colorspace Gray) + if [[ $invert == "false" ]]; then + magick_args+=(-negate) + fi +fi + +if [[ $trim == "true" ]]; then + magick_args+=(-bordercolor black -border 1 -trim +repage) +fi + +magick_args+=(-resize "${pixel_width}x${pixel_height}" -threshold "${threshold}%" -negate -compress none pbm:-) + +tmp_output=$(mktemp) +trap 'rm -f "$tmp_output"' EXIT + +if [[ $mode == "braille" ]]; then + if ! magick "${magick_args[@]}" | awk ' + BEGIN { + braille_base = 10240 + dot[0,0] = 1 + dot[0,1] = 2 + dot[0,2] = 4 + dot[1,0] = 8 + dot[1,1] = 16 + dot[1,2] = 32 + dot[0,3] = 64 + dot[1,3] = 128 + } + { + sub(/#.*/, "") + for (i = 1; i <= NF; i++) { + token[++token_count] = $i + } + } + END { + if (token[1] != "P1") { + exit 1 + } + + width = token[2] + height = token[3] + pixel_offset = 4 + + for (y = 0; y < height; y += 4) { + line = "" + for (x = 0; x < width; x += 2) { + code = 0 + for (dy = 0; dy < 4; dy++) { + for (dx = 0; dx < 2; dx++) { + if (y + dy < height && x + dx < width) { + pixel = token[pixel_offset + ((y + dy) * width) + x + dx] + if (pixel == 1) { + code += dot[dx,dy] + } + } + } + } + + if (code == 0) { + line = line " " + } else { + line = line sprintf("%c", braille_base + code) + } + } + sub(/[ ]+$/, "", line) + lines[++line_count] = line + } + + first = 1 + while (first <= line_count && lines[first] ~ /^[ ]*$/) { + first++ + } + + last = line_count + while (last >= first && lines[last] ~ /^[ ]*$/) { + last-- + } + + for (i = first; i <= last; i++) { + print lines[i] + } + } + ' >"$tmp_output"; then + echo "Unable to convert logo image: $image_path" >&2 + exit 1 + fi +else + if ! magick "${magick_args[@]}" | awk ' + BEGIN { + block["11"] = "█" + block["10"] = "▀" + block["01"] = "▄" + block["00"] = " " + } + { + sub(/#.*/, "") + for (i = 1; i <= NF; i++) { + token[++token_count] = $i + } + } + END { + if (token[1] != "P1") { + exit 1 + } + + width = token[2] + height = token[3] + pixel_offset = 4 + + for (y = 0; y < height; y += 2) { + line = "" + for (x = 0; x < width; x++) { + top = token[pixel_offset + (y * width) + x] + bottom = (y + 1 < height) ? token[pixel_offset + ((y + 1) * width) + x] : 0 + line = line block[top bottom] + } + sub(/[ ]+$/, "", line) + lines[++line_count] = line + } + + first = 1 + while (first <= line_count && lines[first] ~ /^[ ]*$/) { + first++ + } + + last = line_count + while (last >= first && lines[last] ~ /^[ ]*$/) { + last-- + } + + for (i = first; i <= last; i++) { + print lines[i] + } + } + ' >"$tmp_output"; then + echo "Unable to convert logo image: $image_path" >&2 + exit 1 + fi +fi + +if [[ ! -s $tmp_output ]]; then + echo "No logo pixels found in: $image_path" >&2 + exit 1 +fi + +if [[ $output_path == "-" ]]; then + cat "$tmp_output" +else + mkdir -p "$(dirname "$output_path")" + cp "$tmp_output" "$output_path" + echo "Wrote screensaver logo text to $output_path" +fi