27 Commits

Author SHA1 Message Date
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
rootiest 2b58cc8a13 Merge pull request 'feat(config): enable desktop swatch and improve comment alignment' (#11) from feat/desktop-swatch-and-config-alignment into main
Reviewed-on: #11
2026-05-03 18:58:02 +00:00
rootiest e0967a8388 feat(config): maintain comment alignment in set_config
Auto Label PRs / label (pull_request) Successful in 3s
Test PR / test (pull_request) Successful in 6s
Release on Merge / release (pull_request) Successful in 6s
Updated set_config to dynamically adjust leading whitespace of trailing
comments when toggling between true/false (or any values of different
lengths). This ensures that inline comments in config.toml remain
vertically aligned after programmatic updates.
2026-05-03 14:57:02 -04:00
rootiest 8d94bc4006 feat(config): enable swatch by default for desktop mode
Set swatch = true as the default in the stock [desktop] configuration
and internal defaults. This ensures that desktop notifications include
a visual color swatch by default. Updated README and tests accordingly.
2026-05-03 14:56:31 -04:00
rootiest 10014d5ee9 Merge pull request 'fix(help): clarify --swatch applies to terminal output and desktop notifications' (#10) from fix/swatch-help-text into main
Reviewed-on: #10
2026-05-03 18:30:19 +00:00
rootiest 8333d249d9 fix(help): clarify --swatch applies to terminal and notifications
Auto Label PRs / label (pull_request) Successful in 3s
Test PR / test (pull_request) Successful in 6s
Release on Merge / release (pull_request) Successful in 6s
The --swatch flag now controls both the inline terminal color block and
the desktop notification icon (via ImageMagick). Update the help text
to reflect this so users know the flag affects both output contexts.
2026-05-03 14:29:30 -04:00
rootiest 383d547a32 Merge pull request 'docs: update curl download instructions from latest release' (#9) from docs/update-download-url into main
Reviewed-on: #9
2026-05-03 18:22:54 +00:00
rootiest 1d92dc911e docs: add curl download instructions from latest release
Auto Label PRs / label (pull_request) Successful in 3s
Test PR / test (pull_request) Successful in 6s
Release on Merge / release (pull_request) Has been skipped
2026-05-03 14:20:29 -04:00
rootiest 24747230e1 Merge pull request 'feat: implement notification color swatch and dependency verification' (#8) from feat/notify-swatch into main
Reviewed-on: #8
2026-05-03 18:09:54 +00:00
rootiest 867b8ccf26 docs: update README with swatch and dependency checks
Test PR / test (pull_request) Successful in 7s
Release on Merge / release (pull_request) Successful in 6s
2026-05-03 14:08:53 -04:00
rootiest 7c882bb26c feat: implement notification color swatch and dependency verification
Auto Label PRs / label (pull_request) Successful in 3s
Test PR / test (pull_request) Successful in 6s
2026-05-03 14:05:11 -04:00
rootiest f2be6c151e Merge pull request 'ci: add workflow to automatically label PRs based on title' (#7) from ci/auto-label-prs into main 2026-05-03 03:38:42 +00:00
rootiest b78becad5f ci: add workflow to automatically label PRs based on title
Auto Label PRs / label (pull_request) Successful in 3s
Test PR / test (pull_request) Successful in 5s
Release on Merge / release (pull_request) Has been skipped
2026-05-02 23:38:21 -04:00
rootiest 48b436b20a Merge pull request 'feat: implement config management flags and documentation' (#6) from feat/manage-config into main 2026-05-03 03:33:43 +00:00
rootiest 616aa6f2f1 docs, test: add config management documentation and tests
Test PR / test (pull_request) Successful in 6s
Release on Merge / release (pull_request) Successful in 6s
2026-05-02 23:33:23 -04:00
rootiest 1129563000 Merge pull request 'ci/test: add gitea workflow and comprehensive test coverage' (#5) from ci/test-workflow into main 2026-05-03 03:30:59 +00:00
rootiest 9ab65a6b1f ci/test: add gitea workflow and comprehensive test coverage
Test PR / test (pull_request) Successful in 6s
Release on Merge / release (pull_request) Successful in 5s
2026-05-02 23:30:43 -04:00
rootiest 67596389ec Merge pull request 'feat: implement config management flags' (#4) from feat/manage-config into main
Reviewed-on: #4
2026-05-02 03:13:52 +00:00
rootiest 456071f2da feat: implement config management flags
Release on Merge / release (pull_request) Successful in 1m9s
- Add `--get-config` to print the current configuration.

- Add `--set-config` to update specific configuration keys in the TOML file.

- Add `--reset-config` to restore the default configuration.

- Refactor `do_install` to use the new `write_default_config` function.

- Update `--help` text to document the new configuration management flags.
2026-05-01 23:13:06 -04:00
rootiest 100fecda18 Merge pull request 'ci/test: add test suite and release label exclusions' (#3) from ci/tests into main
Reviewed-on: #3
2026-04-29 02:44:37 +00:00
rootiest 078118f689 Merge branch 'ci/label-exclusions' into ci/tests
Release on Merge / release (pull_request) Has been skipped
2026-04-28 22:41:39 -04:00
rootiest 0d7b93cab7 ci: skip releases for testing or documentation PRs
Add a condition to the release workflow to skip execution if the PR
is labeled with 'Kind/Testing' or 'Kind/Documentation'. This prevents
unnecessary releases for non-functional changes.
2026-04-28 22:39:59 -04:00
rootiest 37a5497336 test: add comprehensive bash test suite
- Implement tests/run_tests.sh with mocked environment
- Cover color conversion, multi-format output, and config loading
- Verify clipboard and notification integrations via mocks
- Document testing procedure in README.md
2026-04-28 22:31:57 -04:00
rootiest 344fc140dc Merge pull request 'fix: notify when clipboard utility is missing' (#2) from fix/copy-fail into main
Reviewed-on: #2
2026-04-29 02:18:26 +00:00
rootiest 5f659b90e3 fix: notify when clipboard utility is missing
Release on Merge / release (pull_request) Successful in 6s
- Add notify_error function to send normal urgency warning notifications
- Update process_color to track clipboard failure and trigger error notification
- Improve warning message when clipboard utilities (wl-copy/xclip) are absent
2026-04-28 22:17:31 -04:00
rootiest d170bd15cf refactor: remove references to legacy setting
The `--alpha` flag and `alpha = true/false` config file options remain
functional but references to them are removed as they are now superseded
by the output formats with alpha channels (rgba, hexa, hsla).
New users/configurations should use the relevant output formats. The
legacy flag/options remain active for backward-compatibility with older
configs/scripts.
2026-04-27 23:58:14 -04:00
6 changed files with 942 additions and 157 deletions
+42
View File
@@ -0,0 +1,42 @@
name: Auto Label PRs
on:
pull_request:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Apply labels based on PR title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
API_URL: ${{ github.api_url }}
run: |
shopt -s nocasematch
labels="["
if [[ "$PR_TITLE" =~ ^(ci|test)(/|:) ]]; then
labels="${labels}13," # 13 is Kind/Testing
fi
if [[ "$PR_TITLE" =~ ^docs(/|:) ]]; then
labels="${labels}14," # 14 is Kind/Documentation
fi
labels="${labels%,}]"
if [ "$labels" != "[]" ]; then
echo "Adding labels to PR #${PR_NUMBER}: ${labels}"
curl -X POST "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${PR_NUMBER}/labels" \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"labels\": ${labels}}"
else
echo "No matching prefixes found in title: ${PR_TITLE}"
fi
+4 -1
View File
@@ -8,7 +8,10 @@ on:
jobs: jobs:
release: release:
if: github.event.pull_request.merged == true if: |
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.labels.*.name, 'Kind/Testing') &&
!contains(github.event.pull_request.labels.*.name, 'Kind/Documentation')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
+22
View File
@@ -0,0 +1,22 @@
name: Test PR
on:
pull_request:
branches:
- main
jobs:
test:
if: "!contains(github.event.pull_request.labels.*.name, 'Kind/Documentation')"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Make scripts executable
run: |
chmod +x color-tool
chmod +x tests/run_tests.sh
- name: Run test suite
run: ./tests/run_tests.sh
+83 -19
View File
@@ -10,33 +10,62 @@ A feature-rich, portable CLI color utility for Linux, specializing in color pick
- **Smart JSON Output:** Generate machine-readable JSON tables for easy integration with other tools. - **Smart JSON Output:** Generate machine-readable JSON tables for easy integration with other tools.
- **Color Naming:** Resolve hex values to human-readable names via thecolorapi.com. - **Color Naming:** Resolve hex values to human-readable names via thecolorapi.com.
- **Visual Previews:** Render color swatches directly in your terminal using truecolor ANSI escapes. - **Visual Previews:** Render color swatches directly in your terminal using truecolor ANSI escapes.
- **Desktop Integration:** Built-in support for clipboard copying (`wl-copy`/`xclip`) and desktop notifications. - **Desktop Integration:** Built-in support for clipboard copying (`wl-copy`/`xclip`) and rich desktop notifications with visual color swatches.
- **Highly Configurable:** Separate configuration profiles for standard terminal usage and desktop/launcher mode. - **Highly Configurable:** Separate configuration profiles for standard terminal usage and desktop/launcher mode.
- **Portable Design:** The entire tool is contained within a single Bash script, with embedded Python helpers. - **Portable Design:** The entire tool is contained within a single Bash script, with embedded Python helpers.
## 📋 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
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`).
- **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
Simply download the `color-tool` script and run the install command: ### 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:
```bash ```bash
chmod +x color-tool # Download and install the latest version of the color-tool script
./color-tool --install curl -sSLO https://git.rootiest.dev/rootiest/color-tool/releases/download/latest/color-tool && bash color-tool --install && rm ./color-tool
``` ```
This will: This will:
1. Move the script to `~/.local/share/color-tool/`. 1. Install the script to `~/.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 `~/.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 so you can launch the picker from your application menu.
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/latest) 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
@@ -52,6 +81,10 @@ Options:
--[no-]copy Copy result to clipboard --[no-]copy Copy result to clipboard
--[no-]notify Show desktop notification --[no-]notify Show desktop notification
--desktop GUI mode: pick → copy → notify --desktop GUI mode: pick → copy → notify
--get-config Print the current configuration
--set-config Update the configuration (e.g. --set-config desktop --copy)
--reset-config Restore the configuration file to its default values
--check-deps Check system dependencies and environment support
--install Install to ~/.local/share/ and symlink to ~/.local/bin/ --install Install to ~/.local/share/ and symlink to ~/.local/bin/
--help, -h Show this help message --help, -h Show this help message
``` ```
@@ -84,22 +117,53 @@ You can define your preferred defaults in `~/.config/color-tool/config.toml`. Th
```toml ```toml
[defaults] [defaults]
output = "hex" # default output format(s) # Set any option to true to enable it by default when using the terminal
output = "hex" # default output format(s): hex, rgb, hsl, rgba, hsla, hexa, all
json = false # output in JSON format json = false # output in JSON format
swatch = false # show color swatch in terminal swatch = false # show color swatch in terminal
name = false # fetch color name name = false # fetch color name from thecolorapi.com
copy = false # copy result to clipboard copy = false # copy result to clipboard
pick = false # auto-launch picker pick = false # auto-launch color picker when invoked with no arguments
notify = false # show desktop notification notify = false # show desktop notification
[desktop] [desktop]
output = "hex" # Defaults for --desktop mode (launched from the app menu; copy is always enabled by default)
json = false output = "hex" # format to copy
name = false json = false # copy JSON format instead of plain text
copy = true name = false # fetch color name (requires network)
notify = true copy = true # copy result to clipboard
notify = true # show desktop notification with the copied value
swatch = true # show color swatch in notification
``` ```
You can also use the CLI to manage your configuration:
```bash
# View current config
color-tool --get-config
# Update specific keys (e.g., enable swatch by default)
color-tool --set-config defaults --swatch
# Set output format for desktop mode
color-tool --set-config desktop output=rgba
# Reset to factory defaults
color-tool --reset-config
```
## 🧪 Testing
The project includes a comprehensive test suite that verifies color conversion, configuration loading, and system integrations (clipboard/notifications) using mocks.
To run the tests:
```bash
./tests/run_tests.sh
```
The test script creates a temporary isolated environment, so it won't affect your system configuration or active clipboard.
## 🤝 Credits ## 🤝 Credits
The `wl-colorpicker-plasma` integration is based on the original work by [SASUPERNOVA](https://github.com/SASUPERNOVA/wl-colorpicker-plasma). The `wl-colorpicker-plasma` integration is based on the original work by [SASUPERNOVA](https://github.com/SASUPERNOVA/wl-colorpicker-plasma).
+549 -137
View File
@@ -23,6 +23,32 @@ 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" CONFIG_FILE="$HOME/.config/color-tool/config.toml"
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="$HOME/.local/share/color-tool"
BIN_DIR="$HOME/.local/bin"
BIN_PATH="$BIN_DIR/color-tool"
CONFIG_DIR="$HOME/.config/color-tool"
APP_DIR="$HOME/.local/share/applications"
DESKTOP_FILE="$APP_DIR/color-tool.desktop"
# ── Initial Defaults ────────────────────────────────────────────────────────── # ── Initial Defaults ──────────────────────────────────────────────────────────
@@ -44,6 +70,7 @@ desktop_name=0
desktop_notify=1 desktop_notify=1
desktop_copy=1 desktop_copy=1
desktop_output="hex" desktop_output="hex"
desktop_swatch=1
# Track which settings were explicitly set via CLI flags to ensure they override config # Track which settings were explicitly set via CLI flags to ensure they override config
cli_json="" cli_json=""
@@ -57,13 +84,189 @@ cli_output=""
# ── Config Loader ───────────────────────────────────────────────────────────── # ── Config Loader ─────────────────────────────────────────────────────────────
get_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Config file not found at $CONFIG_FILE" >&2
exit 1
fi
local first_section=1
while IFS= read -r line || [[ -n "$line" ]]; do
local stripped="${line%%#*}"
stripped="${stripped%"${stripped##*[![:space:]]}"}"
if [[ -z "$stripped" ]]; then
continue
fi
if [[ "$stripped" =~ ^\[([A-Za-z_-]+)\]$ ]]; then
if [[ $first_section -eq 0 ]]; then
echo ""
fi
first_section=0
echo "$stripped"
elif [[ "$stripped" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local val="${BASH_REMATCH[2]}"
printf "%-6s = %s\n" "$key" "$val"
fi
done <"$CONFIG_FILE"
}
set_config() {
local target_section="defaults"
if [[ $# -gt 0 ]]; then
if [[ "$1" == "default" || "$1" == "defaults" ]]; then
target_section="defaults"
shift
elif [[ "$1" == "desktop" ]]; then
target_section="desktop"
shift
fi
fi
local new_output=""
local new_json=""
local new_swatch=""
local new_name=""
local new_copy=""
local new_pick=""
local new_notify=""
local new_alpha=""
while [[ $# -gt 0 ]]; do
case "$1" in
--output=*) new_output="${1#*=}" ;;
output=*) new_output="${1#*=}" ;;
--json) new_json="true" ;;
--no-json) new_json="false" ;;
--alpha) new_alpha="true" ;;
--no-alpha) new_alpha="false" ;;
--name) new_name="true" ;;
--no-name) new_name="false" ;;
--swatch) new_swatch="true" ;;
--no-swatch) new_swatch="false" ;;
--copy) new_copy="true" ;;
--no-copy) new_copy="false" ;;
--notify) new_notify="true" ;;
--no-notify) new_notify="false" ;;
--pick) new_pick="true" ;;
--no-pick) new_pick="false" ;;
-*)
echo "Error: Unknown option for --set-config: $1" >&2
exit 1
;;
*)
echo "Error: Unknown argument for --set-config: $1" >&2
exit 1
;;
esac
shift
done
if [[ -n "$new_output" ]]; then
new_output="${new_output#\"}"
new_output="${new_output%\"}"
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Config file not found at $CONFIG_FILE. Run --install first." >&2
exit 1
fi
local temp_file
temp_file="$(mktemp)"
local current_section=""
while IFS= read -r line || [[ -n "$line" ]]; do
local original_line="$line"
local parsed_line="${line%%#*}"
if [[ "$parsed_line" =~ ^\[([A-Za-z_-]+)\]$ ]]; then
current_section="${BASH_REMATCH[1]}"
echo "$original_line" >>"$temp_file"
continue
fi
if [[ "$current_section" == "$target_section" && "$parsed_line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*= ]]; then
local key="${BASH_REMATCH[1]}"
local new_val=""
case "$key" in
output)
if [[ -n "$new_output" ]]; then new_val="\"$new_output\""; fi
;;
json)
if [[ -n "$new_json" ]]; then new_val="$new_json"; fi
;;
alpha)
if [[ -n "$new_alpha" ]]; then new_val="$new_alpha"; fi
;;
swatch)
if [[ -n "$new_swatch" ]]; then new_val="$new_swatch"; fi
;;
name)
if [[ -n "$new_name" ]]; then new_val="$new_name"; fi
;;
copy)
if [[ -n "$new_copy" ]]; then new_val="$new_copy"; fi
;;
pick)
if [[ -n "$new_pick" ]]; then new_val="$new_pick"; fi
;;
notify)
if [[ -n "$new_notify" ]]; then new_val="$new_notify"; fi
;;
esac
if [[ -n "$new_val" ]]; then
if [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)([^[:space:]#]+)(.*)$ ]]; then
local prefix="${BASH_REMATCH[1]}"
local old_val="${BASH_REMATCH[2]}"
local suffix="${BASH_REMATCH[3]}"
# Adjust whitespace to keep comments aligned if the value length changed
if [[ "$key" != "output" ]]; then
local old_len=${#old_val}
local new_len=${#new_val}
local diff=$((old_len - new_len))
if [[ $diff -gt 0 ]]; then
# new value is shorter, add spaces to suffix
local spaces=""
for ((i = 0; i < diff; i++)); do spaces+=" "; done
suffix="$spaces$suffix"
elif [[ $diff -lt 0 ]]; then
# new value is longer, remove spaces from suffix
local remove=$((-diff))
for ((i = 0; i < remove; i++)); do
if [[ "$suffix" == " "* ]]; then
suffix="${suffix# }"
fi
done
fi
fi
echo "$prefix$new_val$suffix" >>"$temp_file"
continue
elif [[ "$original_line" =~ ^([[:space:]]*[A-Za-z_]+[[:space:]]*=[[:space:]]*)(.*)$ ]]; then
echo "${BASH_REMATCH[1]}$new_val" >>"$temp_file"
continue
fi
fi
fi
echo "$original_line" >>"$temp_file"
done <"$CONFIG_FILE"
mv "$temp_file" "$CONFIG_FILE"
}
# Parse ~/.config/color-tool/config.toml and apply [defaults] and [desktop] values. # Parse ~/.config/color-tool/config.toml and apply [defaults] and [desktop] values.
load_config() { load_config() {
[[ ! -f "$CONFIG_FILE" ]] && return 0 [[ ! -f "$CONFIG_FILE" ]] && return 0
local section="" key val local section="" key val
while IFS= read -r line; do while IFS= read -r line; do
line="${line%%#*}" # strip inline comments line="${line%%#*}" # strip inline comments
if [[ "$line" =~ ^[[:space:]]*$ ]]; then continue; fi if [[ "$line" =~ ^[[:space:]]*$ ]]; then continue; fi
# Section header # Section header
@@ -76,64 +279,95 @@ load_config() {
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*=[[:space:]]*([^[:space:]]+) ]]; then if [[ "$line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*=[[:space:]]*([^[:space:]]+) ]]; then
key="${BASH_REMATCH[1]}" key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2],,}" val="${BASH_REMATCH[2],,}"
val="${val#\"}" ; val="${val%\"}" val="${val#\"}"
val="${val%\"}"
case "$section:$key" in case "$section:$key" in
defaults:json) [[ "$val" == "true" ]] && json_mode=1 || json_mode=0 ;; defaults:json) [[ "$val" == "true" ]] && json_mode=1 || json_mode=0 ;;
defaults:alpha) [[ "$val" == "true" ]] && alpha_mode=1 || alpha_mode=0 ;; defaults:alpha) [[ "$val" == "true" ]] && alpha_mode=1 || alpha_mode=0 ;;
defaults:name) [[ "$val" == "true" ]] && name_mode=1 || name_mode=0 ;; defaults:name) [[ "$val" == "true" ]] && name_mode=1 || name_mode=0 ;;
defaults:copy) [[ "$val" == "true" ]] && copy_mode=1 || copy_mode=0 ;; defaults:copy) [[ "$val" == "true" ]] && copy_mode=1 || copy_mode=0 ;;
defaults:pick) [[ "$val" == "true" ]] && config_pick=1 || config_pick=0 ;; defaults:pick) [[ "$val" == "true" ]] && config_pick=1 || config_pick=0 ;;
defaults:notify) [[ "$val" == "true" ]] && notify_mode=1 || notify_mode=0 ;; defaults:notify) [[ "$val" == "true" ]] && notify_mode=1 || notify_mode=0 ;;
defaults:swatch) [[ "$val" == "true" ]] && swatch_mode=1 || swatch_mode=0 ;; defaults:swatch) [[ "$val" == "true" ]] && swatch_mode=1 || swatch_mode=0 ;;
defaults:output) output_formats="$val" ;; defaults:output) output_formats="$val" ;;
desktop:json) [[ "$val" == "true" ]] && desktop_json=1 || desktop_json=0 ;; desktop:json) [[ "$val" == "true" ]] && desktop_json=1 || desktop_json=0 ;;
desktop:alpha) [[ "$val" == "true" ]] && desktop_alpha=1 || desktop_alpha=0 ;; desktop:alpha) [[ "$val" == "true" ]] && desktop_alpha=1 || desktop_alpha=0 ;;
desktop:name) [[ "$val" == "true" ]] && desktop_name=1 || desktop_name=0 ;; desktop:name) [[ "$val" == "true" ]] && desktop_name=1 || desktop_name=0 ;;
desktop:notify) [[ "$val" == "true" ]] && desktop_notify=1 || desktop_notify=0 ;; desktop:notify) [[ "$val" == "true" ]] && desktop_notify=1 || desktop_notify=0 ;;
desktop:copy) [[ "$val" == "true" ]] && desktop_copy=1 || desktop_copy=0 ;; desktop:swatch) [[ "$val" == "true" ]] && desktop_swatch=1 || desktop_swatch=0 ;;
desktop:output) desktop_output="$val" ;; desktop:copy) [[ "$val" == "true" ]] && desktop_copy=1 || desktop_copy=0 ;;
desktop:output) desktop_output="$val" ;;
esac esac
fi fi
done < "$CONFIG_FILE" done <"$CONFIG_FILE"
}
write_default_config() {
mkdir -p "$(dirname "$CONFIG_FILE")"
cat >"$CONFIG_FILE" <<EOF
# color-tool configuration
# Source: $REPO_URL
[defaults]
# Set any option to true to enable it by default when using the terminal
output = "hex" # default output format(s): hex, rgb, hsl, rgba, hsla, hexa, all
json = false # output in JSON format
swatch = false # show color swatch in terminal
name = false # fetch color name from thecolorapi.com
copy = false # copy result to clipboard
pick = false # auto-launch color picker when invoked with no arguments
notify = false # show desktop notification
[desktop]
# Defaults for --desktop mode (launched from the app menu; copy is always enabled by default)
output = "hex" # format to copy
json = false # copy JSON format instead of plain text
name = false # fetch color name (requires network)
copy = true # copy result to clipboard
notify = true # show desktop notification with the copied value
swatch = true # show color swatch in notification
EOF
}
reset_config() {
write_default_config
echo "Configuration reset to defaults: $CONFIG_FILE"
} }
# ── 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\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-copy 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}--install${reset} Install to ~/.local/share/color-tool/ and symlink into ~/.local/bin/\n" printf " ${BOLD}${CYAN}--get-config${RESET} Print the current configuration\n"
printf " ${bold}${cyan}--help${reset}, ${bold}${cyan}-h${reset} Show this help message\n\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}--check-deps${RESET} Check system dependencies and environment support\n"
printf " ${BOLD}${CYAN}--install${RESET} Install to ~/.local/share/color-tool/ and symlink into ~/.local/bin/\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}~/.config/color-tool/config.toml${RESET}\n"
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${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 ─────────────────────────────────────────────────────
@@ -150,6 +384,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() {
@@ -159,7 +413,7 @@ copy_to_clipboard() {
elif command -v xclip &>/dev/null; then elif command -v xclip &>/dev/null; then
printf '%s' "$text" | xclip -selection clipboard printf '%s' "$text" | xclip -selection clipboard
else else
echo "Warning: no clipboard utility found (install wl-copy or xclip)" >&2 echo "Warning: Missing clipboard utility. Please install wl-clipboard (preferred) or xclip." >&2
echo " Value: $text" >&2 echo " Value: $text" >&2
return 1 return 1
fi fi
@@ -167,8 +421,30 @@ copy_to_clipboard() {
notify_result() { notify_result() {
local value="$1" local value="$1"
local hex_color="${2:-}"
command -v notify-send &>/dev/null || return 0 command -v notify-send &>/dev/null || return 0
notify-send -i "color-picker" "color-tool" "$value" || true
local icon_name="color-picker"
if [[ $swatch_mode -eq 1 && -n "$hex_color" ]]; then
local swatch_path="/tmp/color_swatch_${hex_color//\#/}.png"
if command -v magick >/dev/null 2>&1; then
if magick -size 64x64 xc:"$hex_color" "$swatch_path" 2>/dev/null; then
icon_name="$swatch_path"
fi
elif command -v convert >/dev/null 2>&1; then
if convert -size 64x64 xc:"$hex_color" "$swatch_path" 2>/dev/null; then
icon_name="$swatch_path"
fi
fi
fi
notify-send -i "$icon_name" "color-tool" "$value" || true
}
notify_error() {
local value="$1"
command -v notify-send &>/dev/null || return 0
notify-send -u normal -i "dialog-warning" "color-tool" "$value" || true
} }
# ── Color picker ────────────────────────────────────────────────────────────── # ── Color picker ──────────────────────────────────────────────────────────────
@@ -176,7 +452,7 @@ notify_result() {
# Generate the internal Python helper for KDE Plasma color picking # Generate the internal Python helper for KDE Plasma color picking
generate_picker_script() { generate_picker_script() {
local target="$1" local target="$1"
cat > "$target" <<'EOF' cat >"$target" <<'EOF'
#!/usr/bin/python3 #!/usr/bin/python3
# #
# Original work Copyright (C) 2024 SASUPERNOVA # Original work Copyright (C) 2024 SASUPERNOVA
@@ -215,22 +491,22 @@ EOF
run_color_picker() { run_color_picker() {
check_kde_wayland || return 1 check_kde_wayland || return 1
local picker_path="$SCRIPT_DIR/wl-colorpicker-plasma.py" local picker_path="$SCRIPT_DIR/wl-colorpicker-plasma.py"
local cleanup=0 local cleanup=0
if [[ ! -f "$picker_path" ]]; then if [[ ! -f "$picker_path" ]]; then
picker_path=$(mktemp /tmp/wl-colorpicker-XXXXXX.py) picker_path=$(mktemp /tmp/wl-colorpicker-XXXXXX.py)
generate_picker_script "$picker_path" generate_picker_script "$picker_path"
cleanup=1 cleanup=1
fi fi
local picked local picked
picked=$(python3 "$picker_path") picked=$(python3 "$picker_path")
local exit_code=$? local exit_code=$?
[[ $cleanup -eq 1 ]] && rm -f "$picker_path" [[ $cleanup -eq 1 ]] && rm -f "$picker_path"
if [[ $exit_code -eq 0 ]]; then if [[ $exit_code -eq 0 ]]; then
echo "$picked" echo "$picked"
else else
@@ -242,93 +518,174 @@ 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
}
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
_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() {
local missing=()
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")
fi
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")
fi
if ! command -v python3 >/dev/null 2>&1; then
missing+=("python3 — required for color conversions and picker GUI")
elif ! python3 -c "import PyQt6.QtDBus" >/dev/null 2>&1; then
missing+=("python3-pyqt6 — required for the KDE color picker DBus interface")
fi
if ! command -v jq >/dev/null 2>&1; then
missing+=("jq — required for JSON formatting and color names")
fi
if ! command -v curl >/dev/null 2>&1; then
missing+=("curl — required for fetching color names from the API")
fi
if ! command -v notify-send >/dev/null 2>&1; then
missing+=("libnotify (notify-send) — required for desktop notifications")
fi
local env_ok=1
if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then
env_ok=0
fi
local desktop="${XDG_CURRENT_DESKTOP:-}"
if [[ "$desktop" != *KDE* ]] && [[ -z "${KDE_FULL_SESSION:-}" ]]; then
env_ok=0
fi
if [[ $env_ok -eq 0 ]]; then
missing+=("KDE Plasma on Wayland — required for the screen color picker feature")
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
printf "\n ${BOLD}${YELLOW}Warnings — Missing optional dependencies:${RESET}\n"
for item in "${missing[@]}"; do
printf " - %s\n" "$item"
done
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")
mkdir -p "$config_dir" for dir in "${dirs[@]}"; do
if [[ ! -f "$config_file" ]]; then if [[ ! -d "$dir" ]]; then
cat > "$config_file" <<'EOF' printf " ${DIM}Creating %s...${RESET}\n" "$dir"
# color-tool configuration mkdir -p "$dir"
# https://github.com/rootiest/color-tool fi
done
[defaults] # Core installation steps
# Set any option to true to enable it by default when using the terminal cp "$src_script" "$DATA_DIR/color-tool"
output = "hex" # default output format(s): hex, rgb, hsl, rgba, hsla, hexa, all chmod +x "$DATA_DIR/color-tool"
json = false # output in JSON format generate_picker_script "$DATA_DIR/wl-colorpicker-plasma.py"
alpha = false # include alpha channel (8-digit hex) ln -sf "$DATA_DIR/color-tool" "$BIN_PATH"
swatch = false # show color swatch in terminal
name = false # fetch color name from thecolorapi.com
copy = false # copy result to clipboard
pick = false # auto-launch color picker when invoked with no arguments
notify = false # show desktop notification
[desktop] # Config setup
# Defaults for --desktop mode (launched from the app menu; copy is always enabled by default) if [[ ! -f "$CONFIG_FILE" ]]; then
output = "hex" # format to copy write_default_config
json = false # copy JSON format instead of plain text printf " ${BOLD}${GREEN}config${RESET} %s ${DIM}(sample created)${RESET}\n" "$CONFIG_FILE"
alpha = false # include alpha channel
name = false # fetch color name (requires network)
copy = true # copy result to clipboard
notify = true # show desktop notification with the copied value
EOF
printf " config %s (sample created)\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
} }
# ── Color conversion ────────────────────────────────────────────────────────── # ── Color conversion ──────────────────────────────────────────────────────────
@@ -381,7 +738,7 @@ print(json.dumps(formats))
validate_output_formats() { validate_output_formats() {
local formats="$1" local formats="$1"
local valid_fmts=("hex" "hexa" "rgb" "rgba" "hsl" "hsla") local valid_fmts=("hex" "hexa" "rgb" "rgba" "hsl" "hsla")
IFS=',' read -ra ADDR <<< "$formats" IFS=',' read -ra ADDR <<<"$formats"
for fmt in "${ADDR[@]}"; do for fmt in "${ADDR[@]}"; do
[[ "$fmt" == "all" ]] && continue [[ "$fmt" == "all" ]] && continue
local is_valid=0 local is_valid=0
@@ -398,18 +755,23 @@ validate_output_formats() {
process_color() { process_color() {
local input="$1" local input="$1"
local formats_json local formats_json
formats_json=$(get_all_formats "$input") || { echo "Error: Invalid color: $input" >&2; return 1; } formats_json=$(get_all_formats "$input") || {
echo "Error: Invalid color: $input" >&2
return 1
}
local hex_color
hex_color=$(echo "$formats_json" | jq -r '.hex // empty')
local name="" local name=""
if [[ $name_mode -eq 1 ]]; then if [[ $name_mode -eq 1 ]]; then
local hex_for_api local hex_for_api="${hex_color//#/}"
hex_for_api=$(echo "$formats_json" | jq -r '.hex' | tr -d '#')
name=$(curl -s "https://www.thecolorapi.com/id?hex=$hex_for_api" | jq -r '.name.value // empty') name=$(curl -s "https://www.thecolorapi.com/id?hex=$hex_for_api" | jq -r '.name.value // empty')
fi fi
local selected_fmts=() local selected_fmts=()
local valid_fmts=("hex" "hexa" "rgb" "rgba" "hsl" "hsla") local valid_fmts=("hex" "hexa" "rgb" "rgba" "hsl" "hsla")
IFS=',' read -ra ADDR <<< "$output_formats" IFS=',' read -ra ADDR <<<"$output_formats"
for fmt in "${ADDR[@]}"; do for fmt in "${ADDR[@]}"; do
if [[ "$fmt" == "all" ]]; then if [[ "$fmt" == "all" ]]; then
selected_fmts=("${valid_fmts[@]}") selected_fmts=("${valid_fmts[@]}")
@@ -436,7 +798,10 @@ process_color() {
if [[ $json_mode -eq 1 ]]; then if [[ $json_mode -eq 1 ]]; then
output_text="$json_obj" output_text="$json_obj"
else else
output_text=$(IFS=' ' ; echo "${display_parts[*]}") output_text=$(
IFS=' '
echo "${display_parts[*]}"
)
[[ -n "$name" ]] && output_text="$output_text ($name)" [[ -n "$name" ]] && output_text="$output_text ($name)"
fi fi
@@ -446,14 +811,29 @@ process_color() {
r=$(echo "$formats_json" | jq -r '._raw.r') r=$(echo "$formats_json" | jq -r '._raw.r')
g=$(echo "$formats_json" | jq -r '._raw.g') g=$(echo "$formats_json" | jq -r '._raw.g')
b=$(echo "$formats_json" | jq -r '._raw.b') b=$(echo "$formats_json" | jq -r '._raw.b')
if [[ $json_mode -eq 1 ]]; then printf "\033[48;2;${r};${g};${b}m \033[0m\n" if [[ $json_mode -eq 1 ]]; then
printf "\033[48;2;${r};${g};${b}m \033[0m\n"
else printf "\033[48;2;${r};${g};${b}m \033[0m "; fi else printf "\033[48;2;${r};${g};${b}m \033[0m "; fi
fi fi
echo -e "$output_text" echo -e "$output_text"
fi fi
[[ $copy_mode -eq 1 ]] && copy_to_clipboard "$output_text" || true local copy_failed=0
[[ $notify_mode -eq 1 ]] && notify_result "$output_text" || true if [[ $copy_mode -eq 1 ]]; then
if ! copy_to_clipboard "$output_text"; then
copy_failed=1
fi
fi
if [[ $notify_mode -eq 1 ]]; then
if [[ $copy_failed -eq 1 ]]; then
notify_error "Missing clipboard utility. Please install wl-clipboard (preferred) or xclip.
Value: $output_text"
else
notify_result "$output_text" "$hex_color"
fi
fi
} }
# ── Argument parsing ────────────────────────────────────────────────────────── # ── Argument parsing ──────────────────────────────────────────────────────────
@@ -465,31 +845,62 @@ do_pick=0
# First pass: collect CLI overrides # First pass: collect CLI overrides
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--help|-h) show_help; exit 0 ;; --help | -h)
--output) cli_output="$2"; shift ;; show_help
--json) cli_json=1 ;; exit 0
--no-json) cli_json=0 ;; ;;
--alpha) cli_alpha=1; cli_output="hexa" ;; --output)
--no-alpha)cli_alpha=0 ;; cli_output="$2"
--name) cli_name=1 ;; shift
--no-name) cli_name=0 ;; ;;
--swatch) cli_swatch=1 ;; --json) cli_json=1 ;;
--no-swatch)cli_swatch=0 ;; --no-json) cli_json=0 ;;
--copy) cli_copy=1 ;; --alpha)
--no-copy) cli_copy=0 ;; cli_alpha=1
--notify) cli_notify=1 ;; cli_output="hexa"
--no-notify)cli_notify=0 ;; ;;
--pick) cli_pick=1 ;; --no-alpha) cli_alpha=0 ;;
--no-pick) cli_pick=0 ;; --name) cli_name=1 ;;
--desktop) desktop_mode=1 ;; --no-name) cli_name=0 ;;
--install) do_install; exit 0 ;; --swatch) cli_swatch=1 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;; --no-swatch) cli_swatch=0 ;;
*) args+=("$1") ;; --copy) cli_copy=1 ;;
--no-copy) cli_copy=0 ;;
--notify) cli_notify=1 ;;
--no-notify) cli_notify=0 ;;
--pick) cli_pick=1 ;;
--no-pick) cli_pick=0 ;;
--desktop) desktop_mode=1 ;;
--install)
do_install
exit 0
;;
--get-config)
get_config
exit 0
;;
--set-config)
shift
set_config "$@"
exit 0
;;
--reset-config)
reset_config
exit 0
;;
--check-deps)
check_deps
exit 0
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*) args+=("$1") ;;
esac esac
shift shift
done done
# Apply final hierarchy: CLI Flags > Desktop Config (if --desktop) > Default Config
if [[ $desktop_mode -eq 1 ]]; then if [[ $desktop_mode -eq 1 ]]; then
json_mode=${cli_json:-$desktop_json} json_mode=${cli_json:-$desktop_json}
alpha_mode=${cli_alpha:-$desktop_alpha} alpha_mode=${cli_alpha:-$desktop_alpha}
@@ -497,8 +908,8 @@ if [[ $desktop_mode -eq 1 ]]; then
notify_mode=${cli_notify:-$desktop_notify} notify_mode=${cli_notify:-$desktop_notify}
output_formats=${cli_output:-$desktop_output} output_formats=${cli_output:-$desktop_output}
copy_mode=${cli_copy:-$desktop_copy} copy_mode=${cli_copy:-$desktop_copy}
swatch_mode=${cli_swatch:-$desktop_swatch}
do_pick=${cli_pick:-1} do_pick=${cli_pick:-1}
swatch_mode=${cli_swatch:-0} # swatch usually off in desktop mode
else else
json_mode=${cli_json:-$json_mode} json_mode=${cli_json:-$json_mode}
alpha_mode=${cli_alpha:-$alpha_mode} alpha_mode=${cli_alpha:-$alpha_mode}
@@ -525,7 +936,8 @@ elif [[ ${#args[@]} -eq 0 ]]; then
picked="$(run_color_picker)" || exit 1 picked="$(run_color_picker)" || exit 1
[[ -n "$picked" ]] && args+=("$picked") [[ -n "$picked" ]] && args+=("$picked")
elif [[ -t 0 ]]; then elif [[ -t 0 ]]; then
show_help; exit 0 show_help
exit 0
fi fi
fi fi
+242
View File
@@ -0,0 +1,242 @@
#!/bin/bash
set -euo pipefail
# ── Setup ─────────────────────────────────────────────────────────────────────
# Using a static-ish path since we can't use command substitution in cat
TEST_DIR="/tmp/color-tool-tests-repro"
BIN_DIR="$TEST_DIR/bin"
rm -rf "$TEST_DIR"
mkdir -p "$BIN_DIR"
# Mock notify-send
cat <<'MOCK' > "$BIN_DIR/notify-send"
#!/bin/bash
echo "NOTIFY: $*" >> "/tmp/color-tool-tests-repro/notify.log"
MOCK
# Mock wl-copy
cat <<'MOCK' > "$BIN_DIR/wl-copy"
#!/bin/bash
cat > "/tmp/color-tool-tests-repro/clipboard.txt"
MOCK
# Mock python3
cat <<'MOCK' > "$BIN_DIR/python3"
#!/bin/bash
if [[ "$*" == *wl-colorpicker*.py ]]; then
echo "#aabbcc"
exit 0
fi
exec /usr/bin/python3 "$@"
MOCK
chmod +x "$BIN_DIR"/*
# Symlink essential tools
ln -s /usr/bin/bash "$BIN_DIR/bash"
ln -s /usr/bin/cat "$BIN_DIR/cat"
ln -s /usr/bin/printf "$BIN_DIR/printf"
ln -s /usr/bin/echo "$BIN_DIR/echo"
ln -s /usr/bin/sed "$BIN_DIR/sed"
ln -s /usr/bin/grep "$BIN_DIR/grep"
ln -s /usr/bin/awk "$BIN_DIR/awk"
ln -s /usr/bin/jq "$BIN_DIR/jq"
ln -s /usr/bin/readlink "$BIN_DIR/readlink"
ln -s /usr/bin/dirname "$BIN_DIR/dirname"
ln -s /usr/bin/mkdir "$BIN_DIR/mkdir"
ln -s /usr/bin/mktemp "$BIN_DIR/mktemp"
ln -s /usr/bin/chmod "$BIN_DIR/chmod"
ln -s /usr/bin/rm "$BIN_DIR/rm"
# Mock curl for thecolorapi.com
cat <<'MOCK' > "$BIN_DIR/curl"
#!/bin/bash
if [[ "$*" == *"thecolorapi.com"* ]]; then
echo '{"name": {"value": "Mocked Color Name"}}'
exit 0
fi
exec /usr/bin/curl "$@"
MOCK
chmod +x "$BIN_DIR/curl"
ln -s /usr/bin/mv "$BIN_DIR/mv"
ln -s /usr/bin/tr "$BIN_DIR/tr"
export PATH="$BIN_DIR"
export HOME="$TEST_DIR"
export WAYLAND_DISPLAY=wayland-0
export XDG_CURRENT_DESKTOP=KDE
# Resolve the absolute path of color-tool
COLOR_TOOL="$(/usr/bin/readlink -f ./color-tool)"
# ── Test Helpers ──────────────────────────────────────────────────────────────
total=0
passed=0
it() {
local label="$1"
total=$((total + 1))
printf "Test: %s... " "$label"
}
assert_contains() {
local haystack="$1"
local needle="$2"
if [[ "$haystack" == *"$needle"* ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Expected to find: $needle"
echo " Actual output: $haystack"
return 1
fi
}
# ── Tests ─────────────────────────────────────────────────────────────────────
# 1. Basic Conversion
it "converts hex to rgb"
output=$("$COLOR_TOOL" "#ffffff" --output rgb --no-copy --no-notify)
assert_contains "$output" "rgb(255, 255, 255)"
# 2. Multiple Formats
it "handles multiple formats"
output=$("$COLOR_TOOL" "#000000" --output hex,rgba --no-copy --no-notify)
assert_contains "$output" "#000000 rgba(0, 0, 0, 1.0)"
# 3. Clipboard Integration
it "copies to clipboard (mocked)"
rm -f "$TEST_DIR/clipboard.txt"
"$COLOR_TOOL" "#ff0000" --copy --no-notify >/dev/null
if [[ -f "$TEST_DIR/clipboard.txt" ]]; then
assert_contains "$(cat "$TEST_DIR/clipboard.txt")" "#ff0000"
else
echo "FAIL (clipboard.txt not created)"
fi
# 4. Notification Integration
it "sends notifications (mocked)"
rm -f "$TEST_DIR/notify.log"
"$COLOR_TOOL" "#00ff00" --notify --no-copy >/dev/null
if [[ -f "$TEST_DIR/notify.log" ]]; then
assert_contains "$(cat "$TEST_DIR/notify.log")" "NOTIFY: -i color-picker color-tool #00ff00"
else
echo "FAIL (notify.log not created)"
fi
# 5. Color Picker (Mocked)
it "launches color picker and processes result"
output=$("$COLOR_TOOL" --pick --no-copy --no-notify)
assert_contains "$output" "#aabbcc"
# 6. Config Loading
it "loads defaults from config.toml"
mkdir -p "$HOME/.config/color-tool"
cat <<'CONF' > "$HOME/.config/color-tool/config.toml"
[defaults]
output = "rgba"
CONF
output=$("$COLOR_TOOL" "#ffffff" --no-copy --no-notify)
assert_contains "$output" "rgba(255, 255, 255, 1.0)"
# 7. Error Notification
it "notifies on missing clipboard utility"
rm -f "$BIN_DIR/wl-copy"
rm -f "$TEST_DIR/notify.log"
"$COLOR_TOOL" "#123456" --copy --notify 2>/dev/null >/dev/null
if [[ -f "$TEST_DIR/notify.log" ]]; then
assert_contains "$(cat "$TEST_DIR/notify.log")" "NOTIFY: -u normal -i dialog-warning color-tool Missing clipboard utility"
else
echo "FAIL (notify.log not created)"
fi
# 8. Config Management
it "manages configuration via CLI"
mkdir -p "$HOME/.config/color-tool"
"$COLOR_TOOL" --reset-config >/dev/null
# Test --set-config
"$COLOR_TOOL" --set-config defaults output=rgba
output=$("$COLOR_TOOL" --get-config)
if [[ "$output" == *"output = \"rgba\""* ]]; then
# Test --reset-config
"$COLOR_TOOL" --reset-config >/dev/null
output=$("$COLOR_TOOL" --get-config)
assert_contains "$output" "output = \"hex\""
else
echo "FAIL"
echo " Expected to find: output = \"rgba\""
echo " Actual output: $output"
fi
# 9. JSON Output
it "outputs selected formats as JSON"
output=$("$COLOR_TOOL" "#00ff00" --output hex,rgb --json --no-copy --no-notify)
if [[ "$output" == *'"hex": "#00ff00"'* ]] && [[ "$output" == *'"rgb": "rgb(0, 255, 0)"'* ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Actual output: $output"
fi
# 10. Swatch Output
it "outputs visual swatch"
output=$("$COLOR_TOOL" "#112233" --output hex --swatch --no-copy --no-notify)
if [[ "$output" == *"$(printf "\033[48;2;17;34;51m \033[0m")"* ]] && [[ "$output" == *"#112233"* ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Actual output: $output"
fi
# 11. Color Name Fetching
it "fetches color name from thecolorapi.com"
output=$("$COLOR_TOOL" "#ffffff" --output hex --name --no-copy --no-notify)
assert_contains "$output" "(Mocked Color Name)"
# 12. Invalid Color Input
it "rejects invalid colors"
output=$("$COLOR_TOOL" "invalid-color" --no-copy --no-notify 2>&1 || true)
assert_contains "$output" "Error: Invalid color: invalid-color"
# 13. Invalid Format Input
it "rejects invalid formats"
output=$("$COLOR_TOOL" "#000000" --output badfmt --no-copy --no-notify 2>&1 || true)
assert_contains "$output" "Error: Invalid output format: badfmt"
# 14. Installation Dependency Warnings
it "warns about missing dependencies during install"
mkdir -p "$TEST_DIR/install_bin"
for cmd in bash cat echo printf mkdir mktemp chmod rm cp ln dirname readlink ps; do
if [[ -e "/usr/bin/$cmd" || -L "/usr/bin/$cmd" ]]; then
/usr/bin/ln -s "/usr/bin/$cmd" "$TEST_DIR/install_bin/$cmd"
elif [[ -e "/bin/$cmd" || -L "/bin/$cmd" ]]; then
/usr/bin/ln -s "/bin/$cmd" "$TEST_DIR/install_bin/$cmd"
fi
done
output=$(PATH="$TEST_DIR/install_bin" HOME="$TEST_DIR" WAYLAND_DISPLAY="" XDG_CURRENT_DESKTOP="" KDE_FULL_SESSION="" "$COLOR_TOOL" --install 2>&1 || true)
if [[ "$output" == *"python3 — required"* ]] &&
[[ "$output" == *"jq — required"* ]] &&
[[ "$output" == *"curl — required"* ]] &&
[[ "$output" == *"libnotify (notify-send)"* ]] &&
[[ "$output" == *"KDE Plasma on Wayland"* ]] &&
[[ "$output" == *"ImageMagick"* ]] &&
[[ "$output" == *"wl-clipboard"* ]]; then
echo "PASS"
passed=$((passed + 1))
else
echo "FAIL"
echo " Actual output: $output"
fi
# ── Cleanup ───────────────────────────────────────────────────────────────────
echo "---------------------------------------"
echo "Result: $passed/$total tests passed."
rm -rf "$TEST_DIR"
if [[ $passed -ne $total ]]; then
exit 1
fi