swaywm: add dotfiles/scripts

This commit is contained in:
Josh Lay 2024-06-18 08:09:58 -05:00
parent 49f94bb424
commit 9d26abbb3a
No known key found for this signature in database
GPG key ID: 47AA304B2243B579
22 changed files with 1168 additions and 0 deletions

View file

@ -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}

View file

@ -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.<class> <border> <background> <text> [<indicator> [<child_border>]]
#
# 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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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 "<b>%title</b> (%app_id)"
#
# very-generic titles will benefit from anchors (^...$) - it's effectively a wildcard otherwise
#
# for_window [shell="xdg_shell"] title_format "<b>%title</b> <i>(%app_id)</i>"
# commented out the above to only expose xwayland things and reduce repetition in titles
for_window [shell="xwayland"] title_format "<span> %title <i>[X11]</i> </span>"
# 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

View file

@ -0,0 +1,6 @@
#
## Status Bar:
#
bar {
swaybar_command waybar
}

View file

@ -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'

View file

@ -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

View file

@ -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')

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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