Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dotfiles/.config/hypr/conf/autostart.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@ hl.on("hyprland.start", function ()

-- Start autostart cleanup
hl.exec_cmd("~/.config/hypr/scripts/cleanup.sh")

-- Restore screen filter
hl.exec_cmd("~/.config/hypr/scripts/screen-filter.sh restore")
end)
3 changes: 3 additions & 0 deletions dotfiles/.config/hypr/conf/keybindings/default.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ hl.bind(mainMod .. " + CTRL + L", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-p
hl.bind(mainMod .. " + SHIFT + H", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-hyprsunset"), { description = "Toggle Hyprsunset" })
hl.bind(mainMod .. " + Tab", hl.dsp.exec_cmd("qs -p ~/.local/share/quickshell-overview ipc call overview toggle"), { description = "Open Select Window Menu" })
hl.bind("CTRL + ALT + T", hl.dsp.exec_cmd("~/.config/ml4w/themes/themes.sh"), { description = "Open Select Window Menu" })
hl.bind(mainMod .. " + SHIFT + N", hl.dsp.exec_cmd("~/.config/hypr/scripts/screen-filter.sh"), { description = "Cycle screen color filters" })
hl.bind(mainMod .. " + CTRL + N", hl.dsp.exec_cmd("~/.config/hypr/scripts/screen-filter.sh --menu"), { description = "Open screen filter menu" })
hl.bind(mainMod .. " + ALT + N", hl.dsp.exec_cmd("~/.config/hypr/scripts/screen-filter.sh off"), { description = "Disable screen color filters" })

-- Special workspace (scratchpad)
hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("scratchpad"), { description = "Toggle special workspace scratchpad" })
Expand Down
317 changes: 317 additions & 0 deletions dotfiles/.config/hypr/scripts/screen-filter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
#!/usr/bin/env bash

set -Eeuo pipefail

SHADER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/hypr/shaders"
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/hypr/screen-filter"
APP_NAME="Screen Filter"
NOTIFICATION_ID="screen-filter"

if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]] || ! command -v hyprctl >/dev/null 2>&1; then
echo "Error: Hyprland instance not detected or hyprctl missing." >&2
exit 1
fi

mkdir -p "$SHADER_DIR"

notify() {
local title="$1"
local msg="$2"
local icon="${3:-video-display}"
local timeout="${4:-1500}"

if command -v notify-send >/dev/null 2>&1; then
notify-send "$title" "$msg" \
-a "$APP_NAME" \
-i "$icon" \
-h "string:x-canonical-private-synchronous:$NOTIFICATION_ID" \
-t "$timeout" 2>/dev/null || true
fi
}

save_state() {
local filter_name="$1"
mkdir -p "$(dirname "$STATE_FILE")" 2>/dev/null || true
echo "$filter_name" > "$STATE_FILE" 2>/dev/null || true
}

apply_shader() {
local target="${1:-}"
hyprctl eval "hl.config({ decoration = { screen_shader = '$target' } })" >/dev/null 2>&1 || \
hyprctl keyword decoration:screen_shader "$target" >/dev/null 2>&1
}

get_active_shader() {
local path
path=$(hyprctl getoption decoration:screen_shader 2>/dev/null | awk -F': ' '/str:/ {print $2}' | tr -d '[]' | xargs)
if [[ -n "$path" && "$path" != "EMPTY" && "$path" != "null" ]]; then
echo "$path"
else
echo ""
fi
}

get_available_filters() {
local available=("none")
local default_order=("greyscale" "nightlight" "sepia" "invert-colors")

for name in "${default_order[@]}"; do
if [[ -f "$SHADER_DIR/${name}.frag" || -f "$SHADER_DIR/${name}.glsl" ]]; then
available+=("$name")
fi
done

while IFS= read -r -d '' file; do
local base
base="$(basename "$file")"
base="${base%.*}"

local exists=0
for item in "${available[@]}"; do
if [[ "$item" == "$base" ]]; then
exists=1
break
fi
done
[[ $exists -eq 0 ]] && available+=("$base")
done < <(find "$SHADER_DIR" -maxdepth 1 \( -name "*.frag" -o -name "*.glsl" \) -print0 2>/dev/null)

printf '%s\n' "${available[@]}"
}

resolve_shader_file() {
local name="$1"
if [[ -f "$SHADER_DIR/${name}.frag" ]]; then
echo "$SHADER_DIR/${name}.frag"
elif [[ -f "$SHADER_DIR/${name}.glsl" ]]; then
echo "$SHADER_DIR/${name}.glsl"
else
echo ""
fi
}

