4 Commits

Author SHA1 Message Date
rootiest 9ff6a58814 feat: XDG Base Directory compliance, --uninstall flag, and xclip advisory
Test PR / test (pull_request) Successful in 7s
Auto Label PRs / label (pull_request) Successful in 3s
- Resolve all paths via $XDG_CONFIG_HOME, $XDG_DATA_HOME, and
  $XDG_RUNTIME_DIR with conventional fallbacks (~/.config,
  ~/.local/share, /tmp); BIN_DIR stays ~/.local/bin (no XDG standard)
- Add --uninstall: removes symlink, desktop entry, and data dir with
  colored per-step output; prompts to remove config when interactive,
  skips silently when not; post-run verification reports any failures
- Warn in --check-deps and --install when only xclip is present —
  xclip operates through XWayland and can be unreliable on Wayland
- Normalize dependency messages to package names (wl-clipboard, xclip)
- Update README: XDG paths in install steps, --uninstall in usage,
  xclip advisory in prerequisites
- Fix test sandbox: export XDG_CONFIG_HOME and XDG_DATA_HOME so the
  overridden HOME is respected; add install (test 15) and uninstall
  (test 16) coverage; add cp and ln to mocked tool set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:15:34 -04:00
rootiest 979c0b7436 docs: link script directly in manual install guide
Link directly to the script rather than to the release for the "Manual
Usage" instructions in the Installation section.
2026-05-04 22:07:09 -04:00
rootiest e5bbc5ae19 Merge pull request 'feat: dependency check system and global refactor' (#12) from feat/dependency-check-and-refactor into main
Reviewed-on: #12
2026-05-05 01:46:26 +00:00
rootiest def2127ed6 feat: add dependency check and refactor global state
Auto Label PRs / label (pull_request) Successful in 2s
Test PR / test (pull_request) Successful in 7s
Release on Merge / release (pull_request) Successful in 5s
- Implement check_deps function and --check-deps flag for environment validation.
- Add truecolor support detection for terminal swatches.
- Colorize --install output and stylized logo.
- Refactor script to use global constants for colors and paths.
- Improve installation logic with robust directory creation.
- Update README.md and --help with new flag and detailed prerequisites.
2026-05-04 21:44:01 -04:00
3 changed files with 468 additions and 185 deletions
+42 -15
View File
@@ -16,30 +16,56 @@ A feature-rich, portable CLI color utility for Linux, specializing in color pick
## 📋 Prerequisites ## 📋 Prerequisites
To utilize all features, ensure the following are installed: ### Required Core Dependencies
- **KDE Plasma + Wayland:** Required for the `--pick` functionality. These are mandatory for the script to run and perform basic color conversions:
- **Python 3 + PyQt6:** Required for the picker helper. - **Python 3:** Handles all color space math and conversions.
- **curl + jq:** Required for color naming support. - **jq:** Powers JSON formatting and internal data parsing.
- **wl-clipboard** (Wayland) or **xclip** (X11): Required for clipboard support.
- **libnotify:** Required for desktop notifications (`notify-send`). ### Optional Feature Dependencies
- **ImageMagick (`magick` or `convert`):** Required for generating visual color swatches in desktop notifications. These enable specific functionality and can be installed as needed:
- **KDE Plasma (Wayland):** Required for screen color picking (`--pick`).
- **Python 3 PyQt6:** Required for the KDE color picker DBus interface.
- **wl-clipboard** or **xclip:** Enables copying results to the system clipboard (`--copy`). `wl-clipboard` is strongly preferred on Wayland — `xclip` operates through XWayland and can be unreliable. `--check-deps` and `--install` will warn you if only `xclip` is found.
- **curl:** Required for fetching human-readable color names from the web (`--name`).
- **libnotify:** Enables desktop notifications (`--notify`).
- **ImageMagick:** Generates visual color swatch icons for notifications (`--swatch`).
- **Truecolor Terminal Emulator:** Required for accurate visual swatches in terminal output (`--swatch`).
> **Tip:** Use `color-tool --check-deps` to quickly see which dependencies are currently met on your system.
## 🚀 Installation ## 🚀 Installation
### Automatic Installation (Recommended)
You can download the script directly from the [latest release](https://git.rootiest.dev/rootiest/color-tool/releases/latest) and install it: You can download the script directly from the [latest release](https://git.rootiest.dev/rootiest/color-tool/releases/latest) and install it:
```bash ```bash
curl -sSLO https://git.rootiest.dev/rootiest/color-tool/releases/download/latest/color-tool # Download and install the latest version of the color-tool script
chmod +x color-tool curl -sSLO https://git.rootiest.dev/rootiest/color-tool/releases/download/latest/color-tool && bash color-tool --install && rm ./color-tool
./color-tool --install
``` ```
This will: This will:
1. Move the script to `~/.local/share/color-tool/`. 1. Install the script to `$XDG_DATA_HOME/color-tool/` (default: `~/.local/share/color-tool/`).
2. Symlink the binary to `~/.local/bin/color-tool`. 2. Symlink the binary to `~/.local/bin/color-tool`.
3. Generate a sample configuration at `~/.config/color-tool/config.toml`. 3. Generate a sample configuration at `$XDG_CONFIG_HOME/color-tool/config.toml` (default: `~/.config/color-tool/config.toml`).
4. Create a `.desktop` entry so you can launch the picker from your application menu. 4. Create a `.desktop` entry in `$XDG_DATA_HOME/applications/` so you can launch the picker from your application menu.
5. Verify and warn about any missing optional or required system dependencies. 5. Verify and warn about any missing optional or required system dependencies.
6. Clean up the downloaded script after installation.
### Manual Usage
Alternatively, you can [download the script](https://git.rootiest.dev/rootiest/color-tool/releases/download/latest/color-tool) manually.
Then run it from any location without installation:
```bash
# Make script executable
chmod +x color-tool
# Run script directly
./color-tool --pick --output hex,rgb
```
However, this will not add the script to your `$PATH` or create a desktop entry,
so you won't be able to launch it from the app menu or use it globally without specifying the path.
## 🛠 Usage ## 🛠 Usage
@@ -58,7 +84,8 @@ Options:
--get-config Print the current configuration --get-config Print the current configuration
--set-config Update the configuration (e.g. --set-config desktop --copy) --set-config Update the configuration (e.g. --set-config desktop --copy)
--reset-config Restore the configuration file to its default values --reset-config Restore the configuration file to its default values
--install Install to ~/.local/share/ and symlink to ~/.local/bin/ --check-deps Check system dependencies and environment support
--install Install to $XDG_DATA_HOME/color-tool/ and symlink to ~/.local/bin/
--help, -h Show this help message --help, -h Show this help message
``` ```
@@ -86,7 +113,7 @@ color-tool --desktop --no-name
## ⚙️ Configuration ## ⚙️ Configuration
You can define your preferred defaults in `~/.config/color-tool/config.toml`. The tool uses a priority hierarchy: **CLI Flags > Desktop Config > Default Config**. You can define your preferred defaults in `$XDG_CONFIG_HOME/color-tool/config.toml` (default: `~/.config/color-tool/config.toml`). The tool uses a priority hierarchy: **CLI Flags > Desktop Config > Default Config**.
```toml ```toml
[defaults] [defaults]
+384 -170
View File
@@ -22,9 +22,39 @@ set -euo pipefail
# Resolve the real directory of this script so we can find bundled helpers # Resolve the real directory of this script so we can find bundled helpers
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
CONFIG_FILE="$HOME/.config/color-tool/config.toml"
# XDG Base Directory Specification — prefer env vars, fall back to conventional defaults
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
CONFIG_FILE="$XDG_CONFIG_HOME/color-tool/config.toml"
REPO_URL="https://git.rootiest.dev/rootiest/color-tool" REPO_URL="https://git.rootiest.dev/rootiest/color-tool"
# Colors and styles
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
CYAN='\033[36m'
YELLOW='\033[33m'
GREEN='\033[32m'
MAG='\033[35m'
RED='\033[31m'
# Gradient logo colors
C1='\033[38;2;255;80;80m' # Red-ish
C2='\033[38;2;255;160;0m' # Orange
C3='\033[38;2;220;210;0m' # Yellow
C4='\033[38;2;80;200;80m' # Green
C5='\033[38;2;80;160;255m' # Blue
# Path constants
DATA_DIR="$XDG_DATA_HOME/color-tool"
BIN_DIR="$HOME/.local/bin"
BIN_PATH="$BIN_DIR/color-tool"
CONFIG_DIR="$XDG_CONFIG_HOME/color-tool"
APP_DIR="$XDG_DATA_HOME/applications"
DESKTOP_FILE="$APP_DIR/color-tool.desktop"
# ── Initial Defaults ────────────────────────────────────────────────────────── # ── Initial Defaults ──────────────────────────────────────────────────────────
# Hardcoded base defaults # Hardcoded base defaults
@@ -64,7 +94,7 @@ get_config() {
echo "Error: Config file not found at $CONFIG_FILE" >&2 echo "Error: Config file not found at $CONFIG_FILE" >&2
exit 1 exit 1
fi fi
local first_section=1 local first_section=1
while IFS= read -r line || [[ -n "$line" ]]; do while IFS= read -r line || [[ -n "$line" ]]; do
local stripped="${line%%#*}" local stripped="${line%%#*}"
@@ -72,7 +102,7 @@ get_config() {
if [[ -z "$stripped" ]]; then if [[ -z "$stripped" ]]; then
continue continue
fi fi
if [[ "$stripped" =~ ^\[([A-Za-z_-]+)\]$ ]]; then if [[ "$stripped" =~ ^\[([A-Za-z_-]+)\]$ ]]; then
if [[ $first_section -eq 0 ]]; then if [[ $first_section -eq 0 ]]; then
echo "" echo ""
@@ -84,7 +114,7 @@ get_config() {
local val="${BASH_REMATCH[2]}" local val="${BASH_REMATCH[2]}"
printf "%-6s = %s\n" "$key" "$val" printf "%-6s = %s\n" "$key" "$val"
fi fi
done < "$CONFIG_FILE" done <"$CONFIG_FILE"
} }
set_config() { set_config() {
@@ -98,7 +128,7 @@ set_config() {
shift shift
fi fi
fi fi
local new_output="" local new_output=""
local new_json="" local new_json=""
local new_swatch="" local new_swatch=""
@@ -110,34 +140,34 @@ set_config() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--output=*) new_output="${1#*=}" ;; --output=*) new_output="${1#*=}" ;;
output=*) new_output="${1#*=}" ;; output=*) new_output="${1#*=}" ;;
--json) new_json="true" ;; --json) new_json="true" ;;
--no-json) new_json="false" ;; --no-json) new_json="false" ;;
--alpha) new_alpha="true" ;; --alpha) new_alpha="true" ;;
--no-alpha) new_alpha="false" ;; --no-alpha) new_alpha="false" ;;
--name) new_name="true" ;; --name) new_name="true" ;;
--no-name) new_name="false" ;; --no-name) new_name="false" ;;
--swatch) new_swatch="true" ;; --swatch) new_swatch="true" ;;
--no-swatch) new_swatch="false" ;; --no-swatch) new_swatch="false" ;;
--copy) new_copy="true" ;; --copy) new_copy="true" ;;
--no-copy) new_copy="false" ;; --no-copy) new_copy="false" ;;
--notify) new_notify="true" ;; --notify) new_notify="true" ;;
--no-notify) new_notify="false" ;; --no-notify) new_notify="false" ;;
--pick) new_pick="true" ;; --pick) new_pick="true" ;;
--no-pick) new_pick="false" ;; --no-pick) new_pick="false" ;;
-*) -*)
echo "Error: Unknown option for --set-config: $1" >&2 echo "Error: Unknown option for --set-config: $1" >&2
exit 1 exit 1
;; ;;
*) *)
echo "Error: Unknown argument for --set-config: $1" >&2 echo "Error: Unknown argument for --set-config: $1" >&2
exit 1 exit 1
;; ;;
esac esac
shift shift
done done
if [[ -n "$new_output" ]]; then if [[ -n "$new_output" ]]; then
new_output="${new_output#\"}" new_output="${new_output#\"}"
new_output="${new_output%\"}" new_output="${new_output%\"}"
@@ -147,91 +177,91 @@ set_config() {
echo "Error: Config file not found at $CONFIG_FILE. Run --install first." >&2 echo "Error: Config file not found at $CONFIG_FILE. Run --install first." >&2
exit 1 exit 1
fi fi
local temp_file local temp_file
temp_file="$(mktemp)" temp_file="$(mktemp)"
local current_section="" local current_section=""
while IFS= read -r line || [[ -n "$line" ]]; do while IFS= read -r line || [[ -n "$line" ]]; do
local original_line="$line" local original_line="$line"
local parsed_line="${line%%#*}" local parsed_line="${line%%#*}"
if [[ "$parsed_line" =~ ^\[([A-Za-z_-]+)\]$ ]]; then if [[ "$parsed_line" =~ ^\[([A-Za-z_-]+)\]$ ]]; then
current_section="${BASH_REMATCH[1]}" current_section="${BASH_REMATCH[1]}"
echo "$original_line" >> "$temp_file" echo "$original_line" >>"$temp_file"
continue continue
fi fi
if [[ "$current_section" == "$target_section" && "$parsed_line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*= ]]; then if [[ "$current_section" == "$target_section" && "$parsed_line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*= ]]; then
local key="${BASH_REMATCH[1]}" local key="${BASH_REMATCH[1]}"
local new_val="" local new_val=""
case "$key" in case "$key" in
output) output)
if [[ -n "$new_output" ]]; then new_val="\"$new_output\""; fi if [[ -n "$new_output" ]]; then new_val="\"$new_output\""; fi
;; ;;
json) json)
if [[ -n "$new_json" ]]; then new_val="$new_json"; fi if [[ -n "$new_json" ]]; then new_val="$new_json"; fi
;; ;;
alpha) alpha)
if [[ -n "$new_alpha" ]]; then new_val="$new_alpha"; fi if [[ -n "$new_alpha" ]]; then new_val="$new_alpha"; fi
;; ;;
swatch) swatch)
if [[ -n "$new_swatch" ]]; then new_val="$new_swatch"; fi if [[ -n "$new_swatch" ]]; then new_val="$new_swatch"; fi
;; ;;
name) name)
if [[ -n "$new_name" ]]; then new_val="$new_name"; fi if [[ -n "$new_name" ]]; then new_val="$new_name"; fi
;; ;;
copy) copy)
if [[ -n "$new_copy" ]]; then new_val="$new_copy"; fi if [[ -n "$new_copy" ]]; then new_val="$new_copy"; fi
;; ;;
pick) pick)
if [[ -n "$new_pick" ]]; then new_val="$new_pick"; fi if [[ -n "$new_pick" ]]; then new_val="$new_pick"; fi
;; ;;
notify) notify)
if [[ -n "$new_notify" ]]; then new_val="$new_notify"; fi if [[ -n "$new_notify" ]]; then new_val="$new_notify"; fi
;; ;;
esac esac
if [[ -n "$new_val" ]]; then if [[ -n "$new_val" ]]; then
if [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)([^[:space:]#]+)(.*)$ ]]; then if [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)([^[:space:]#]+)(.*)$ ]]; then
local prefix="${BASH_REMATCH[1]}" local prefix="${BASH_REMATCH[1]}"
local old_val="${BASH_REMATCH[2]}" local old_val="${BASH_REMATCH[2]}"
local suffix="${BASH_REMATCH[3]}" local suffix="${BASH_REMATCH[3]}"
# Adjust whitespace to keep comments aligned if the value length changed # Adjust whitespace to keep comments aligned if the value length changed
if [[ "$key" != "output" ]]; then if [[ "$key" != "output" ]]; then
local old_len=${#old_val} local old_len=${#old_val}
local new_len=${#new_val} local new_len=${#new_val}
local diff=$((old_len - new_len)) local diff=$((old_len - new_len))
if [[ $diff -gt 0 ]]; then if [[ $diff -gt 0 ]]; then
# new value is shorter, add spaces to suffix # new value is shorter, add spaces to suffix
local spaces="" local spaces=""
for ((i=0; i<diff; i++)); do spaces+=" "; done for ((i = 0; i < diff; i++)); do spaces+=" "; done
suffix="$spaces$suffix" suffix="$spaces$suffix"
elif [[ $diff -lt 0 ]]; then elif [[ $diff -lt 0 ]]; then
# new value is longer, remove spaces from suffix # new value is longer, remove spaces from suffix
local remove=$((-diff)) local remove=$((-diff))
for ((i=0; i<remove; i++)); do for ((i = 0; i < remove; i++)); do
if [[ "$suffix" == " "* ]]; then if [[ "$suffix" == " "* ]]; then
suffix="${suffix# }" suffix="${suffix# }"
fi fi
done done
fi fi
fi fi
echo "$prefix$new_val$suffix" >> "$temp_file" echo "$prefix$new_val$suffix" >>"$temp_file"
continue continue
elif [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)(.*)$ ]]; then elif [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)(.*)$ ]]; then
echo "${BASH_REMATCH[1]}$new_val" >> "$temp_file" echo "${BASH_REMATCH[1]}$new_val" >>"$temp_file"
continue continue
fi fi
fi fi
fi fi
echo "$original_line" >> "$temp_file" echo "$original_line" >>"$temp_file"
done < "$CONFIG_FILE" done <"$CONFIG_FILE"
mv "$temp_file" "$CONFIG_FILE" mv "$temp_file" "$CONFIG_FILE"
} }
@@ -312,43 +342,38 @@ reset_config() {
# ── Help ────────────────────────────────────────────────────────────────────── # ── Help ──────────────────────────────────────────────────────────────────────
show_help() { show_help() {
local bold='\033[1m'
local dim='\033[2m'
local reset='\033[0m'
local cyan='\033[36m'
local yellow='\033[33m'
local mag='\033[35m'
printf "\n" printf "\n"
# Title: "color" in a rainbow gradient, "-tool" in bold # Title: "color" in a rainbow gradient, "-tool" in bold
printf " ${bold}\033[38;2;255;80;80mc\033[38;2;255;160;0mo\033[38;2;220;210;0ml\033[38;2;80;200;80mo\033[38;2;80;160;255mr${reset}${bold}-tool${reset}" printf " ${BOLD}${C1}c${C2}o${C3}l${C4}o${C5}r${RESET}${BOLD}-tool${RESET}"
printf " ${dim}— pick, convert, and format colors${reset}\n\n" printf " ${DIM}— pick, convert, and format colors${RESET}\n\n"
printf " ${bold}${yellow}Usage${reset} ${cyan}${0##*/}${reset} ${dim}[OPTIONS]${reset} [COLOR ...]\n\n" printf " ${BOLD}${YELLOW}Usage${RESET} ${CYAN}${0##*/}${RESET} ${DIM}[OPTIONS]${RESET} [COLOR ...]\n\n"
printf " ${bold}${yellow}Options${reset}\n" printf " ${BOLD}${YELLOW}Options${RESET}\n"
printf " ${bold}${cyan}--[no-]pick${reset} Open the KDE Plasma color picker ${dim}(requires KDE + Wayland)${reset}\n" printf " ${BOLD}${CYAN}--[no-]pick${RESET} Open the KDE Plasma color picker ${DIM}(requires KDE + Wayland)${RESET}\n"
printf " ${bold}${cyan}--output${reset} ${dim}FMT${reset} Format(s) to output: ${cyan}hex, rgb, hsl, rgba, hsla, hexa, all${reset} ${dim}(comma-separated)${reset}\n" printf " ${BOLD}${CYAN}--output${RESET} ${DIM}FMT${RESET} Format(s) to output: ${CYAN}hex, rgb, hsl, rgba, hsla, hexa, all${RESET} ${DIM}(comma-separated)${RESET}\n"
printf " ${bold}${cyan}--[no-]json${reset} Output as a JSON table of selected formats\n" printf " ${BOLD}${CYAN}--[no-]json${RESET} Output as a JSON table of selected formats\n"
printf " ${bold}${cyan}--[no-]name${reset} Fetch nearest color name from thecolorapi.com ${dim}(requires curl, jq)${reset}\n" printf " ${BOLD}${CYAN}--[no-]name${RESET} Fetch nearest color name from thecolorapi.com ${DIM}(requires curl, jq)${RESET}\n"
printf " ${bold}${cyan}--[no-]swatch${reset} Include a color swatch in the terminal output and desktop notification\n" printf " ${BOLD}${CYAN}--[no-]swatch${RESET} Include a color swatch in the terminal output and desktop notification\n"
printf " ${bold}${cyan}--[no-]copy${reset} Copy result to clipboard ${dim}(wl-clipboard preferred, xclip as fallback)${reset}\n" printf " ${BOLD}${CYAN}--[no-]copy${RESET} Copy result to clipboard ${DIM}(wl-clipboard preferred, xclip as fallback)${RESET}\n"
printf " ${bold}${cyan}--[no-]notify${reset} Show desktop notification ${dim}(on by default in --desktop)${reset}\n" printf " ${BOLD}${CYAN}--[no-]notify${RESET} Show desktop notification ${DIM}(on by default in --desktop)${RESET}\n"
printf " ${bold}${cyan}--desktop${reset} GUI mode: pick → copy → notify ${dim}(for app menu / .desktop launcher)${reset}\n" printf " ${BOLD}${CYAN}--desktop${RESET} GUI mode: pick → copy → notify ${DIM}(for app menu / .desktop launcher)${RESET}\n"
printf " ${bold}${cyan}--get-config${reset} Print the current configuration\n" printf " ${BOLD}${CYAN}--get-config${RESET} Print the current configuration\n"
printf " ${bold}${cyan}--set-config${reset} Update the configuration ${dim}(e.g. --set-config desktop --copy)${reset}\n" printf " ${BOLD}${CYAN}--set-config${RESET} Update the configuration ${DIM}(e.g. --set-config desktop --copy)${RESET}\n"
printf " ${bold}${cyan}--reset-config${reset} Restore the configuration file to its default values\n" printf " ${BOLD}${CYAN}--reset-config${RESET} Restore the configuration file to its default values\n"
printf " ${bold}${cyan}--install${reset} Install to ~/.local/share/color-tool/ and symlink into ~/.local/bin/\n" printf " ${BOLD}${CYAN}--check-deps${RESET} Check system dependencies and environment support\n"
printf " ${bold}${cyan}--help${reset}, ${bold}${cyan}-h${reset} Show this help message\n\n" printf " ${BOLD}${CYAN}--install${RESET} Install to %s/ and symlink into %s/\n" "$DATA_DIR" "$BIN_DIR"
printf " ${BOLD}${CYAN}--uninstall${RESET} Remove installed files and optionally the configuration\n"
printf " ${BOLD}${CYAN}--help${RESET}, ${BOLD}${CYAN}-h${RESET} Show this help message\n\n"
printf " ${bold}${yellow}Examples${reset}\n" printf " ${BOLD}${YELLOW}Examples${RESET}\n"
printf " ${cyan}${0##*/}${reset} ${mag}\"#534145\"${reset} --swatch\n" printf " ${CYAN}${0##*/}${RESET} ${MAG}\"#534145\"${RESET} --swatch\n"
printf " ${cyan}${0##*/}${reset} --pick --output rgb,hsl --json\n" printf " ${CYAN}${0##*/}${RESET} --pick --output rgb,hsl --json\n"
printf " ${cyan}${0##*/}${reset} \"rgb(255, 100, 0)\" --output hex,hsla\n\n" printf " ${CYAN}${0##*/}${RESET} \"rgb(255, 100, 0)\" --output hex,hsla\n\n"
printf " ${bold}${yellow}Config${reset} ${dim}~/.config/color-tool/config.toml${reset}\n" printf " ${BOLD}${YELLOW}Config${RESET} ${DIM}%s${RESET}\n" "$CONFIG_FILE"
printf " ${dim}[defaults]${reset} keys: ${cyan}output json swatch name copy pick notify${reset}\n" printf " ${DIM}[defaults]${RESET} keys: ${CYAN}output json swatch name copy pick notify${RESET}\n"
printf " ${dim}[desktop]${reset} keys: ${cyan}output json name notify copy swatch${reset} ${dim}(pick always on)${reset}\n\n" printf " ${DIM}[desktop]${RESET} keys: ${CYAN}output json name notify copy swatch${RESET} ${DIM}(pick always on)${RESET}\n\n"
} }
# ── Environment detection ───────────────────────────────────────────────────── # ── Environment detection ─────────────────────────────────────────────────────
@@ -365,6 +390,26 @@ check_kde_wayland() {
fi fi
} }
check_truecolor() {
# Standard way to check for truecolor support
if [[ "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]]; then
return 0
fi
# Some terminals report color count via tput
if command -v tput >/dev/null 2>&1; then
local colors
colors=$(tput colors 2>/dev/null || echo 0)
if [[ $colors -ge 16777216 ]]; then
return 0
fi
fi
# Check for direct color in TERM
if [[ "${TERM:-}" == *"-direct"* ]]; then
return 0
fi
return 1
}
# ── Clipboard & Notifications ───────────────────────────────────────────────── # ── Clipboard & Notifications ─────────────────────────────────────────────────
copy_to_clipboard() { copy_to_clipboard() {
@@ -387,7 +432,7 @@ notify_result() {
local icon_name="color-picker" local icon_name="color-picker"
if [[ $swatch_mode -eq 1 && -n "$hex_color" ]]; then if [[ $swatch_mode -eq 1 && -n "$hex_color" ]]; then
local swatch_path="/tmp/color_swatch_${hex_color//\#/}.png" local swatch_path="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/color_swatch_${hex_color//\#/}.png"
if command -v magick >/dev/null 2>&1; then if command -v magick >/dev/null 2>&1; then
if magick -size 64x64 xc:"$hex_color" "$swatch_path" 2>/dev/null; then if magick -size 64x64 xc:"$hex_color" "$swatch_path" 2>/dev/null; then
icon_name="$swatch_path" icon_name="$swatch_path"
@@ -479,49 +524,97 @@ run_color_picker() {
suggest_path_add() { suggest_path_add() {
local bin_dir="$1" is_fish=0 local bin_dir="$1" is_fish=0
# Direct check
[[ "${SHELL:-}" == */fish ]] && is_fish=1 [[ "${SHELL:-}" == */fish ]] && is_fish=1
# Parent process check (good for sub-shells)
if [[ $is_fish -eq 0 ]]; then if [[ $is_fish -eq 0 ]]; then
local parent local parent
parent=$(ps -p "$PPID" -o comm= 2>/dev/null || true) parent=$(ps -p "$PPID" -o comm= 2>/dev/null || true)
[[ "$parent" == *fish* ]] && is_fish=1 [[ "$parent" == *fish* ]] && is_fish=1
fi fi
if [[ $is_fish -eq 1 ]]; then if [[ $is_fish -eq 1 ]]; then
printf " Run: fish_add_path %s\n" "$bin_dir" printf " ${CYAN}Run:${RESET} fish_add_path %s\n" "$bin_dir"
elif [[ "${SHELL:-}" == */zsh ]]; then
printf " ${CYAN}Add to ~/.zshrc:${RESET} export PATH=\"%s:\$PATH\"\n" "$bin_dir"
else else
printf " For bash/zsh: export PATH=\"%s:\$PATH\"\n" "$bin_dir" # Fallback for Bash or unknown
printf " For fish: fish_add_path %s\n" "$bin_dir" printf " ${CYAN}Add to ~/.bashrc:${RESET} export PATH=\"%s:\$PATH\"\n" "$bin_dir"
printf " ${DIM}(Or use fish_add_path if using Fish)${RESET}\n"
fi fi
} }
check_deps() {
printf "\n ${BOLD}Dependency Check${RESET}\n\n"
_check() {
local label="$1"
local cmd="$2"
local desc="$3"
local required="${4:-0}"
printf " "
if eval "$cmd"; then
printf "${GREEN}✔${RESET} "
else
if [[ $required -eq 1 ]]; then
printf "${RED}✘${RESET} "
else
printf "${YELLOW}⚠${RESET} "
fi
fi
printf "%-25s ${DIM}%s${RESET}\n" "$label" "$desc"
}
_check "python3" "command -v python3 >/dev/null" "Core logic and color conversions" 1
_check "python3-pyqt6" "python3 -c 'import PyQt6.QtDBus' 2>/dev/null" "KDE color picker interface" 0
_check "jq" "command -v jq >/dev/null" "JSON output and data parsing" 1
_check "curl" "command -v curl >/dev/null" "Fetching color names from API" 0
_check "wl-clipboard / xclip" "command -v wl-copy >/dev/null || command -v xclip >/dev/null" "System clipboard integration" 0
if ! command -v wl-copy >/dev/null 2>&1 && command -v xclip >/dev/null 2>&1; then
printf " ${YELLOW} └─ xclip found but wl-clipboard is missing — xclip operates through XWayland and may be unreliable on Wayland${RESET}\n"
fi
_check "libnotify" "command -v notify-send >/dev/null" "Desktop notifications" 0
_check "ImageMagick" "command -v magick >/dev/null || command -v convert >/dev/null" "Color swatches in notifications" 0
_check "KDE Plasma (Wayland)" "[[ -n \"${WAYLAND_DISPLAY:-}\" ]] && { local d=\"${XDG_CURRENT_DESKTOP:-}\"; [[ \"\$d\" == *KDE* || -n \"${KDE_FULL_SESSION:-}\" ]]; }" "Required for screen color picking" 0
_check "Truecolor Support" "check_truecolor" "Color swatches in terminal output" 0
printf "\n ${DIM}Legend: ${GREEN}✔${DIM} Found ${YELLOW}⚠${DIM} Missing (Optional) ${RED}✘${DIM} Missing (Required)${RESET}\n\n"
}
check_dependencies() { check_dependencies() {
local missing=() local missing=()
if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
missing+=("ImageMagick (magick or convert) — required for notification color swatches") missing+=("ImageMagick (magick or convert) — required for notification color swatches")
fi fi
if ! command -v wl-copy >/dev/null 2>&1 && ! command -v xclip >/dev/null 2>&1; then if ! command -v wl-copy >/dev/null 2>&1 && ! command -v xclip >/dev/null 2>&1; then
missing+=("wl-clipboard (wl-copy) or xclip — required for clipboard copy support") missing+=("wl-clipboard or xclip — required for clipboard copy support")
elif ! command -v wl-copy >/dev/null 2>&1 && command -v xclip >/dev/null 2>&1; then
missing+=("wl-clipboard — xclip is available but operates through XWayland and may be unreliable on Wayland; wl-clipboard is preferred")
fi fi
if ! command -v python3 >/dev/null 2>&1; then if ! command -v python3 >/dev/null 2>&1; then
missing+=("python3 — required for color conversions and picker GUI") missing+=("python3 — required for color conversions and picker GUI")
elif ! python3 -c "import PyQt6.QtDBus" >/dev/null 2>&1; then elif ! python3 -c "import PyQt6.QtDBus" >/dev/null 2>&1; then
missing+=("python3-pyqt6 — required for the KDE color picker DBus interface") missing+=("python3-pyqt6 — required for the KDE color picker DBus interface")
fi fi
if ! command -v jq >/dev/null 2>&1; then if ! command -v jq >/dev/null 2>&1; then
missing+=("jq — required for JSON formatting and color names") missing+=("jq — required for JSON formatting and color names")
fi fi
if ! command -v curl >/dev/null 2>&1; then if ! command -v curl >/dev/null 2>&1; then
missing+=("curl — required for fetching color names from the API") missing+=("curl — required for fetching color names from the API")
fi fi
if ! command -v notify-send >/dev/null 2>&1; then if ! command -v notify-send >/dev/null 2>&1; then
missing+=("libnotify (notify-send) — required for desktop notifications") missing+=("libnotify (notify-send) — required for desktop notifications")
fi fi
local env_ok=1 local env_ok=1
if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then
env_ok=0 env_ok=0
@@ -530,76 +623,189 @@ check_dependencies() {
if [[ "$desktop" != *KDE* ]] && [[ -z "${KDE_FULL_SESSION:-}" ]]; then if [[ "$desktop" != *KDE* ]] && [[ -z "${KDE_FULL_SESSION:-}" ]]; then
env_ok=0 env_ok=0
fi fi
if [[ $env_ok -eq 0 ]]; then if [[ $env_ok -eq 0 ]]; then
missing+=("KDE Plasma on Wayland — required for the screen color picker feature") missing+=("KDE Plasma on Wayland — required for the screen color picker feature")
fi fi
if ! check_truecolor; then
missing+=("Truecolor support not detected — the --swatch feature in the terminal may not work as intended")
fi
if [[ ${#missing[@]} -gt 0 ]]; then if [[ ${#missing[@]} -gt 0 ]]; then
local yellow='\033[33m' printf "\n ${BOLD}${YELLOW}Warnings — Missing optional dependencies:${RESET}\n"
local bold='\033[1m'
local reset='\033[0m'
printf "\n ${bold}${yellow}Warnings — Missing optional dependencies:${reset}\n"
for item in "${missing[@]}"; do for item in "${missing[@]}"; do
printf " - %s\n" "$item" printf " - %s\n" "$item"
done done
printf " ${yellow}Functionality will be limited without these installed.${reset}\n" printf " ${YELLOW}Functionality will be limited without these installed.${RESET}\n"
fi fi
} }
do_install() { do_install() {
local data_dir="$HOME/.local/share/color-tool"
local bin_dir="$HOME/.local/bin"
local bin_link="$bin_dir/color-tool"
local src_script local src_script
src_script="$(readlink -f "$0")" src_script="$(readlink -f "$0")"
mkdir -p "$data_dir" "$bin_dir" printf "\n ${BOLD}Installing ${C1}c${C2}o${C3}l${C4}o${C5}r${RESET}${BOLD}-tool...${RESET}\n\n"
cp "$src_script" "$data_dir/color-tool"
chmod +x "$data_dir/color-tool"
generate_picker_script "$data_dir/wl-colorpicker-plasma.py"
ln -sf "$data_dir/color-tool" "$bin_link"
local config_dir="$HOME/.config/color-tool" # Efficient Directory Creation Loop
local config_file="$config_dir/config.toml" local dirs=("$BIN_DIR" "$DATA_DIR" "$CONFIG_DIR" "$APP_DIR")
if [[ ! -f "$config_file" ]]; then for dir in "${dirs[@]}"; do
if [[ ! -d "$dir" ]]; then
printf " ${DIM}Creating %s...${RESET}\n" "$dir"
mkdir -p "$dir"
fi
done
# Core installation steps
cp "$src_script" "$DATA_DIR/color-tool"
chmod +x "$DATA_DIR/color-tool"
generate_picker_script "$DATA_DIR/wl-colorpicker-plasma.py"
ln -sf "$DATA_DIR/color-tool" "$BIN_PATH"
# Config setup
if [[ ! -f "$CONFIG_FILE" ]]; then
write_default_config write_default_config
printf " config %s (sample created)\n" "$config_file" printf " ${BOLD}${GREEN}config${RESET} %s ${DIM}(sample created)${RESET}\n" "$CONFIG_FILE"
else else
printf " config %s\n" "$config_file" printf " ${BOLD}${GREEN}config${RESET} %s\n" "$CONFIG_FILE"
fi fi
local app_dir="$HOME/.local/share/applications" # Desktop Entry (using consolidated BIN_PATH)
local desktop_file="$app_dir/color-tool.desktop" cat >"$DESKTOP_FILE" <<EOF
local bin_path="$HOME/.local/bin/color-tool"
mkdir -p "$app_dir"
cat >"$desktop_file" <<EOF
[Desktop Entry] [Desktop Entry]
Version=1.1 Version=1.1
Type=Application Type=Application
Name=Color Tool Name=Color Tool
Comment=Pick a color and copy it to the clipboard Comment=Pick a color and copy it to the clipboard
Exec=$bin_path --desktop Exec=$BIN_PATH --desktop
Icon=color-picker Icon=color-picker
Categories=Utility;Graphics; Categories=Utility;Graphics;
Keywords=color;picker;hex;rgb;clipboard; Keywords=color;picker;hex;rgb;clipboard;
Terminal=false Terminal=false
NoDisplay=false NoDisplay=false
EOF EOF
printf " app %s\n" "$desktop_file"
printf "Installed:\n" # Summary output
printf " data %s/\n" "$data_dir" printf " ${BOLD}${GREEN}app${RESET} %s\n" "$DESKTOP_FILE"
printf " bin %s -> %s\n" "$bin_link" "$data_dir/color-tool" printf " ${BOLD}${GREEN}data${RESET} %s/\n" "$DATA_DIR"
printf " ${BOLD}${GREEN}bin${RESET} %s -> %s\n" "$BIN_PATH" "$DATA_DIR/color-tool"
if [[ ":$PATH:" != *":$bin_dir:"* ]]; then # Path verification
printf "\nNote: %s is not in your PATH.\n" "$bin_dir" if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
suggest_path_add "$bin_dir" printf "\n${BOLD}${YELLOW}Warning:${RESET} %s is not in your PATH.\n" "$BIN_DIR"
suggest_path_add "$BIN_DIR"
fi fi
check_dependencies check_dependencies
} }
do_uninstall() {
printf "\n ${BOLD}Uninstalling ${C1}c${C2}o${C3}l${C4}o${C5}r${RESET}${BOLD}-tool...${RESET}\n\n"
local errors=0
local remove_config=0
# 1. Binary symlink
printf " ${DIM}Removing binary symlink...${RESET}\n"
if [[ -L "$BIN_PATH" ]]; then
if rm "$BIN_PATH"; then
printf " ${BOLD}${GREEN}bin${RESET} Removed %s\n" "$BIN_PATH"
else
printf " ${BOLD}${RED}bin${RESET} Failed to remove %s\n" "$BIN_PATH" >&2
errors=$((errors + 1))
fi
elif [[ -e "$BIN_PATH" ]]; then
printf " ${BOLD}${YELLOW}bin${RESET} %s is not a symlink — skipping\n" "$BIN_PATH"
else
printf " ${DIM}bin${RESET} %s not found — skipping\n" "$BIN_PATH"
fi
# 2. Desktop entry
printf " ${DIM}Removing desktop entry...${RESET}\n"
if [[ -f "$DESKTOP_FILE" ]]; then
if rm "$DESKTOP_FILE"; then
printf " ${BOLD}${GREEN}app${RESET} Removed %s\n" "$DESKTOP_FILE"
else
printf " ${BOLD}${RED}app${RESET} Failed to remove %s\n" "$DESKTOP_FILE" >&2
errors=$((errors + 1))
fi
else
printf " ${DIM}app${RESET} %s not found — skipping\n" "$DESKTOP_FILE"
fi
# 3. Data directory
printf " ${DIM}Removing data directory...${RESET}\n"
if [[ -d "$DATA_DIR" ]]; then
if rm -rf "$DATA_DIR"; then
printf " ${BOLD}${GREEN}data${RESET} Removed %s/\n" "$DATA_DIR"
else
printf " ${BOLD}${RED}data${RESET} Failed to remove %s/\n" "$DATA_DIR" >&2
errors=$((errors + 1))
fi
else
printf " ${DIM}data${RESET} %s not found — skipping\n" "$DATA_DIR"
fi
# 4. Config directory — prompt if interactive, skip silently if not
if [[ -d "$CONFIG_DIR" ]]; then
if [[ -t 0 ]]; then
printf "\n ${BOLD}${YELLOW}?${RESET} Remove configuration directory?\n"
printf " ${DIM}%s/${RESET}\n" "$CONFIG_DIR"
printf " ${BOLD}[y/N]${RESET} "
local answer
read -r answer
if [[ "${answer,,}" == "y" || "${answer,,}" == "yes" ]]; then
remove_config=1
else
printf " ${BOLD}${DIM}config${RESET} %s/ kept\n" "$CONFIG_DIR"
fi
else
printf " ${DIM}config${RESET} Non-interactive mode — keeping %s/\n" "$CONFIG_DIR"
fi
fi
if [[ $remove_config -eq 1 ]]; then
printf " ${DIM}Removing configuration directory...${RESET}\n"
if rm -rf "$CONFIG_DIR"; then
printf " ${BOLD}${GREEN}config${RESET} Removed %s/\n" "$CONFIG_DIR"
else
printf " ${BOLD}${RED}config${RESET} Failed to remove %s/\n" "$CONFIG_DIR" >&2
errors=$((errors + 1))
fi
fi
# Verification
printf "\n ${BOLD}Verification${RESET}\n\n"
local verify_errors=0
local labels=("bin" "app" "data")
local paths=("$BIN_PATH" "$DESKTOP_FILE" "$DATA_DIR")
if [[ $remove_config -eq 1 ]]; then
labels+=("config")
paths+=("$CONFIG_DIR")
fi
local i
for i in "${!labels[@]}"; do
local label="${labels[$i]}"
local path="${paths[$i]}"
printf " "
if [[ ! -e "$path" && ! -L "$path" ]]; then
printf "${GREEN}✔${RESET} %-8s %s\n" "$label" "$path"
else
printf "${RED}✘${RESET} %-8s %s ${RED}(still present)${RESET}\n" "$label" "$path"
verify_errors=$((verify_errors + 1))
fi
done
printf "\n"
if [[ $((errors + verify_errors)) -eq 0 ]]; then
printf " ${BOLD}${GREEN}color-tool has been successfully uninstalled.${RESET}\n\n"
else
printf " ${BOLD}${RED}Uninstall completed with errors.${RESET} Some files may need to be removed manually.\n\n" >&2
return 1
fi
}
# ── Color conversion ────────────────────────────────────────────────────────── # ── Color conversion ──────────────────────────────────────────────────────────
get_all_formats() { get_all_formats() {
@@ -787,6 +993,10 @@ while [[ $# -gt 0 ]]; do
do_install do_install
exit 0 exit 0
;; ;;
--uninstall)
do_uninstall
exit 0
;;
--get-config) --get-config)
get_config get_config
exit 0 exit 0
@@ -800,6 +1010,10 @@ while [[ $# -gt 0 ]]; do
reset_config reset_config
exit 0 exit 0
;; ;;
--check-deps)
check_deps
exit 0
;;
-*) -*)
echo "Unknown option: $1" >&2 echo "Unknown option: $1" >&2
exit 1 exit 1
+42
View File
@@ -59,9 +59,13 @@ MOCK
chmod +x "$BIN_DIR/curl" chmod +x "$BIN_DIR/curl"
ln -s /usr/bin/mv "$BIN_DIR/mv" ln -s /usr/bin/mv "$BIN_DIR/mv"
ln -s /usr/bin/tr "$BIN_DIR/tr" ln -s /usr/bin/tr "$BIN_DIR/tr"
ln -s /usr/bin/cp "$BIN_DIR/cp"
ln -s /usr/bin/ln "$BIN_DIR/ln"
export PATH="$BIN_DIR" export PATH="$BIN_DIR"
export HOME="$TEST_DIR" export HOME="$TEST_DIR"
export XDG_CONFIG_HOME="$TEST_DIR/.config"
export XDG_DATA_HOME="$TEST_DIR/.local/share"
export WAYLAND_DISPLAY=wayland-0 export WAYLAND_DISPLAY=wayland-0
export XDG_CURRENT_DESKTOP=KDE export XDG_CURRENT_DESKTOP=KDE
@@ -232,6 +236,44 @@ else
echo " Actual output: $output" echo " Actual output: $output"
fi fi
# 15. Install — files placed in correct XDG directories
it "installs files to XDG directories"
# Clear any artifacts from earlier tests before verifying a clean install
rm -rf "$XDG_DATA_HOME/color-tool" \
"$XDG_DATA_HOME/applications/color-tool.desktop" \
"$HOME/.local/bin" 2>/dev/null || true
install_out=$("$COLOR_TOOL" --install 2>&1)
if [[ -f "$XDG_DATA_HOME/color-tool/color-tool" ]] &&
[[ -f "$XDG_DATA_HOME/color-tool/wl-colorpicker-plasma.py" ]] &&
[[ -L "$HOME/.local/bin/color-tool" ]] &&
[[ -f "$XDG_DATA_HOME/applications/color-tool.desktop" ]] &&
[[ -f "$XDG_CONFIG_HOME/color-tool/config.toml" ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Install output: $install_out"
echo " Files found:"
/usr/bin/find "$TEST_DIR/.local" "$TEST_DIR/.config" \( -type f -o -type l \) 2>/dev/null | /usr/bin/sort || true
fi
# 16. Uninstall — removes installed files, config is kept in non-interactive mode
it "uninstalls installed files (keeps config)"
uninstall_out=$("$COLOR_TOOL" --uninstall 2>&1)
if [[ ! -e "$HOME/.local/bin/color-tool" ]] &&
[[ ! -L "$HOME/.local/bin/color-tool" ]] &&
[[ ! -d "$XDG_DATA_HOME/color-tool" ]] &&
[[ ! -f "$XDG_DATA_HOME/applications/color-tool.desktop" ]] &&
[[ -f "$XDG_CONFIG_HOME/color-tool/config.toml" ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Uninstall output: $uninstall_out"
echo " Files still present:"
/usr/bin/find "$TEST_DIR/.local" "$TEST_DIR/.config" \( -type f -o -type l \) 2>/dev/null | /usr/bin/sort || true
fi
# ── Cleanup ─────────────────────────────────────────────────────────────────── # ── Cleanup ───────────────────────────────────────────────────────────────────
echo "---------------------------------------" echo "---------------------------------------"
echo "Result: $passed/$total tests passed." echo "Result: $passed/$total tests passed."