diff --git a/outerheaven.init3.home/.config/sway/sway/10-variables.conf b/outerheaven.init3.home/.config/sway/sway/10-variables.conf new file mode 100644 index 0000000..f8ff6cb --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/10-variables.conf @@ -0,0 +1,14 @@ +### Variables +# key bindings, preferred applications +# NOTE: Mod1 = alt, Mod4 = Super key +set $mod Mod4 +#set $mod Mod1 +set $left Left +set $down Down +set $up Up +set $right Right + +set $menu exec fuzzel --log-level=error --log-no-syslog +set $term kitty --directory $HOME +#set $term tilix -w $HOME +#set $term alacritty --working-directory ${HOME} diff --git a/outerheaven.init3.home/.config/sway/sway/11-colors.conf b/outerheaven.init3.home/.config/sway/sway/11-colors.conf new file mode 100644 index 0000000..c05f170 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/11-colors.conf @@ -0,0 +1,80 @@ +# Constants +# note: black was claimed as 'transparent' in the source where this was found +# +# nord colors from here: https://www.nordtheme.com/docs/colors-and-palettes +set { + $white #ffffff + $cyan #00afd7 + $acqua #00d787 + $wine #72003e + $magenta #af005f + $orange #ff8700 + $silver #e4e4e4 + $elegant #1b1d1e + $black #000000 + $nord0 #2e3440 + $nord1 #3b4252 + $nord2 #434c5e + $nord3 #4c566a + $nord4 #d8dee9 + $nord5 #e5e9f0 + $nord6 #eceff4 + $nord7 #8fbcbb + $nord8 #88c0d0 + $nord9 #81a1c1 + $nord10 #5e81ac + $nord11 #bf616a + $nord12 #d08770 + $nord13 #ebcb8b + $nord14 #a3be8c + $nord15 #b48ead +} + +# format: +# client. [ []] +# +# The default colors are: +# ┌──────────────────┬─────────┬────────────┬─────────┬───────────┬──────────────┐ +# │ class │ border │ background │ text │ indicator │ child_border │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │background │ n/a │ #ffffff │ n/a │ n/a │ n/a │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │focused │ #4c7899 │ #285577 │ #ffffff │ #2e9ef4 │ #285577 │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │focused_inactive │ #333333 │ #5f676a │ #ffffff │ #484e50 │ #5f676a │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │focused_tab_title │ #333333 │ #5f676a │ #ffffff │ n/a │ n/a │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │unfocused │ #333333 │ #222222 │ #888888 │ #292d2e │ #222222 │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │urgent │ #2f343a │ #900000 │ #ffffff │ #900000 │ #900000 │ +# ├──────────────────┼─────────┼────────────┼─────────┼───────────┼──────────────┤ +# │placeholder │ #000000 │ #0c0c0c │ #ffffff │ #000000 │ #0c0c0c │ +# └──────────────────┴─────────┴────────────┴─────────┴───────────┴──────────────┘ + +# Dracula colors: +# client.focused #6272A4 #6272A4 #F8F8F2 #6272A4 #6272A4 +# client.focused_inactive #44475A #44475A #F8F8F2 #44475A #44475A +# client.unfocused #282A36 #282A36 #BFBFBF #282A36 #282A36 +# +# Garuda (distribution) colors: +# client.focused #608080 $nord2 $nord6 $nord7 $nord8 +# client.focused_inactive #608080 $nord0 $nord4 $nord3 $nord3 +# client.unfocused #608080 $nord0 $nord4 $nord3 $nord3 +# client.urgent #608080 $nord13 $nord0 $nord7 $nord13 + + +# last-used: +# Garuda but tweaked: diff borders, nord alert/yellow +#client.background $black +#client.focused $nord2 $nord2 $nord6 $nord7 $nord8 +#client.focused_inactive $nord3 $nord0 $nord4 $nord3 $nord3 +#client.unfocused $nord3 $nord0 $nord4 $nord3 $nord3 +#client.urgent $nord12 $nord13 $nord0 $nord7 $nord13 + +# xfce kind of look, from https://github.com/ayamir/nord-and-light/blob/master/.config/sway/config +# client.focused $nord10 $nord10 $nord1 $nord1 $nord10 +# client.focused_inactive $nord5 $nord5 $nord1 $nord5 $nord2 +# client.unfocused $nord5 $nord5 $nord1 $nord5 $nord2 +# client.urgent #7c818c #bf616a $nord1 #900000 #bf616a +# client.placeholder $black #0c0c0c $nord1 $black #0c0c0c diff --git a/outerheaven.init3.home/.config/sway/sway/12-displays.conf b/outerheaven.init3.home/.config/sway/sway/12-displays.conf new file mode 100644 index 0000000..b696c7a --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/12-displays.conf @@ -0,0 +1,55 @@ +## workspace setup +workspace 1 output $MON_CENTER +workspace 2 output $MON_RIGHT +workspace 3 output $MON_LEFT +workspace 4 output $MON_RIGHT + +## configure displays +# use the following page to identify the correct subpixel rendering methods +# http://www.lagom.nl/lcd-test/subpixel.php +# query display info: 'swaymsg -t get_outputs' +# important notes: +# adaptive sync will cause some displays to flicker +# 10-bit color breaks sharing of *that* display with 'xdg-desktop-portal-wlr' +# negative coords will make menus and some XWayland things behave poorly: base left-most display on 0,0 + +# layout with 4k/center display at native res/scaling: +output $MON_CENTER { + position 1440 400 + resolution 3840x2160@159.975Hz + subpixel rgb +# render_bit_depth 10 +# adaptive_sync on +# max_render_time 6 +} + +# trying left monitor with lower refresh rate, it's a little flaky going back to gdm +output $MON_LEFT { + position 0 0 + resolution 2560x1440@165.000Hz + subpixel vrgb + transform 270 +# adaptive_sync on +# max_render_time 7 +} + +output $MON_RIGHT { + position 5280 400 + resolution 3840x2160@159.975Hz + subpixel rgb +# max_render_time 6 +} + +# for mouse tracking in XWayland/games, ensure the center display is marked primary +exec_always xrandr --output $MON_CENTER --primary + +# enable adaptive sync on capable displays - incapable are gracefully handled +# output * adaptive_sync on +# enabling here warrants enabling in '/etc/sway/sddm-greeter.config' as well; remember ~/git/workstation/ copy (if still relevant) + +# random-wallpaper script; using 'exec_always' allows for a new wallpaper on each config load +exec_always ~/.config/sway/scripts/wallpaper.py --select unique ~/Pictures/wallpapers/mac +# change '--mode single' to '--mode multiple' for a unique image per display + +# don't let floating windows get ridiculously large +floating_maximum_size 1920 x 1080 diff --git a/outerheaven.init3.home/.config/sway/sway/13-assignments.conf b/outerheaven.init3.home/.config/sway/sway/13-assignments.conf new file mode 100644 index 0000000..1b8403c --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/13-assignments.conf @@ -0,0 +1,67 @@ +## config for assigning applications to specific workspaces/displays (see 'display.cfg') +# see 'man 5 sway' CRITERIA for qualifying criteria + +# ensure unigine benchmarks run on main display +assign [title="Unigine*" app_id=""] workspace 1 +assign [title="Steam Sign In" class="^(?i)Steam"] workspace 1 + +#assign [app_id="work"] workspace work +#assign [class="Virt-manager"] workspace control +#assign [app_id="control"] workspace control +assign [app_id="control"] workspace 3 +assign [app_id="scratch"] workspace 3 + +# workspace 1/center/main monitor assignment +# assign the pcsx2 game window here - main window goes to ws3 below +assign [class="PCSX2" title="^.*(nterlaced|rogressive).*$"] workspace 1 + +# workspace 2/right monitor assignment +assign [app_id="kitty" title="stats"] workspace 2 +assign [app_id="nethogs"] workspace 2 +assign [app_id="btm"] workspace 2 +assign [app_id="ticker"] workspace 2 +assign [app_id="deluge"] workspace 2 +assign [app_id="Trezor Suite"] workspace 2 +assign [title="Cryptowatch"] workspace 2 +assign [app_id="mpv"] workspace 2 +assign [app_id="CiscoCollabHost"] workspace 2 +assign [app_id="audacious"] workspace 2 +assign [app_id="com.belmoussaoui.Authenticator"] workspace 2 +assign [app_id="gnome-system-monitor"] workspace 2 +assign [app_id="imv"] workspace 2 +assign [app_id="org.strawberrymusicplayer.strawberry"] workspace 2 +assign [app_id="rhythmbox"] workspace 2 +assign [app_id="python" title="^(?i)vorta.*$"] workspace 2 +# vscodium, weird name - commented out, like it to go wherever when not (fractional) scaling 4k display on wayland +#assign [app_id="codium-url-handler"] workspace 2 + +# workspace 3/left (vertical) monitor assignment +assign [app_id="pavucontrol"] workspace 3 +assign [app_id="thunderbird"] workspace 3 +assign [app_id="evolution"] workspace 3 +assign [app_id="org.kde.quassel"] workspace 3 +assign [app_id="[Dd]iscord"] workspace 3 +assign [class="[Dd]iscord"] workspace 3 +# two ways for Slack, xwayland and native wayland - also covers non-native app (eg: browser) +assign [app_id="Slack"] workspace 3 +assign [title="Slack.*"] workspace 3 +assign [app_id="Element"] workspace 3 +assign [title="Element.*" app_id=""] workspace 3 +assign [class="[Ss]ignal"] workspace 3 +assign [app_id="[Ss]ignal"] workspace 3 +assign [title="Wine System Tray" class="steam_app_.*$"] workspace 3 +# move main pcsx2 window and the log to the third/left-most workspace +assign [class="PCSX2" title="PCSX2 .*$"] workspace 3 +assign [class="PCSX2" title="PCSX2 Program Log"] workspace 3 +assign [app_id="transmission-gtk"] workspace 3 + +# move work-related firefox windows to 'work' workspace, #4 +assign [app_id="work-firefox"] workspace 4 + +# move steam windows around +# want it in the default workspace of another display +# on xwayland Steam windows render at 1 FPS if *all* windows aren't visible +# fixed in 2023 beta update +# assign [title="Steam Big Picture Mode" class="[Ss]team"] workspace 1 +assign [title="Friends List.*" class="[Ss]team"] workspace 3 +# assign [title="[Ss]team" class="[Ss]team"] workspace 2 diff --git a/outerheaven.init3.home/.config/sway/sway/14-input.conf b/outerheaven.init3.home/.config/sway/sway/14-input.conf new file mode 100644 index 0000000..f854753 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/14-input.conf @@ -0,0 +1,33 @@ +## input cfg; eg: mouse, keyboard, touchpad +# see `man 5 sway-input` for more information +# query: swaymsg -t get_inputs + +# specific devices are used in an attempt to fix Firefox crashing on config reloads +# presumably due to it capturing a bunch of devices that aren't related +# alts: +# set $mouse type:pointer +# set $keyboard type:keyboard + +# make keyboard go faaast, turn on numlock, and set US layout +# input "9456:8346:Metadot_-_Das_Keyboard_DK5QS" { +input "9456:8353:Das_Keyboard_Das_Keyboard_6_Pro" { + repeat_delay 250 + repeat_rate 40 + xkb_numlock enabled + xkb_layout us +} + +input "1133:16519:Logitech_G903_LS" { +# disable mouse acceleration (enabled by default; to set it manually, use "adaptive" instead of "flat") + accel_profile "flat" +# set mouse sensitivity (between -1 and 1) + pointer_accel 0.0 +} + +# try to cater to laptops with touchpads +input "type:touchpad" { + left_handed enabled + tap enabled + natural_scroll disabled + dwt enabled +} diff --git a/outerheaven.init3.home/.config/sway/sway/15-keybinds.conf b/outerheaven.init3.home/.config/sway/sway/15-keybinds.conf new file mode 100644 index 0000000..703260a --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/15-keybinds.conf @@ -0,0 +1,147 @@ +### Key bindings +# +# Basics: +# +# + # Start a terminal + bindsym $mod+Return exec $term + bindsym $mod+KP_Enter exec $term + + # Kill focused window + bindsym $mod+Shift+q kill + + # Start your launcher + bindsym $mod+d exec $menu + + # Reload the configuration file + bindsym $mod+Shift+c reload + + # logout + bindsym $mod+Shift+e exec swaynag -t warning -m 'Do you really want to log out?' -b 'Yes, exit sway' 'swaymsg exit' + + # lock screen - alt+l and super+l + bindsym --no-repeat Mod1+l exec ${HOME}/.config/sway/scripts/locker.py + bindsym --no-repeat Mod4+l exec ${HOME}/.config/sway/scripts/locker.py +# +# Moving around: +# + # Move focus around + bindsym $mod+$left focus left + bindsym $mod+$down focus down + bindsym $mod+$up focus up + bindsym $mod+$right focus right + # Move windows around + bindsym $mod+Shift+$left move left + bindsym $mod+Shift+$down move down + bindsym $mod+Shift+$up move up + bindsym $mod+Shift+$right move right + # Cycle between workspaces + bindsym $mod+control+Right workspace next + bindsym $mod+control+Left workspace prev + +# +# Workspaces: +# + # Switch to workspace + bindsym $mod+1 workspace 1 + bindsym $mod+2 workspace 2 + bindsym $mod+3 workspace 3 + bindsym $mod+4 workspace 4 + bindsym $mod+5 workspace 5 + bindsym $mod+6 workspace 6 + bindsym $mod+7 workspace 7 + bindsym $mod+8 workspace 8 + bindsym $mod+9 workspace 9 + bindsym $mod+0 workspace 10 + # Move focused container to workspace + bindsym $mod+Shift+1 move container to workspace 1 + bindsym $mod+Shift+2 move container to workspace 2 + bindsym $mod+Shift+3 move container to workspace 3 + bindsym $mod+Shift+4 move container to workspace 4 + bindsym $mod+Shift+5 move container to workspace 5 + bindsym $mod+Shift+6 move container to workspace 6 + bindsym $mod+Shift+7 move container to workspace 7 + bindsym $mod+Shift+8 move container to workspace 8 + bindsym $mod+Shift+9 move container to workspace 9 + bindsym $mod+Shift+0 move container to workspace 10 + # Note: workspaces can have any name you want, not just numbers. + # We just use 1-10 as the default. +# +# Layout stuff: +# + # You can "split" the current object of your focus with + # $mod+b or $mod+v, for horizontal and vertical splits + # respectively. + bindsym $mod+b splith + bindsym $mod+v splitv + + # Switch the current container between different layout styles + bindsym $mod+s layout stacking + bindsym $mod+w layout tabbed + bindsym $mod+e layout toggle split + + # Make the current focus fullscreen + bindsym $mod+f fullscreen + + # Toggle the current focus between tiling and floating mode + bindsym $mod+Shift+space floating toggle + + # Swap focus between the tiling area and the floating area + bindsym $mod+space focus mode_toggle + + # Move focus to the parent container + bindsym $mod+a focus parent +# +# Scratchpad: +# + # Sway has a "scratchpad", which is a bag of holding for windows. + # You can send windows there and get them back later. + + # Move the currently focused window to the scratchpad + bindsym $mod+Shift+minus move scratchpad + + # Show the next scratchpad window or hide the focused scratchpad window. + # If there are multiple scratchpad windows, this command cycles through them. + bindsym $mod+minus scratchpad show +# +# Resizing containers: +# +mode "resize" { + # left will shrink the containers width + # right will grow the containers width + # up will shrink the containers height + # down will grow the containers height + bindsym $left resize shrink width 20px + bindsym $down resize grow height 20px + bindsym $up resize shrink height 20px + bindsym $right resize grow width 20px + + # Return to default mode + bindsym Return mode "default" + bindsym Escape mode "default" +} +bindsym $mod+r mode "resize" + +# +# Screenshots: +# + bindsym --no-repeat Print exec "~/.config/sway/scripts/screenshot.py region" + bindsym --no-repeat Shift+Print exec "~/.config/sway/scripts/screenshot.py window" + +# +# Media Control: +# note: only coverage for the two media keys and knob on the Das 5QS keyboard +# for the rest, see: +# https://wiki.archlinux.org/title/Sway#Custom_keybindings +# first, set up some aliases for brevity +# ... expect title/body to follow as two more strings + set $media-notify exec notify-send --urgency normal --expire-time 1000 --transient + bindsym --locked XF86AudioPlay $media-notify --icon /usr/share/icons/breeze-dark/status/22/media-playback-playing.svg 'Media Playback' 'Play state toggled'; exec playerctl play-pause + bindsym --locked XF86AudioNext $media-notify --icon /usr/share/icons/breeze-dark/actions/32/go-next.svg 'Media Playback' 'Skipped to the next track'; exec playerctl next +# bindsym --locked XF86AudioRaiseVolume exec ~/.config/sway/scripts/volume.py raise 1 +# bindsym --locked XF86AudioLowerVolume exec ~/.config/sway/scripts/volume.py lower 1 +# weird problem with knobs on the DAC and keyboard competing for this keybind, even though the DAC should only manage itself... +# instead, bind them to exec echo to prevent '438u' spam + bindsym XF86AudioRaiseVolume nop + bindsym XF86AudioLowerVolume nop + bindsym --locked XF86AudioMute $media-notify --icon /usr/share/icons/breeze-dark/status/22/audio-volume-muted.svg 'Volume adjustment' 'Mute toggled'; exec pactl set-sink-mute @DEFAULT_SINK@ toggle diff --git a/outerheaven.init3.home/.config/sway/sway/16-for_windows.conf b/outerheaven.init3.home/.config/sway/sway/16-for_windows.conf new file mode 100644 index 0000000..a0fb465 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/16-for_windows.conf @@ -0,0 +1,84 @@ +## per-window preferences +# eg: title format, floating, opacity +# +# add app_id to window title +# styling requires pango be enabled in the font of the main config +# for_window [title="."] title_format "%title (%app_id)" +# +# very-generic titles will benefit from anchors (^...$) - it's effectively a wildcard otherwise +# +# for_window [shell="xdg_shell"] title_format "%title (%app_id)" +# commented out the above to only expose xwayland things and reduce repetition in titles +for_window [shell="xwayland"] title_format " %title [X11] " + +# added 3/17/2023: enable CSDs for floating windows - client side decorations +for_window [floating] border csd; sticky enable + +# make polkit, file managers, and other dialog prompts float +# - no longer a fullscreen-ish window +# - 'sticky'; follows across workspaces +# - has csd for potential shadows/etc +# gnome polkit segfaults sometimes when needed (eg: stopping a service as regular user), don't use it +for_window [app_id="xfce-polkit"] floating enable; sticky enable +for_window [app_id="lxqt-policykit-agent"] floating enable; sticky enable +for_window [app_id="thunar"] floating enable; sticky enable; border normal +for_window [app_id="solaar"] floating enable; sticky enable; border normal +for_window [app_id="nm-connection-editor"] floating enable; sticky enable; border csd + +# common things that should float, also made sticky +# snippet from gentoo wiki for Sway: https://wiki.gentoo.org/wiki/Sway#Automatic_floating_windows +for_window [window_role="pop-up"] floating enable; sticky enable +for_window [window_role="bubble"] floating enable; sticky enable +for_window [window_role="dialog"] floating enable; sticky enable +for_window [window_role="task_dialog"] floating enable; sticky enable +for_window [window_role="About"] floating enable; sticky enable +for_window [window_type="menu"] floating enable; sticky enable +for_window [window_type="dialog"] floating enable; sticky enable +for_window [app_id="org.kde.elisa" title="Configure.*$"] sticky enable; floating enable +for_window [app_id="albert"] floating enable; border none +for_window [app_id="com.github.gi_lom.dialect"] floating enable +# make Discord file open windows only float - specifically cased app_id and no title +for_window [app_id="Discord" title=""] floating enable +for_window [app_id="org.gnome.Calculator"] floating enable +# for_window [app_id="org.gnome.Calendar"] floating enable +for_window [app_id="org.kde.dolphin"] floating enable; border normal +for_window [app_id="org.mozilla.firefox" title="^About( Mozilla)? Firefox$"] floating enable; sticky enable; border normal +for_window [app_id="org.mozilla.firefox" title="^Library$"] floating enable; sticky enable; border normal +for_window [app_id="thunderbird" title="^About Mozilla Thunderbird$"] floating enable; sticky enable +for_window [class="XEyes" title="xeyes"] floating enable; border none + +# make the main pcsx2 window float -- the double spacing is significant +for_window [class="PCSX2" title="PCSX2 .*$"] floating enable + +# make mpv / other windows sticky, appear on any workspace on that output +for_window [app_id=mpv] sticky enable; border csd + +# get rid of annoying webex popup +for_window [app_id="org.mozilla.firefox" title="Firefox — Sharing Indicator"] kill + +# mark wayland/xwayland browser windows, to inhibit idle when fullscreen +#for_window [class="Chromium-browser"] mark Browser +#for_window [class="Brave-browser"] mark Browser +#for_window [class="firefox"] mark Browser +#for_window [class="work-firefox"] mark Browser +#for_window [app_id="Chromium-browser"] mark Browser +#for_window [app_id="brave-browser"] mark Browser +#for_window [app_id="firefox"] mark Browser +#for_window [app_id="org.mozilla.firefox"] mark Browser +#for_window [app_id="work-firefox"] mark Browser + +# CS2 is floating for some reason, stop it +for_window [class="cs2" title="Counter-Strike 2"] floating disable; fullscreen enable; border none; max_render_time off + +# inhibit scrensaver for fullscreen browser windows +#for_window [con_mark="Browser"] { +# inhibit_idle fullscreen +# max_render_time off +#} + +# misc Steam fixes, also from Gentoo wiki (link above) +for_window [class="^Steam$" title="^Settings$"] floating enable; sticky enable +for_window [class="steam" title="Steam Settings"] floating enable; sticky enable +for_window [class="^Steam$" title="^Steam - Self Updater$"] floating enable; sticky enable +for_window [class="^Steam$" title="^Screenshot Uploader$"] floating enable; sticky enable +for_window [class="^Steam$" title="^Steam Guard - Computer Authorization Required$"] floating enable; sticky enable diff --git a/outerheaven.init3.home/.config/sway/sway/50-rules-browser.conf b/outerheaven.init3.home/.config/sway/sway/50-rules-browser.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/60-bindings-media.conf b/outerheaven.init3.home/.config/sway/sway/60-bindings-media.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/60-bindings-screenshot.conf b/outerheaven.init3.home/.config/sway/sway/60-bindings-screenshot.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/60-bindings-volume.conf b/outerheaven.init3.home/.config/sway/sway/60-bindings-volume.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/65-mode-passthrough.conf b/outerheaven.init3.home/.config/sway/sway/65-mode-passthrough.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/90-bar.conf b/outerheaven.init3.home/.config/sway/sway/90-bar.conf new file mode 100644 index 0000000..b58b85e --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/90-bar.conf @@ -0,0 +1,6 @@ +# +## Status Bar: +# +bar { + swaybar_command waybar +} diff --git a/outerheaven.init3.home/.config/sway/sway/90-swayidle.conf b/outerheaven.init3.home/.config/sway/sway/90-swayidle.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/95-autostart-policykit-agent.conf b/outerheaven.init3.home/.config/sway/sway/95-autostart-policykit-agent.conf new file mode 100644 index 0000000..e69de29 diff --git a/outerheaven.init3.home/.config/sway/sway/config b/outerheaven.init3.home/.config/sway/sway/config new file mode 100644 index 0000000..c7a2525 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/config @@ -0,0 +1,36 @@ +# Config for sway +# +# See `man 5 sway` for a complete reference. +font pango:SFProDisplay Regular 12 +smart_borders on +default_border pixel 1 +default_floating_border pixel 1 +# gaps +#smart_gaps on +#gaps inner 1 +# default_floating_border normal +# default_floating_border none +hide_edge_borders --i3 smart +tiling_drag disable +floating_modifier none +workspace_layout tabbed +titlebar_border_thickness 1 +titlebar_padding 4 2 +title_align center +workspace_auto_back_and_forth yes +# cursor stuff - default size is 24 +#seat seat0 xcursor_theme Bibata-Modern-Classic 32 +#seat seat0 xcursor_theme Bibata-Modern-Ice 24 +#seat seat0 xcursor_theme capitaine-cursors-light 24 + +# map human friendly names for displays to outputs, may be used by included configs +set $MON_LEFT DP-2 +set $MON_CENTER DP-1 +set $MON_RIGHT DP-3 + +# include distribution + user things +include '$(/usr/libexec/sway/layered-include "/usr/share/sway/config.d/*.conf" "/etc/sway/config.d/*.conf" "${XDG_CONFIG_HOME:-$HOME/.config}/sway/*.conf")' + +# run script which handles conditional/timely autostarts. uses dict w/ this structure: +# {'autostarts': { 'pre': [], 'weekend': [], 'common': [], 'work': []}} +exec 'python3 ~/.config/sway/scripts/startup.py' diff --git a/outerheaven.init3.home/.config/sway/sway/environment b/outerheaven.init3.home/.config/sway/sway/environment new file mode 100644 index 0000000..88722df --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/environment @@ -0,0 +1,48 @@ +#!/bin/sh +# vim: set ft=sh: +# shellcheck disable=SC2034 +# +# Set environment variables for Sway session +# +# Useful variables for wlroots: +# https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/master/docs/env_vars.md +# +#WLR_NO_HARDWARE_CURSORS=1 +PATH="${HOME}/.cargo/bin:${HOME}/.local/bin:${PATH}" +#SWAY_EXTRA_ARGS="$SWAY_EXTRA_ARGS --debug" ## Pass extra arguments to the /usr/bin/sway executable +GTK2_RC_FILES="${HOME}/.gtkrc-2.0" +QT_STYLE_OVERRIDE=kvantum # use kvantum to try to mirror gtk w/ QT +# XDG_CURRENT_DESKTOP=sway +XDG_DATA_DIRS="${HOME}/.local/share/:${XDG_DATA_DIRS}" +EDITOR=/usr/bin/nvim # change default CLI editor to 'nvim'; Fedora defaults to nano +GNUPGHOME=${HOME}/.gnupg/trezor # use Trezor hardware wallet for GPG signing +# ref: https://github.com/romanz/trezor-agent/blob/master/doc/README-GPG.md +LIBVIRT_DEFAULT_URI="qemu:///system" # default to the system context with qemu/libvirt +SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/keyring/ssh # use the SSH keyring spawned by the DE (if Sway, ~/.config/sway/startup.py) +ANSIBLE_NOCOWS=1 # disable ansible cowsay tomfoolery, I control my ranch [poorly] +MOZ_CRASHREPORTER_DISABLE=1 # disable crash reports for mozilla things -- they eat space while submission is disabled +MOZ_ENABLE_WAYLAND=1 +# GDK_BACKEND=wayland # superfluous? +KITTY_ENABLE_WAYLAND=1 +QT_QPA_PLATFORM=wayland # encourage wayland for QT/KDE apps +PROTON_LOG_DIR=$(mktemp --tmpdir -d proton_logs.XXXX) +SYSTEMD_PAGER='' # disable the auto pager +_JAVA_AWT_WM_NONREPARENTING=1 +NO_AT_BRIDGE=1 +BEMENU_BACKEND=wayland +VAAPI_MPEG4_ENABLED=true +# increase on-disk cache from 1G to 4, NOTE: may further multiply across architectures (32/64) +# MESA_SHADER_CACHE_MAX_SIZE=4G + +# electron vars - wanted features, common args, or wayland specific args (for Sway) +# *not* used by Electron applications directly, used elsewhere in session (ie: autostarts) +W_ELECTRON_FEATURES="VaapiVideoDecoder,VaapiVideoEncoder,WebRTCPipeWireCapturer,UseOzonePlatform,WaylandWindowDecorations,VaapiVideoDecodeLinuxGL" +# egl causes at least mattermost (potentially other things) to have 'GPU' crashes - while others are fine (ie: Discord) +#W_ELECTRON_ARGS="--silent --enable-gpu --use-gl=egl --enable-features='${W_ELECTRON_FEATURES}' --ozone-platform-hint=auto" +W_ELECTRON_ARGS="--silent --enable-gpu-rasterization --enable-sync --disable-features='AudioServiceSandbox,Vulkan' --enable-features='${W_ELECTRON_FEATURES}' --ozone-platform-hint=auto" + +# maximum 'eFfIciEncY', avoid a ton of syscalls - set TZ to use localtime for glibc benefit, ref: +# https://blog.packagecloud.io/set-environment-variable-save-thousands-of-system-calls/ +# flatpak things get weird with this, commenting out +#TZ=:/etc/localtime +# untested theory, may improve: TZ=:../usr/share/zoneinfo/America/Chicago diff --git a/outerheaven.init3.home/.config/sway/sway/scripts/locker.py b/outerheaven.init3.home/.config/sway/sway/scripts/locker.py new file mode 100755 index 0000000..2d4f6ac --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/scripts/locker.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +'''simple screen locker for Sway, run by $mod+l''' +from os import environ +from i3ipc import Connection + + +def lock_session(manager): + '''function for the lock sequence + pauses any playing media and runs swaylock to lock the session''' + + lock_commands = ['playerctl pause', + 'swaylock -f'] + + for command in lock_commands: + print(f'Executing: {command}') + manager.command(f'exec {command}') + + +try: + # explicitly tied to sway/swaysock + # don't get the impression swaylock works w/ i3 + # ...which this module also supports/would use naively + SWAYSOCK = environ['SWAYSOCK'] + + # use subprocess/xrandr to get the 4k display to (later) make it primary + # otherwise games seem to get confused on monitor/resolution + + + # with the socket, connect and lock + _wm = Connection(socket_path=SWAYSOCK, auto_reconnect=True) + lock_session(_wm) + + # clean up, disconnect from WM + _wm.main_quit() + +except IOError as e: + # Handle exceptions related to the connection + print("There was a problem establishing the connection, socket:", SWAYSOCK) + print(e) + +except KeyError: + print('The "SWAYSOCK" var is not defined') diff --git a/outerheaven.init3.home/.config/sway/sway/scripts/screenshot.py b/outerheaven.init3.home/.config/sway/sway/scripts/screenshot.py new file mode 100755 index 0000000..8fbf34c --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/scripts/screenshot.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +This script provides screenshot handling for Sway + +Determines the mode (region or window), and then takes a capture + +Expects window manager keybinds like below: + bindsym --no-repeat Print exec "~/.config/sway/scripts/screenshot.py region" + bindsym --no-repeat Shift+Print exec "~/.config/sway/scripts/screenshot.py window" + +If mode is 'region', the tool 'slurp' is used to select the region to capture. + +If mode is 'window', the WM is asked which display is active (to capture). + +In both cases, the 'grim' utility does the image capture. Captures go here: + ~/Pictures/screenshots/Screenshot_*.png + +Fedora dependencies: + - sudo dnf in python-{i3ipc,pillow} + +Note: while i3ipc aspects of this will work with i3... + slurp/grim likely will not +""" +import argparse +import os +import subprocess +from time import strftime +from i3ipc import Connection, Event +from PIL import Image +from PIL.PngImagePlugin import PngInfo + +# setup argparse +# take one arg, m/mode: selection/window +parser = argparse.ArgumentParser( + description='Take some screenshots for Sway using IPC and slurp/grim') + +# add main arg, screenshot mode -- region (selection area) or [focused] window +parser.add_argument('mode', type=str, choices=['region', 'window', 'w', 'r'], + help='''Screenshot "mode", either: + A selected area ('region') or + the focused display ('window')"''') + +# instantiate args +args = parser.parse_args() + +# env prep +# get the current time +now = strftime("%Y-%m-%dT-%H%M%z") +# use strftime - similar iso format as 'datetime', with 1 minor fix +# no ':' - in the off chance they are uploaded to Jira +# ex: 2022-11-21T-2337-0600 + +# set a var for the homedir using os.environ - arguably not portable? /shrug +homedir = os.environ['HOME'] +screenshot_dir = f'{homedir}/Pictures/screenshots' +screenshot_path = f'{screenshot_dir}/Screenshot_{now}.png' +preview_command = f"imv -d -s none '{screenshot_path}'" +# preview_command = f"xdg-open '{screenshot_path}'" + +if not os.path.isdir(screenshot_dir): + print(f"Screenshot dir doesn't exist, creating {screenshot_dir}") + os.mkdir(screenshot_dir) +else: + print(f'Screenshot dir ({screenshot_dir}) exists, continuing') + +# misc functions +def determine_ss_cmd(): + '''based on mode, determine the screenshot command''' + # screenshot command handling (based on mode) + # grim uses compression level 1 in both cases + # neglible quality difference while saving space + if args.mode in ['window', 'w']: + # use wm connection to determine the active output + outputs = _wm.get_outputs() + for output in outputs: + if output.focused: + active_output = output.name + print(f'determined active output: {active_output}') + command = f"grim -l 1 -c -o {active_output} '{screenshot_path}'" + elif args.mode in ['region', 'r']: + # omits -c to leave out cursors + command = f"slurp -d | grim -l 1 -g - '{screenshot_path}'" + return command + +def preview_focus(_wm, _event): + '''function called by window manager new_window events + checks if new window is preview, if so: give it focus''' + if _event.container.app_id == 'imv': + # give the preview focus + # for Sway we use 'app_id', for i3 this is probably 'class' + _wm.command('[app_id=imv] focus') + # once the preview window is focused, close our connection to the wm + _wm.main_quit() + +def wm_connect(): + '''get the party started, create a connection to the window manager''' + conn = Connection(auto_reconnect=True) + # on new window events check if screenshot preview window gets focus + conn.on(Event.WINDOW_NEW, preview_focus) + return conn + +def _run_command(command): + print(f'Command: {command}') + _r = subprocess.run(command, shell=True, capture_output=True, check=False) + if _r.stderr: + raise subprocess.CalledProcessError( + returncode = _r.returncode, + cmd = _r.args, + stderr = _r.stderr + ) + if _r.stdout: + print(f"Command Result: {_r.stdout.decode('utf-8')}") + return _r + +# begin screenshot/preview process +# connect to the window manager -- Sway +# (i3 could work, may need grim/slurp replacements) +_wm = wm_connect() +# determine screenshot command - differs if window or region mode +screenshot_command = determine_ss_cmd() + +# run the screenshot/preview commands +# previewing/sending focus only if screenshot is taken +SS_RC = -1 +try: + ss_result = _run_command(screenshot_command) + SS_RC = ss_result.returncode +except subprocess.CalledProcessError as error: + print('screenshot failed/aborted') + # clean up after ourselves, close the wm loop + _wm.main_quit() + +# if the screenshot succeeded, place a comment on the image +# and then preview/focus the window +if SS_RC == 0: + # + # construct the comment for the screenshot + # immediately after it's taken, find the focused window details + wm_tree = _wm.get_tree() + wm_focused = wm_tree.find_focused() + app_name = wm_focused.name + app_id = wm_focused.app_id + COMMENT = f"Screenshot of '{app_name} (app_id: {app_id}) at {now}'" + print(f'storing comment: {COMMENT}') + # + # open the screenshot for (metadata) modification + # adding the application title/window class/date as a comment + # visible using 'exiftool', easier sorting through command line + ss_obj = Image.open(screenshot_path) + ss_metadata = PngInfo() + ss_metadata.add_text("Comment", COMMENT) + # + # write the (commented) image back out + ss_obj.save(screenshot_path, pnginfo=ss_metadata) + # + # open the preview with 'imv' + print(f"exec preview: {preview_command}") + _wm.command(f'exec {preview_command}') + # + # start the main loop for the window manager + # basically wait for the preview listener to fire + # when the preview window opens, a message is sent to give it focus + # afterwards we exit + _wm.main() diff --git a/outerheaven.init3.home/.config/sway/sway/scripts/startup.py b/outerheaven.init3.home/.config/sway/sway/scripts/startup.py new file mode 100755 index 0000000..a4a9ff0 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/scripts/startup.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long +""" +A smart but also lazy login autostart manager for i3/Sway. + +Will conditionally exec other things defined in a YML dict. ie: every day, work days, or weekends + +Required i3/Sway config line: + exec /home/jlay/.config/sway/scripts/startup.py + +Config sample: +--- +autostarts: + pre: [] # blocking tasks that run every day, before any other section. intended for backups/updates + common: [] # non-blocking tasks that run every day + weekend: [] # blocking tasks for weekends, after 'pre' but before 'common' + work: [] # non-blocking tasks run if Monday through Friday between 8AM - 4PM + +Dependencies: python3-i3ipc +""" +import os +import subprocess +from datetime import datetime +from time import sleep +import argparse +from textwrap import dedent +from systemd import journal +import yaml.loader +from i3ipc import Connection +from xdg import BaseDirectory + + +def log_message( + message: str, level: str, syslog_identifier: str = "sway-autostart" +) -> None: + """Given `message`, send it to the journal and print + + Accepts 'journal' levels. ie: `journal.LOG_{ERR,INFO,CRIT,EMERG}' + """ + valid_levels = { + journal.LOG_EMERG, + journal.LOG_ALERT, + journal.LOG_CRIT, + journal.LOG_ERR, + journal.LOG_WARNING, + journal.LOG_NOTICE, + journal.LOG_INFO, + journal.LOG_DEBUG, + } + if level not in valid_levels: + raise ValueError(f"Invalid log level: {level}") + print(message) + journal.send(message, PRIORITY=level, SYSLOG_IDENTIFIER=syslog_identifier) + + +def parse_args(): + """If run interactively, this provides arg function to the user""" + description_text = dedent( + f"""\ + A smart but also lazy login autostart manager for i3/Sway. + + Will conditionally exec other things defined in a YML dict. ie: every day, work days, or weekends + + Required i3/Sway config line: + exec {os.path.abspath(__file__)} + + Config sample: + --- + autostarts: + pre: [] # blocking tasks that run every day, before any other section. intended for backups/updates + common: [] # non-blocking tasks that run every day + weekend: [] # blocking tasks for weekends, after 'pre' but before 'common' + work: [] # non-blocking tasks run if Monday through Friday between 8AM - 4PM + """ + ) + + class PlainDefaultFormatter( + argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter + ): + """Combines two ArgParse formatter classes: + - argparse.ArgumentDefaultsHelpFormatter + - argparse.RawDescriptionHelpFormatter""" + + parser = argparse.ArgumentParser( + description=description_text, formatter_class=PlainDefaultFormatter + ) + + # Default path for the config + default_config = os.path.join(BaseDirectory.xdg_config_home, "autostart-i3ipc.yml") + + parser.add_argument( + "-c", + "--config", + default=default_config, + help="Path to the YML configuration file.", + ) + + return parser.parse_args() + + +# OOPy way to determine if it's a work day -- mon<->friday, 3AM<->5PM +class WorkTime(datetime): + """datetime but with work on the mind""" + + def is_workday(self): + """determine if it's a work day: monday-friday between 3AM and 5PM. + Use .vacation file to go on vacation""" + + # first check if ~/.vacation exists - if so, not a work day + if os.path.isfile(os.path.expanduser("~/.vacation")): + return False + + # note: last number in range isn't included + if not self.is_weekend() and self.hour in range(8, 16): + return True + return False + + def is_weekend(self): + """determine if it's the weekend or not, ISO week day outside 1-5""" + if self.isoweekday() not in range(1, 6): + return True + return False + + +if __name__ == "__main__": + args = parse_args() + config_path = args.config + + # get the current time + now = WorkTime.now() + # determine if it's a work day using WorkTime above + workday = now.is_workday() + weekend = now.is_weekend() + + # initialize empty lists for the different categories + wants = [] # non-blocking tasks from 'common' and 'workday' sections in config + pre_list = [] # blocking tasks before the rest + weekend_list = [] # non-blocking tasks for weekend days/logins + + # check the config file for existence/structure. if found, extend the lists + if os.path.exists(config_path): + print(f"found/loading config: '{config_path}'") + with open(config_path, "r", encoding="utf-8") as _config: + config_file = yaml.load(_config, Loader=yaml.FullLoader) + try: + loaded_starts = config_file["autostarts"] + wants.extend(loaded_starts["common"]) + if loaded_starts["pre"]: + pre_list.extend(loaded_starts["pre"]) + if workday: + wants.extend(loaded_starts["work"]) + if weekend: + weekend_list.extend(loaded_starts["weekend"]) + except KeyError as key_err: + log_message( + f"Key not found in {config_path}: {key_err.args[0]}", + journal.LOG_ERR, + ) + except NameError as name_err: + log_message(f"name error: {name_err}", journal.LOG_ERR) + + # get the party started, create a connection to the window manager + _wm = Connection(auto_reconnect=True) + + # start the blocking tasks - 'pre' and 'weekend' + # avoid sending them to the WM, would become backgrounded/async + for pre_item in pre_list: + try: + log_message( + f'running (blocking) "pre" task: "{pre_item}"', journal.LOG_INFO + ) + subprocess.run(pre_item, shell=True, check=False) + except subprocess.CalledProcessError as pre_ex: + log_message(f'failed "{pre_item}": {pre_ex.output}', journal.LOG_ERR) + + if weekend: + for weekend_item in weekend_list: + try: + log_message( + f'running (blocking) "weekend" task: "{weekend_item}"', + journal.LOG_INFO, + ) + subprocess.check_output(weekend_item, shell=True) + except subprocess.CalledProcessError as weekend_except: + log_message( + f'Exception during "{weekend_item}": {weekend_except.output}', + journal.LOG_ERR, + ) + + # launch 'common' and 'work' tasks; not expected to block, sent to window manager + for wanteditem in wants: + command = "exec " + wanteditem + log_message(f'sending to WM: "{command}"', journal.LOG_INFO) + reply = _wm.command(command) + sleep(0.1) + if reply[0].error: + # note: this doesn't check return codes + # serves to check if there was a parsing/comm. error with the WM + log_message( + f'autostart "{command}" failed, couldn\'t reach WM', journal.LOG_ERR + ) + + _wm.main_quit() diff --git a/outerheaven.init3.home/.config/sway/sway/scripts/volume.py b/outerheaven.init3.home/.config/sway/sway/scripts/volume.py new file mode 100755 index 0000000..e7c36d0 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/scripts/volume.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +'''Manages PulseAudio volume for a sink (output) by percentage + +Run by Sway on XF86Audio{Raise,Lower}Volume''' +import argparse +import pulsectl +from pydbus import SessionBus + +def get_pulse(): + '''Returns a Pulse object for inspection/modification''' + return pulsectl.Pulse('volume-changer') + +def get_sink(pulse, sink_name=None): + '''Get / return a PulseAudio sink (output) object by name. + + Used to query and set the volume. If `sink_name` is omitted, return *default* + + Args: + pulse (Pulse): The Pulse object to query, see `get_pulse()` + sink_name (str, optional): The name of the PulseAudio sink to look for. + If omitted the default sink is assumed + + Returns: + pulsectl.PulseSinkInfo''' + if sink_name is None: + return pulse.get_sink_by_name(pulse.server_info().default_sink_name) + return pulse.get_sink_by_name(sink_name) + +def get_volume_rounded(pulse, sink): + '''Return the volume of the provided sink + + Returned as a rounded int averaged across channels, assumed linked''' + return round(pulse.volume_get_all_chans(sink) * 100) + +def set_volume(pulse, sink, adjustment): + '''Changes the PulseAudio `sink` volume by the given `adjustment` + + `sink` should be a pactl object; see `get_sink` + + `adjustment` should be a float, ie 0.01 for *raising* 1% + Invert (* -1) to lower''' + pulse.volume_change_all_chans(sink, adjustment) + +# Create argument parser +parser = argparse.ArgumentParser(description='Change audio volume.') +parser.add_argument('direction', choices=['raise', 'lower'], help='The direction to change the volume.') +parser.add_argument('percentage', nargs='?', default=1, type=int, help='The percentage to change the volume.') +parser.add_argument('--sink', default=None, help='The PulseAudio sink (name) to manage.') + +# Parse arguments +args = parser.parse_args() +# Calculate the volume change as a float, inverse it if *lowering* +# used as a multiplier +change = args.percentage / 100 +if args.direction == 'lower': + change = change * -1 + +# construct empty dict for JSON output/data +# interesting info is appended later +data = {"sink": "", + "change": "", + "start_vol": "", + "new_vol": ""} + +# connect to the notification bus +notifications = SessionBus().get('.Notifications') + +# get pulse / connect +try: + with get_pulse() as _p: + # query the default sink + sink_def = get_sink(pulse=_p) + + # get the starting vol + start_vol = get_volume_rounded(pulse=_p, sink=sink_def) + + # change the volume + set_volume(pulse=_p, sink=sink_def, adjustment=change) + + # query the volume again + new_volume = get_volume_rounded(pulse=_p, sink=sink_def) + + # construct data dict for CLI output/reference + data['sink'] = sink_def.name + data['change'] = change + data['start_vol'] = start_vol + data['new_vol'] = new_volume + + # Create a desktop notification + notification_id = notifications.Notify( + 'volume-changer', 0, 'dialog-information', + 'Volume Change', + f"Now {data['new_vol']}%, was {data['start_vol']}%", + [], {}, 1000) +except pulsectl.PulseError as e: + data['sink'] = None + data['change'] = 'Impossible, exception: {e}' + # notify that we couldn't work with pulseaudio/compatible daemon + notification_id = notifications.Notify( + 'volume-changer', 0, 'dialog-error', + 'Volume Change', + f"Exception: {e}", + [], {}, 1000) + +print(data, flush=True) diff --git a/outerheaven.init3.home/.config/sway/sway/scripts/wallpaper.py b/outerheaven.init3.home/.config/sway/sway/scripts/wallpaper.py new file mode 100755 index 0000000..42aadd5 --- /dev/null +++ b/outerheaven.init3.home/.config/sway/sway/scripts/wallpaper.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +wallpaper.py - random wallpaper/output utility for i3/Sway + +usage: wallpaper.py [-h] [--select {common,unique}] directory + +Selection modes: + Common: One wallpaper selected and shared for *all* displays + Unique: One wallpaper selected for *each* display +""" +import argparse +import os +import sys +import random +import asyncio +from typing import List +from i3ipc.aio import Connection + +EXTS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] + +def parse_args(): + '''Handles argparse on startup''' + parser = argparse.ArgumentParser(description='Random Wallpaper Setter for Sway') + parser.add_argument('directory', type=str, help='Directory containing wallpapers') + parser.add_argument('--select', + type=str, + choices=['common', 'unique'], + default='common', + help='Wallpaper selection mode: all displays, or each?') + return parser.parse_args() + +async def main(): + '''you know what it is''' + args = parse_args() + sway = await Connection(auto_reconnect=True).connect() + def list_image_files(d: str) -> List[str]: + ''' + Given the path to a *'directory'*, returns a list of image files to consider for wallpapers. + ''' + return [os.path.join(d, f) for f in os.listdir(d) if os.path.splitext(f)[1].lower() in EXTS] + + async def set_wallpaper(file_path: str, output=None): + ''' + Given an image path, sets the wallpaper for (optional) outputs in i3/Sway. + + If no output is specified then *all* will receive the wallpaper. + ''' + print(f"{output if output else 'all'}: wallpaper='{file_path}'") + if output: + await sway.command(f'output "{output}" bg "{file_path}" fill') + else: + for _output in await sway.get_outputs(): + await sway.command(f'output "{_output.name}" bg "{file_path}" fill') + + if os.path.isdir(args.directory): + image_files = list_image_files(args.directory) + else: + sys.exit(f'ERR: not a directory: {args.directory}') + if not image_files: + print("No image files found in the specified directory.") + return + + print(f'Found {len(image_files)} candidate image files') + outputs = await sway.get_outputs() + + if args.select == 'common': + wallpaper = random.choice(image_files) + await set_wallpaper(wallpaper) + else: # args.select == 'unique', we need to determine a wallpaper for each display + for output in outputs: + if len(image_files) == 0: + print(f"Not enough images in '{args.directory}' for each display.") + break + wallpaper = random.choice(image_files) + image_files.remove(wallpaper) + await set_wallpaper(wallpaper, output.name) + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(main()) + except KeyboardInterrupt: + pass