format_title() {
local raw="$1"
if [[ "$raw" == "none" ]]; then
echo "Turn Off (Disabled)"
return
fi
if [[ "$raw" == "greyscale" ]]; then
echo "Grayscale"
return
fi
if [[ "$raw" =~ ^blue-light-filter-([0-9]+)$ ]]; then
echo "Blue Light Filter (${BASH_REMATCH[1]}%)"
return
fi
echo "$raw" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1'
}

cycle_filter() {
mapfile -t FILTERS < <(get_available_filters)

if (( ${#FILTERS[@]} <= 1 )); then
apply_shader ""
save_state "none"
notify "πŸ–₯️ $APP_NAME" "No shader files found in $SHADER_DIR" "dialog-warning" 2500
exit 0
fi

local current_path
current_path="$(get_active_shader)"
local current_filter="none"

if [[ -n "$current_path" ]]; then
local filename
filename="$(basename "$current_path")"
current_filter="${filename%.*}"
fi

local current_idx=0
for i in "${!FILTERS[@]}"; do
if [[ "${FILTERS[$i]}" == "$current_filter" ]]; then
current_idx=$i
break
fi
done

local next_idx=$(( (current_idx + 1) % ${#FILTERS[@]} ))
local next_filter="${FILTERS[$next_idx]}"

if [[ "$next_filter" == "none" ]]; then
apply_shader ""
save_state "none"
notify "πŸ–₯️ $APP_NAME" "Filter Disabled" "video-display" 1500
else
local target_file
target_file="$(resolve_shader_file "$next_filter")"
if [[ -n "$target_file" && -f "$target_file" ]]; then
apply_shader "$target_file"
save_state "$next_filter"
local display_name
display_name="$(format_title "$next_filter")"
notify "πŸ–₯️ $APP_NAME" "Active: $display_name" "video-display" 1500
else
apply_shader ""
save_state "none"
notify "πŸ–₯️ $APP_NAME" "Filter Disabled (Missing File)" "dialog-warning" 2000
fi
fi
}

rofi_menu() {
if ! command -v rofi >/dev/null 2>&1; then
echo "Error: rofi is not installed." >&2
exit 1
fi

mapfile -t available_raw < <(get_available_filters)
if (( ${#available_raw[@]} == 0 )); then
notify "πŸ–₯️ $APP_NAME" "No shader files found in $SHADER_DIR" "dialog-warning" 2500
exit 0
fi

local current_path
current_path="$(get_active_shader)"
local current_filter="none"
if [[ -n "$current_path" ]]; then
local filename
filename="$(basename "$current_path")"
current_filter="${filename%.*}"
fi

local rofi_lines=()
local mapping=()
local active_idx=0

for idx in "${!available_raw[@]}"; do
local name="${available_raw[$idx]}"
local display
display="$(format_title "$name")"

if [[ "$name" == "$current_filter" ]]; then
active_idx=$idx
display="βœ“ $display (Active)"
else
display=" $display"
fi

rofi_lines+=("$display")
mapping+=("$name")
done

local rofi_theme_arg=()
local rofi_dir="${XDG_CONFIG_HOME:-$HOME/.config}/rofi"
if [[ -f "$rofi_dir/config-compact.rasi" ]]; then
rofi_theme_arg=(-config "$rofi_dir/config-compact.rasi")
elif [[ -f "$rofi_dir/config-short.rasi" ]]; then
rofi_theme_arg=(-config "$rofi_dir/config-short.rasi")
elif [[ -f "$rofi_dir/config.rasi" ]]; then
rofi_theme_arg=(-config "$rofi_dir/config.rasi")
fi

local lines_count=${#rofi_lines[@]}
local selected_idx
selected_idx=$(printf '%s\n' "${rofi_lines[@]}" | rofi -dmenu -i -replace "${rofi_theme_arg[@]}" -no-show-icons -l "$lines_count" -p "Screen Filter" -selected-row "$active_idx" -format "i" 2>/dev/null || true)

if [[ -z "$selected_idx" ]]; then
exit 0
fi

local chosen="${mapping[$selected_idx]}"
if [[ "$chosen" == "none" ]]; then
apply_shader ""
save_state "none"
notify "πŸ–₯️ $APP_NAME" "Filter Disabled" "video-display" 1500
else
local target_file
target_file="$(resolve_shader_file "$chosen")"
if [[ -n "$target_file" && -f "$target_file" ]]; then
apply_shader "$target_file"
save_state "$chosen"
local display_name
display_name="$(format_title "$chosen")"
notify "πŸ–₯️ $APP_NAME" "Active: $display_name" "video-display" 1500
fi
fi
}

restore_state() {
if [[ -f "$STATE_FILE" ]]; then
local saved_filter
saved_filter=$(<"$STATE_FILE")
if [[ -n "$saved_filter" && "$saved_filter" != "none" ]]; then
local target_file
target_file="$(resolve_shader_file "$saved_filter")"
if [[ -n "$target_file" && -f "$target_file" ]]; then
apply_shader "$target_file"
exit 0
fi
fi
fi
apply_shader ""
}

case "${1:-cycle}" in
cycle|next)
cycle_filter
;;
-m|--menu|menu|rofi)
rofi_menu
;;
restore|init)
restore_state
;;
off|none|clear|disable)
apply_shader ""
save_state "none"
notify "πŸ–₯️ $APP_NAME" "Filter Disabled" "video-display" 1500
;;
status)
active="$(get_active_shader)"
if [[ -n "$active" ]]; then
echo "Active shader: $(basename "$active") ($active)"
else
echo "Active shader: None (Disabled)"
fi
;;
list)
echo "Available shaders in $SHADER_DIR:"
get_available_filters
;;
set|on)
if [[ -z "${2:-}" ]]; then
echo "Usage: $(basename "$0") set <shader_name>" >&2
exit 1
fi
target_file="$(resolve_shader_file "$2")"
if [[ -n "$target_file" ]]; then
apply_shader "$target_file"
save_state "$2"
display_name="$(format_title "$2")"
notify "πŸ–₯️ $APP_NAME" "Active: $display_name" "video-display" 1500
else
echo "Error: Shader '$2' not found in $SHADER_DIR" >&2
exit 1
fi
;;
-h|--help|help)
cat <<EOF
Usage: $(basename "$0") [COMMAND]

Commands:
cycle, next Cycle to the next available screen filter (default)
-m, --menu, rofi Open interactive Rofi shader picker
restore, init Restore saved shader state on system startup (silent)
off, disable Turn off screen shader
set <name> Directly enable a specific shader
status Show currently active shader
list List all discoverable shaders
help, -h Show this help message
EOF
;;
*)
echo "Unknown argument: $1. Use --help for usage." >&2
exit 1
;;
esac
43 changes: 43 additions & 0 deletions dotfiles/.config/hypr/shaders/blue-light-filter-25.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform sampler2D tex;
out vec4 fragColor;

const float temperature = 3000.0;
const float temperatureStrength = 0.25;

#define WithQuickAndDirtyLuminancePreservation
const float LuminancePreservationFactor = 1.0;

// function from https://www.shadertoy.com/view/4sc3D7
// valid from 1000 to 40000 K (and additionally 0 for pure full white)
vec3 colorTemperatureToRGB(const in float temperature) {
// values from: http://blenderartists.org/forum/showthread.php?270332-OSL-Goodness&p=2268693&viewfull=1#post2268693
mat3 m = (temperature <= 6500.0) ? mat3(vec3(0.0, -2902.1955373783176, -8257.7997278925690),
vec3(0.0, 1669.5803561666639, 2575.2827530017594),
vec3(1.0, 1.3302673723350029, 1.8993753891711275))
: mat3(vec3(1745.0425298314172, 1216.6168361476490, -8257.7997278925690),
vec3(-2666.3474220535695, -2173.1012343082230, 2575.2827530017594),
vec3(0.55995389139931482, 0.70381203140554553, 1.8993753891711275));
return mix(clamp(vec3(m[0] / (vec3(clamp(temperature, 1000.0, 40000.0)) + m[1]) + m[2]), vec3(0.0), vec3(1.0)),
vec3(1.0), smoothstep(1000.0, 0.0, temperature));
}

void main() {
vec4 pixColor = texture(tex, v_texcoord);

// RGB
vec3 color = vec3(pixColor[0], pixColor[1], pixColor[2]);

#ifdef WithQuickAndDirtyLuminancePreservation
color *= mix(1.0, dot(color, vec3(0.2126, 0.7152, 0.0722)) / max(dot(color, vec3(0.2126, 0.7152, 0.0722)), 1e-5),
LuminancePreservationFactor);
#endif

color = mix(color, color * colorTemperatureToRGB(temperature), temperatureStrength);

vec4 outCol = vec4(color, pixColor[3]);

fragColor = outCol;
}
Loading