diff --git a/etc/sway/config.d/40-autostart-applications.conf b/etc/sway/config.d/40-autostart-applications.conf index ff2fe24..45dd671 100644 --- a/etc/sway/config.d/40-autostart-applications.conf +++ b/etc/sway/config.d/40-autostart-applications.conf @@ -3,10 +3,14 @@ exec { $polkit_agent + $swappy_notify $dex_autostart + $wlsunset $autotiling + $term_server $cliphist_store $cliphist_watch + $autoname_workspaces $pcmanfm_daemon } exec_always { @@ -16,4 +20,5 @@ exec_always { $poweralertd $watch_playerctl $calendar_daemon + $import_gsettings } diff --git a/etc/sway/variables b/etc/sway/variables index 548dabb..4fe832c 100644 --- a/etc/sway/variables +++ b/etc/sway/variables @@ -21,6 +21,26 @@ set $menu fuzzel # Add --to-code to bindsym, support for non-latin layouts set $bindsym bindsym --to-code +# Volume notification +set $volume_bar /usr/share/sway/scripts/volume-notify.sh + +# Brightness notification +set $brightness_bar /usr/share/sway/scripts/brightness-notify.sh + +# brightness control +set $brightness_step bash -c 'echo $(( $(light -Mr) / 100 * 5 < 1 ? 1 : $(( $(light -Mr) / 100 * 5 )) ))' +set $brightness_up light -r -A $($brightness_step) && $brightness_bar +set $brightness_down light -r -U $($brightness_step) && $brightness_bar + +# Volume control +set $volume_down pulsemixer --change-volume -5 && $volume_bar +set $volume_up pulsemixer --change-volume +5 && $volume_bar +set $volume_mute pulsemixer --toggle-mute && $volume_bar + +# clipboard history +set $clipboard cliphist list | fuzzel -dmenu -p "Select item to copy" --lines 10 --width 35 | cliphist decode | wl-copy +set $clipboard-del cliphist list | fuzzel -dmenu -p "Select item to delete" --lines 10 --width 35 | cliphist delete + # Pulseaudo command set $pulseaudio $term_float pulsemixer @@ -54,12 +74,21 @@ set $screenshot_active $pipe_active | $swappy && $notify_paste # PolicyKit agent set $polkit_agent /usr/bin/mate-polkit & +# Night Light +set $wlsunset '[ -x "$(command -v wlsunset)" ] && /usr/share/sway/scripts/sunset.sh "on"' + # Autotiling script set $autotiling '[ -x "$(command -v autotiling)" ] && autotiling -w 1 3 5 7 9' +# Automatic workspace names +set $autoname_workspaces '[ -f /usr/share/sway/scripts/autoname-workspaces.py ] && /usr/share/sway/scripts/autoname-workspaces.py' + # restart kanshi https://github.com/emersion/kanshi/issues/43#issuecomment-531679213 set $kanshi '[ -x "$(command -v kanshi)" ] && pkill kanshi; exec kanshi' +# Workaround for applying GTK theme +set $import_gsettings '[ -f /usr/share/sway/scripts/import-gsettings.sh ] && /usr/share/sway/scripts/import-gsettings.sh' + # Dex autostart daemon set $dex_autostart '[ -x "$(command -v dex)" ] && gdbus wait --session org.kde.StatusNotifierWatcher && dex --autostart' diff --git a/usr/share/sway/scripts/autoname-workspaces.py b/usr/share/sway/scripts/autoname-workspaces.py new file mode 100755 index 0000000..6b9aafe --- /dev/null +++ b/usr/share/sway/scripts/autoname-workspaces.py @@ -0,0 +1,165 @@ +#!/usr/bin/python + +# This script requires i3ipc-python package (install it from a system package manager +# or pip). +# It adds icons to the workspace name for each open window. +# Set your keybindings like this: set $workspace1 workspace number 1 +# Add your icons to WINDOW_ICONS. +# Based on https://github.com/maximbaz/dotfiles/blob/master/bin/i3-autoname-workspaces + +import argparse +import i3ipc +import logging +import re +import signal +import sys + +WINDOW_ICONS = { + "audacious": "", + "azote": "", + "balena-etcher": "", + "Chromium": "", + "chromium": "", + "code": "", + "discord": "", + "engrampa": "", + "firefox": "", + "foot": "", + "gimp-2.10": "", + "gimp-2.99": "", + "Google-chrome": "", + "google-chrome": "", + "gpk-application": "", + "libreoffice-startcenter": "", + "libreoffice-writer": "", + "libreoffice-calc": "", + "libreoffice-impress": "", + "libreoffice-draw": "", + "lximage-qt": "", + "mate-calc": "", + "mpv": "", + "nheko": "", + "nwg-look": "", + "nwg-displays": "", + "org.telegram.desktop": "", + "org.qbittorrent.qBittorrent": "", + "org.pwmt.zathura": "", + "org.inkscape.inkscape": "", + "pavucontrol": "", + "pluma": "", + "pcmanfm": "", + "simple-scan": "", + "software-properties-gtk": "", + "steam": "", + "sway-input-config": "", + "telegram": "", + "Thunderbird": "", + "thunderbird": "", + "transmission-gtk": "", + "vlc": "" +} + +DEFAULT_ICON = "" + + +def icon_for_window(window): + name = None + if window.app_id is not None and len(window.app_id) > 0: + name = window.app_id.lower() + elif window.window_class is not None and len(window.window_class) > 0: + name = window.window_class.lower() + + if name in WINDOW_ICONS: + return WINDOW_ICONS[name] + + logging.info("No icon available for window with name: %s" % str(name)) + return DEFAULT_ICON + + +def rename_workspaces(ipc): + for workspace in ipc.get_tree().workspaces(): + name_parts = parse_workspace_name(workspace.name) + icon_tuple = () + for w in workspace: + if w.app_id is not None or w.window_class is not None: + icon = icon_for_window(w) + if not ARGUMENTS.duplicates and icon in icon_tuple: + continue + icon_tuple += (icon,) + name_parts["icons"] = " ".join(icon_tuple) + " " + new_name = construct_workspace_name(name_parts) + ipc.command('rename workspace "%s" to "%s"' % (workspace.name, new_name)) + + +def undo_window_renaming(ipc): + for workspace in ipc.get_tree().workspaces(): + name_parts = parse_workspace_name(workspace.name) + name_parts["icons"] = None + new_name = construct_workspace_name(name_parts) + ipc.command('rename workspace "%s" to "%s"' % (workspace.name, new_name)) + ipc.main_quit() + sys.exit(0) + + +def parse_workspace_name(name): + return re.match( + "(?P[0-9]+):?(?P\w+)? ?(?P.+)?", name + ).groupdict() + + +def construct_workspace_name(parts): + new_name = str(parts["num"]) + if parts["shortname"] or parts["icons"]: + new_name += ":" + + if parts["shortname"]: + new_name += parts["shortname"] + + if parts["icons"]: + new_name += " " + parts["icons"] + + return new_name + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="This script automatically changes the workspace name in sway depending on your open applications." + ) + parser.add_argument( + "--duplicates", + "-d", + action="store_true", + help="Set it when you want an icon for each instance of the same application per workspace.", + ) + parser.add_argument( + "--logfile", + "-l", + type=str, + default="/tmp/sway-autoname-workspaces.log", + help="Path for the logfile.", + ) + args = parser.parse_args() + global ARGUMENTS + ARGUMENTS = args + + logging.basicConfig( + level=logging.INFO, + filename=ARGUMENTS.logfile, + filemode="w", + format="%(message)s", + ) + + ipc = i3ipc.Connection() + + for sig in [signal.SIGINT, signal.SIGTERM]: + signal.signal(sig, lambda signal, frame: undo_window_renaming(ipc)) + + def window_event_handler(ipc, e): + if e.change in ["new", "close", "move"]: + rename_workspaces(ipc) + + ipc.on("window", window_event_handler) + + rename_workspaces(ipc) + + ipc.main() diff --git a/usr/share/sway/scripts/brightness-notify.sh b/usr/share/sway/scripts/brightness-notify.sh new file mode 100755 index 0000000..604d56c --- /dev/null +++ b/usr/share/sway/scripts/brightness-notify.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +VALUE=$(light -G | cut -d'.' -f1) +TEXT="Brightness: ${VALUE}%" + +notify-send \ + --expire-time 800 \ + --hint string:x-canonical-private-synchronous:brightness \ + --hint "int:value:$VALUE" \ + --hint "int:transient:1" \ + "${TEXT}" diff --git a/usr/share/sway/scripts/first-empty-workspace.py b/usr/share/sway/scripts/first-empty-workspace.py new file mode 100755 index 0000000..82b3505 --- /dev/null +++ b/usr/share/sway/scripts/first-empty-workspace.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +from argparse import ArgumentParser + +import i3ipc + +# Assumption: it exists 10 workspaces (otherwise, change this value) +NUM_WORKSPACES = 10 + +if __name__ == "__main__": + + arguments_parser = ArgumentParser() + arguments_parser.add_argument('-s', + '--switch', + action='store_true', + help='switch to the first empty workspace' + ) + arguments_parser.add_argument('-m', + '--move', + action='store_true', + help='move the currently focused container to the first empty workspace' + ) + arguments = arguments_parser.parse_args() + assert (arguments.switch or arguments.move) # at least one of the flags must be specification + + ipc = i3ipc.Connection() + tree = ipc.get_tree() + current_workspace = tree.find_focused().workspace() + workspaces = tree.workspaces() # includes current_workspace + + workspace_numbers = [workspace.num for workspace in workspaces] + empty_workspace_numbers = set([number for number in range(1, NUM_WORKSPACES + 1)]) - set(workspace_numbers) + # Take into consideration that the current workspace exists but might be empty + if len(current_workspace.nodes) == 0: + empty_workspace_numbers.add(current_workspace.num) + + # Get the minor empty workspaces number (or set it as the current workspaces number if all are busy) + first_empty_workspace_number = current_workspace.num + if empty_workspace_numbers: + first_empty_workspace_number = min(empty_workspace_numbers) + + # Use the value of first_empty_workspace_number to make the requested actions + if arguments.move and arguments.switch: + # Avoid wallpaper flickering when moving and switching by specifying both actions in the same Sway's command + reply = ipc.command( + "move container to workspace number {}, workspace number {}".format(first_empty_workspace_number, + first_empty_workspace_number)) + assert reply[0].success # exit with non-zero status if the assertion fails + elif arguments.switch: + reply = ipc.command("workspace number {}".format(first_empty_workspace_number)) + assert reply[0].success # exit with non-zero status if the assertion fails + elif arguments.move: + reply = ipc.command("move container to workspace number {}".format(first_empty_workspace_number)) + assert reply[0].success # exit with non-zero status if the assertion fails diff --git a/usr/share/sway/scripts/import-gsettings.sh b/usr/share/sway/scripts/import-gsettings.sh new file mode 100755 index 0000000..f98010c --- /dev/null +++ b/usr/share/sway/scripts/import-gsettings.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# usage: import-gsettings +config="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" +if [ ! -f "$config" ]; then exit 1; fi + +# Color scheme for GTK apps can be 0 (light, default) and 1 (dark) +color_scheme="$(grep 'gtk-application-prefer-dark-theme' "$config" | sed 's/.*\s*=\s*//')" +cursor_theme="$(grep 'gtk-cursor-theme-name' "$config" | sed 's/.*\s*=\s*//')" +font_name="$(grep 'gtk-font-name' "$config" | sed 's/.*\s*=\s*//')" +gnome_schema="org.gnome.desktop.interface" +gnome_wm_schema="org.gnome.desktop.wm.preferences" +gtk_theme="$(grep 'gtk-theme-name' "$config" | sed 's/.*\s*=\s*//')" +icon_theme="$(grep 'gtk-icon-theme-name' "$config" | sed 's/.*\s*=\s*//')" + +gsettings set "$gnome_schema" gtk-theme "$gtk_theme" +gsettings set "$gnome_schema" icon-theme "$icon_theme" +gsettings set "$gnome_schema" cursor-theme "$cursor_theme" +gsettings set "$gnome_schema" font-name "$font_name" +gsettings set "$gnome_wm_schema" button-layout "" + +if [ "$color_scheme" -eq 1 ]; then + gsettings set "$gnome_schema" color-scheme "prefer-dark" +else + gsettings set "$gnome_schema" color-scheme "prefer-light" +fi diff --git a/usr/share/sway/scripts/recorder.sh b/usr/share/sway/scripts/recorder.sh new file mode 100755 index 0000000..5b77da1 --- /dev/null +++ b/usr/share/sway/scripts/recorder.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -x + +pgrep wf-recorder +status=$? + +countdown() { + notify "Recording in 3 seconds" -t 1000 + sleep 1 + notify "Recording in 2 seconds" -t 1000 + sleep 1 + notify "Recording in 1 seconds" -t 1000 + sleep 1 +} + +notify() { + line=$1 + shift + notify-send "Recording" "${line}" -i /usr/share/icons/Yaru/scalable/devices/camera-video-symbolic.svg "$*" +} + +if [ $status != 0 ]; then + target_path=$(xdg-user-dir VIDEOS) + timestamp=$(date +'recording_%Y%m%d-%H%M%S') + + notify "Select a region to record" -t 1000 + area=$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp) + + countdown + (sleep 0.5 && pkill -RTMIN+8 waybar) & + + if [ "$1" == "-a" ]; then + file="$target_path/$timestamp.mp4" + wf-recorder --audio -g "$area" --file="$file" + else + file="$target_path/$timestamp.webm" + wf-recorder -g "$area" -c libvpx --codec-param="qmin=0" --codec-param="qmax=25" --codec-param="crf=4" --codec-param="b:v=1M" --file="$file" + fi + + pkill -RTMIN+8 waybar && notify "Finished recording ${file}" +else + pkill --signal SIGINT wf-recorder + pkill -RTMIN+8 waybar +fi diff --git a/usr/share/sway/scripts/scratchpad.sh b/usr/share/sway/scripts/scratchpad.sh new file mode 100755 index 0000000..f9329f8 --- /dev/null +++ b/usr/share/sway/scripts/scratchpad.sh @@ -0,0 +1,15 @@ +#!/bin/bash +tooltip=$(swaymsg -r -t get_tree | jq -r 'recurse(.nodes[]) | first(select(.name=="__i3_scratch")) | .floating_nodes | .[] | "\(.app_id) | \(.name)"') +count=$(echo -n "$tooltip" | grep -c '^') + +if [ "$count" -eq 0 ]; then + exit 1 +elif [ "$count" -eq 1 ]; then + class="one" +elif [ "$count" -gt 1 ]; then + class="many" +else + class="unknown" +fi + +printf '{"text":"%s", "class":"%s", "alt":"%s", "tooltip":"%s"}\n' "$count" "$class" "$class" "${tooltip//$'\n'/'\n'}" diff --git a/usr/share/sway/scripts/screenshot-notify.sh b/usr/share/sway/scripts/screenshot-notify.sh new file mode 100755 index 0000000..17884e6 --- /dev/null +++ b/usr/share/sway/scripts/screenshot-notify.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e +DIR=${XDG_PICTURES_DIR:-$HOME/Pictures} + +while true; do + mkdir -p "$DIR" && inotifywait -q -e create "$DIR" --format '%w%f' | xargs notify-send "Screenshot saved" +done diff --git a/usr/share/sway/scripts/sunset.sh b/usr/share/sway/scripts/sunset.sh new file mode 100755 index 0000000..6e7bd2d --- /dev/null +++ b/usr/share/sway/scripts/sunset.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +#Startup function +function start() { + [[ -f "$HOME/.config/wlsunset/config" ]] && source "$HOME/.config/wlsunset/config" + temp_low=${temp_low:-"4000"} + temp_high=${temp_high:-"6500"} + duration=${duration:-"900"} + sunrise=${sunrise:-"07:00"} + sunset=${sunset:-"19:00"} + location=${location:-"on"} + fallback_longitude=${fallback_longitude:-"8.7"} + fallback_latitude=${fallback_latitude:-"50.1"} + + if [ "${location}" = "on" ]; then + if [[ -z ${longitude+x} ]] || [[ -z ${latitude+x} ]]; then + GEO_CONTENT=$(curl -sL https://freegeoip.live/json/) + fi + longitude=${longitude:-$(echo "$GEO_CONTENT" | jq '.longitude // empty')} + longitude=${longitude:-$fallback_longitude} + latitude=${latitude:-$(echo "$GEO_CONTENT" | jq '.latitude // empty')} + latitude=${latitude:-$fallback_latitude} + + echo longitude: "$longitude" latitude: "$latitude" + + wlsunset -l "$latitude" -L "$longitude" -t "$temp_low" -T "$temp_high" -d "$duration" & + else + wlsunset -t "$temp_low" -T "$temp_high" -d "$duration" -S "$sunrise" -s "$sunset" & + fi +} + +#Accepts managing parameter +case $1'' in +'off') + pkill wlsunset + ;; + +'on') + start + ;; + +'toggle') + if pkill -0 wlsunset; then + pkill wlsunset + else + start + fi + ;; +'check') + command -v wlsunset + exit $? + ;; +esac + +#Returns a string for Waybar +if pkill -0 wlsunset; then + class="on" + tooltip="Night Color mode: enabled" +else + class="off" + tooltip="Night Color mode: disabled" +fi + +printf '{"alt":"%s", "tooltip":"%s"}\n' "$class" "$tooltip" diff --git a/usr/share/sway/scripts/volume-notify.sh b/usr/share/sway/scripts/volume-notify.sh new file mode 100755 index 0000000..07a8ec2 --- /dev/null +++ b/usr/share/sway/scripts/volume-notify.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +VOLUME=$(pulsemixer --get-volume) +# get first percent value +VOLUME=${VOLUME%%%*} +VOLUME=${VOLUME##* } + +TEXT="Volume: ${VOLUME}%" +case $(pulsemixer --get-mute) in + *1) + TEXT="Volume: muted" + VOLUME=0 + ;; +esac + +notify-send \ + --app-name sway \ + --expire-time 800 \ + --hint string:x-canonical-private-synchronous:volume \ + --hint "int:value:$VOLUME" \ + --hint "int:transient:1" \ + "${TEXT}"