integrate home-manager config

This commit is contained in:
jaina heartles 2024-07-21 22:36:29 -07:00
parent a470338c43
commit 826a0e5525
45 changed files with 5308 additions and 1 deletions

View file

@ -0,0 +1,95 @@
@import "theme_cat_mocha"
@import "theme_jwm"
* {
font-family: 'Iosevka Comfy Duo, Symbols Nerd Font, Font Awesome';
}
.background {
background-color: rgba(#000, 0);
}
.bar {
background-color: rgba($cat-mantle, 0.9);
padding: 5px 10px;
border-radius: 10px;
margin: 10px;
margin-bottom: 0px;
}
.modules > widget {
background-color: rgba(#000, 0);
}
.modules > widget > *,
.modules > * {
background-color: rgba($cat-surface1, 0.7);
border-radius: 10px;
margin: 0px 10px;
padding: 0px 10px;
}
.modules > .container {
padding: 0px;
}
.modules > .container > widget > *,
.modules > .container > * {
padding: 0px 8px;
}
.modules > .container > widget:first-child > *,
.modules > .container > *:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
padding-left: 10px;
}
.modules > .container > widget:last-child > *,
.modules > .container > *:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
padding-right: 10px;
}
.workspaces { padding: 0px; }
.workspaces .item {
font-size: 90%;
min-width: 12px;
padding: 0px 8px;
border-radius: 10px;
background-color: rgba(0,0,0,0);
color: $cat-text;
}
.workspaces .item:focus,
.workspaces .item.workspace-active,
.workspaces .item:hover {
font-size: 100%;
}
.workspaces .item.workspace-active {
background-color: $jwm-accent;
color: $jwm-accent-contrast;
}
.audio-controls { padding: 0; }
.audio-controls .volume-control { padding-right: 6px; }
.audio-controls .mic-control .icon { padding-left: 6px; }
.volume-control .icon { font-size: 18px; }
.volume-control .value { padding-left: 6px; }
.volume-control.muted .icon { color: $cat-red; }
.volume-control.muted .value {
color: $cat-subtext0;
text-decoration: line-through;
}
.mic-control.muted .icon { color: $cat-subtext0; }
.playerctl.playing .music-icon {
background-color: $jwm-accent;
color: $jwm-accent-contrast;
}
.playerctl.paused .details { color: $cat-subtext0; }
.playerctl .details > *:first-child, .playerctl > *:first-child { padding-left: 0px; }
.playerctl .details > *:last-child, .playerctl > *:last-child { padding-right: 0px; }
.playerctl .details > * { padding: 0px 5px; }

View file

@ -0,0 +1,288 @@
;; --------- Variables
;; --------- Listeners
(deflisten hyprland-window
`
socat -U - /tmp/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | \
grep 'activewindow>>' --line-buffered | \
sed 's/^activewindow>>[^,]*,//' --unbuffered
`)
(deflisten hyprland-submap
`
socat -U - /tmp/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | \
grep 'submap>>' --line-buffered | \
sed 's/^submap>>//' --unbuffered
`)
(deflisten workspace-list
:initial '[]'
`hyprland-workspaces _ | jq --compact-output --unbuffered '[
.[] | select(.id > 0)
]'`)
(deflisten current-workspace
:initial '[]'
`hyprland-workspaces _ | jq --compact-output --unbuffered '
.[] | select(.id > 0) | select(.class | contains("active")) | .id
'`)
(deflisten audio-out-volume
:initial 0
`
pamixer --get-volume
pactl subscribe | grep --line-buffered "'change' on sink" | xargs -I {} pamixer --get-volume
`
)
(deflisten audio-out-mute
:initial false
`
pamixer --get-mute
pactl subscribe | grep --line-buffered "'change' on sink" | xargs -I {} pamixer --get-mute
`
)
(deflisten audio-in-mute
:initial false
`
pamixer --get-mute --source @DEFAULT_SOURCE@
pactl subscribe | grep --line-buffered "'change' on source" | xargs -I {} pamixer --get-mute --source @DEFAULT_SOURCE@
`
)
(deflisten playerctl-status :initial "" `playerctl status --player strawberry --follow --format '{{lc(status)}}'`)
(deflisten playerctl-title `playerctl metadata --player strawberry --follow --format '{{title}}'`)
(deflisten playerctl-artist `playerctl metadata --player strawberry --follow --format '{{artist}}'`)
(deflisten playerctl-album `playerctl metadata --player strawberry --follow --format '{{album}}'`)
(deflisten playerctl-position `playerctl metadata --player strawberry --follow --format '{{duration(position)}}'`)
(deflisten playerctl-length `playerctl metadata --player strawberry --follow --format '{{duration(mpris:length)}}'`)
;; -------------- Widgets
(defwidget window-title [?limit-width]
(label
:visible {strlength(hyprland-window) > 0}
:text hyprland-window
:class "window-title"
:tooltip hyprland-window
:limit-width {limit-width ?: 999}
)
)
(defwidget wm-mode []
(eventbox
:class "wm-mode"
:visible {strlength(hyprland-submap) > 0}
:onclick `hyprctl dispatch submap '' >/dev/null`
(label
:text hyprland-submap
:tooltip hyprland-submap
)
)
)
(defwidget workspaces [monitor]
(box
:class "workspaces"
(eventbox
:onscroll `~/.config/eww/hyprworkspace {} ${monitor} ${current-workspace}`
(box
(for i in {jq(workspace-list, '[ .[] | select (.monitor == "${monitor}") ]')}
(button
:onclick "bash -c 'hyprctl dispatch workspace ${i.id} >/dev/null'"
:class "item ${i.class}"
"${i.name}"
)
)
)
)
)
)
(defwidget clock []
(label
:class "clock"
:text {formattime(EWW_TIME, "%H:%M")}
)
)
(defwidget battery [bat]
(box
:class {"battery " + (EWW_BATTERY[bat].capacity < 15 && EWW_BATTERY[bat].status != "Charging" ? "critical" : "") }
:style {EWW_BATTERY[bat].status == "Charging" ? "padding-left: 8px" : "padding-left: 3px"}
:space-evenly false
(box
:visible {EWW_BATTERY[bat].status == 'Charging'}
:style "margin-right: 3px"
(transform
:rotate 75
:translate-x "-19px"
:translate-y "-4px"
(label :text ""))
)
(transform
:visible {EWW_BATTERY[bat].status != 'Charging'}
:rotate 75
:translate-x "-21px"
(label :text {
EWW_BATTERY[bat].capacity < 10 ? "" :
EWW_BATTERY[bat].capacity < 20 ? "" :
EWW_BATTERY[bat].capacity < 30 ? "" :
EWW_BATTERY[bat].capacity < 40 ? "" :
EWW_BATTERY[bat].capacity < 50 ? "" :
EWW_BATTERY[bat].capacity < 60 ? "" :
EWW_BATTERY[bat].capacity < 70 ? "" :
EWW_BATTERY[bat].capacity < 80 ? "" :
EWW_BATTERY[bat].capacity < 90 ? "" :
""
}))
(label :text "100%")
)
)
(defwidget volume-control []
(eventbox
:onclick `pamixer --toggle-mute`
;;:onclick `notify-send "handler triggered"`
:onscroll `
amt=1
if [ "{}" = "up" ]; then
pamixer --increase $amt
else
pamixer --decrease $amt
fi
`
(box
:space-evenly false
:class {"volume-control " + (audio-out-mute ? "muted " : "")}
(label
:class "icon"
:text {
audio-out-mute ? "󰸈" : (
audio-out-volume < 10 ? "󰕿" :
audio-out-volume < 60 ? "󰖀" :
"󰕾"
)
}
)
(label :class "value" :text "${audio-out-volume}%")
)
)
)
(defwidget mic-control []
(eventbox
:class {"mic-control " + (audio-in-mute ? "muted " : "")}
:onclick `pamixer --toggle-mute --source @DEFAULT_SOURCE@`
(label
:class "icon"
:text {audio-in-mute ? "" : ""}
)
)
)
(defwidget audio-controls []
(box :class "container audio-controls" :space-evenly false (volume-control) (mic-control))
)
(defwidget playerctl [?limit-width]
(box
:space-evenly false
:class "container playerctl ${playerctl-status}"
(eventbox
(label :class "music-icon" :text "")
)
(eventbox
:visible { playerctl-status != '' }
:onclick 'playerctl --player strawberry play-pause'
:onrightclick 'playerctl --player strawberry next'
:onmiddleclick 'playerctl --player strawberry previous'
(box
:class "details"
:space-evenly false
:tooltip `${playerctl-title} - ${playerctl-artist} - ${playerctl-album}`
(label
:class "status-icon"
:text { (playerctl-status == 'playing') ? '' : '' }
)
(label
:class "track-info"
:limit-width limit-width
:text `${playerctl-title} - ${playerctl-artist} - ${playerctl-album}`
)
(label
:class "position"
:markup `<small>[${playerctl-position}/${playerctl-length}]</small>`
)
)
)
)
)
;; ---------- Bars
(defwidget bar [monitor ?title-limit-width]
(centerbox :class "bar"
:class "bar"
(box :class "modules modules-left"
:space-evenly false
:halign "start"
(workspaces :monitor monitor)
(wm-mode)
(playerctl :limit-width 60)
)
(box :class "modules modules-center"
:space-evenly false
:halign "center"
(window-title :limit-width title-limit-width)
)
(box :class "modules modules-right"
:space-evenly false
:halign "end"
(audio-controls)
(battery :bat "BAT0")
(clock)
)
)
)
(defwindow bar-window-0
:monitor 0
:geometry (geometry :x "0%"
:width "100%"
:height "30px"
:anchor "top center")
:stacking "fg"
;; :reserve (struts :distance "80px" :side "top")
:exclusive true
:windowtype "dock"
(box (bar :monitor "eDP-1" :title-limit-width 100))
)
(defwindow bar-window-1
:monitor 1
:geometry (geometry :x "0%"
:width "100%"
:height "30px"
:anchor "top center")
:stacking "fg"
;; :reserve (struts :distance "80px" :side "top")
:exclusive true
:windowtype "dock"
(box (bar :monitor "DP-3"))
)
(defwindow bar-window-2
:monitor 2
:geometry (geometry :x "0%"
:width "100%"
:height "30px"
:anchor "top center")
:stacking "fg"
;; :reserve (struts :distance "80px" :side "top")
:exclusive true
:windowtype "dock"
(box (bar :monitor "DP-2" :title-limit-width 60))
)

View file

@ -0,0 +1,22 @@
#!/bin/sh
cmd=$1
monitor=$2
current=$3
if [ "$cmd" = "down" ]; then
to_change=$(
hyprctl workspaces -j | jq --raw-output '
[ .[] | select(.monitor == "'$monitor'" and .id > 0) | .id ] |
([ .[] | select(. < '$current') ] | max) // max
')
elif [ "$cmd" = "up" ]; then
to_change=$(
hyprctl workspaces -j | jq --raw-output '
[ .[] | select(.monitor == "'$monitor'" and .id > 0) | .id ] |
([ .[] | select(. > '$current') ] | min) // min
')
fi
hyprctl dispatch workspace $to_change >/dev/null

View file

@ -0,0 +1,26 @@
$cat-rosewater: #dc8a78;
$cat-flamingo: #dd7878;
$cat-pink: #ea76cb;
$cat-mauve: #8839ef;
$cat-red: #d20f39;
$cat-maroon: #e64553;
$cat-peach: #fe640b;
$cat-yellow: #df8e1d;
$cat-green: #40a02b;
$cat-teal: #179299;
$cat-sky: #04a5e5;
$cat-sapphire: #209fb5;
$cat-blue: #1e66f5;
$cat-lavender: #7287fd;
$cat-text: #4c4f69;
$cat-subtext1: #5c5f77;
$cat-subtext0: #6c6f85;
$cat-overlay2: #7c7f93;
$cat-overlay1: #8c8fa1;
$cat-overlay0: #9ca0b0;
$cat-surface2: #acb0be;
$cat-surface1: #bcc0cc;
$cat-surface0: #ccd0da;
$cat-base: #eff1f5;
$cat-mantle: #e6e9ef;
$cat-crust: #dce0e8;

View file

@ -0,0 +1,26 @@
$cat-rosewater: #f5e0dc;
$cat-flamingo: #f2cdcd;
$cat-pink: #f5c2e7;
$cat-mauve: #cba6f7;
$cat-red: #f38ba8;
$cat-maroon: #eba0ac;
$cat-peach: #fab387;
$cat-yellow: #f9e2af;
$cat-green: #a6e3a1;
$cat-teal: #94e2d5;
$cat-sky: #89dceb;
$cat-sapphire: #74c7ec;
$cat-blue: #89b4fa;
$cat-lavender: #b4befe;
$cat-text: #cdd6f4;
$cat-subtext1: #bac2de;
$cat-subtext0: #a6adc8;
$cat-overlay2: #9399b2;
$cat-overlay1: #7f849c;
$cat-overlay0: #6c7086;
$cat-surface2: #585b70;
$cat-surface1: #45475a;
$cat-surface0: #313244;
$cat-base: #1e1e2e;
$cat-mantle: #181825;
$cat-crust: #11111b;

View file

@ -0,0 +1,3 @@
$jwm-accent: $cat-green;
$jwm-accent2: $cat-blue;
$jwm-accent-contrast: $cat-crust;

View file

@ -0,0 +1,84 @@
{ config, lib, pkgs, ... }:
with lib;
with builtins;
let cfg = config.jainawm.kanshi;
in {
options.jainawm.kanshi = { enable = mkEnableOption "Enable kanshi config"; };
config = mkIf cfg.enable {
wayland.windowManager.sway.config.startup = [{
command = "${pkgs.kanshi}/bin/kanshictl reload";
always = true;
}];
home.packages = [ pkgs.kanshi ];
services.kanshi = {
enable = true;
profiles = let
laptop = "eDP-1";
asus1440 = "Ancor Communications Inc ASUS MG278 0x00001EC0";
hp1080 = "HP Inc. HP 24mh 3CM2023MLQ ";
#asus1440 = "DP-3";
#hp1080 = "DP-2";
in {
undocked = {
outputs = [{
criteria = laptop;
mode = "1920x1200";
}];
};
undocked-game = {
outputs = [{
criteria = laptop;
mode = "1920x1080";
transform = "normal";
status = "enable";
}];
};
docked = let
# asusCoords = {
# x = 0;
# y = 0;
# };
asusCoords = {
x = 1080;
y = 100;
};
mkPosition = x: y:
"${toString (asusCoords.x + x)},${toString (asusCoords.y + y)}";
in {
outputs = [
{
criteria = laptop;
mode = "1920x1200";
#position = "3640,1080";
position = mkPosition 2560 980;
transform = "normal";
status = "enable";
}
{
criteria = asus1440;
mode = "2560x1440"; # TODO: figure out 144Hz
#position = "1080,100";
position = mkPosition 0 0;
transform = "normal";
status = "enable";
}
{
criteria = hp1080;
mode = "1920x1080";
#position = "0,0";
#position = "-1080,100";
position = mkPosition (-1080) 100;
transform = "90";
status = "enable";
}
];
};
};
};
};
}

View file

@ -0,0 +1,152 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.jainawm;
cat = cfg.catppuccin;
profileDir = "${config.xdg.stateHome}/phelper";
profilePath = "${profileDir}/active-profile";
in {
options.jainawm.phelper = {
enable = mkOption {
type = types.bool;
default = true;
};
defaultProfile = mkOption { type = types.str; };
specialisationProfile = mkOption {
type = types.nullOr types.str;
default = null;
};
makeSpecializations = mkOption {
type = types.bool;
default = false;
};
profiles = mkOption {
type = types.attrsOf (types.submodule {
options = {
name = mkOption { type = types.str; };
displayName = mkOption {
type = types.nonEmptyStr;
description = ''
Display name to use for the profile.
'';
};
nicknames = mkOption {
type = types.listOf types.str;
default = [ ];
};
accents = let
colorType = types.enum [
"Rosewater"
"Flamingo"
"Pink"
"Mauve"
"Red"
"Maroon"
"Peach"
"Yellow"
"Green"
"Teal"
"Sky"
"Sapphire"
"Blue"
"Lavender"
];
in {
primary = mkOption {
type = colorType;
description = "The primary accent to use.";
};
secondary = mkOption {
type = colorType;
description = "The secondary accent to use.";
};
};
wallpaper = mkOption { type = types.path; };
};
});
};
};
config = let
useDefault = builtins.isNull cfg.phelper.specialisationProfile;
activeProfileName = if !useDefault then
cfg.phelper.specialisationProfile
else
cfg.phelper.defaultProfile;
activeProfile = getAttr activeProfileName cfg.phelper.profiles;
in mkIf cfg.phelper.enable {
jainawm = {
wallpaper = activeProfile.wallpaper;
accents = activeProfile.accents;
};
assertions = [{
assertion = config.xdg.userDirs.enable;
message = "xdg.userDirs.enable must be set";
}];
home.activation.phelperSetProfile =
let path = "${config.xdg.stateHome}/phelper/active-profile";
in lib.hm.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD mkdir -p "${config.xdg.stateHome}/phelper"
$DRY_RUN_CMD echo "${activeProfile.name}" > "${path}"
'';
# When switching to a "base" (not a profile specialisation) generation,
# keep track of what the
home.activation.phelperTrackSpecialization = lib.mkDefault
(lib.hm.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD rm -f "${config.xdg.stateHome}/phelper/base-generation"
$DRY_RUN_CMD ${pkgs.coreutils}/bin/ln -s "$(
${pkgs.home-manager}/bin/home-manager generations | \
${pkgs.coreutils}/bin/head -n1 | \
${pkgs.gawk}/bin/awk '{ print $7; }'
)" \
"${config.xdg.stateHome}/phelper/base-generation"
'');
specialisation = mkIf useDefault (mapAttrs (n: c: {
configuration = {
jainawm.phelper.specialisationProfile = n;
home.activation.phelperTrackSpecialization =
lib.hm.dag.entryAnywhere "";
};
}) cfg.phelper.profiles);
xdg.configFile = let
mkFiles = name: profile: {
"phelper/profiles/${name}/name".text = profile.name;
"phelper/profiles/${name}/display-name".text = profile.displayName;
"phelper/profiles/${name}/nicknames".text =
lib.concatStringsSep "\n" profile.nicknames;
"phelper/profiles/${name}/wallpaper".source = profile.wallpaper;
"phelper/profiles/${name}/wallpaper.${
builtins.unsafeDiscardStringContext
(lib.lists.last (lib.strings.splitString "." profile.wallpaper))
}".source = profile.wallpaper;
};
in lists.foldl lib.trivial.mergeAttrs { }
(mapAttrsToList mkFiles cfg.phelper.profiles);
home.packages = let
pkg = pkgs.stdenv.mkDerivation {
name = "phelper-tools";
dontUnpack = true;
installPhase = ''
mkdir -p "$out/bin"
cp "${./phelper/phelper.sh}" "$out/bin/phelper"
'';
};
in [ pkg ];
};
}

View file

@ -0,0 +1,46 @@
#!/bin/sh
profile_dir="$XDG_CONFIG_HOME/phelper/profiles"
active_profile_file="$XDG_STATE_HOME/phelper/active-profile"
get_option () {
what="$1"
who="$2"
if [ -z "$who" ]; then
who="$(phelper who)"
fi
path="$profile_dir/$who"
case "$what" in
"display-name") cat "$path/display-name" ;;
"wallpaper") echo "$path/wallpaper" ;;
"nicknames") cat "$path/nicknames" ;;
#"theme_color") echo "$theme_color" ;;
*)
echo "Unknown var"
exit 1
;;
esac
}
do_switch () {
who="$1"
file="$XDG_STATE_HOME/phelper/base-generation/specialisation/$who/activate"
if [ -e "$file" ]; then
"$file"
else
echo "Profile $who not setup"
exit 1
fi
}
arg="$1"
shift 1
case "$arg" in
"list") ls -1 "$profile_dir" ;;
"switch") do_switch "$@" ;;
"who") cat "$active_profile_file" ;;
"get") get_option "$@" ;;
esac

View file

@ -0,0 +1,69 @@
{ lib, pkgs, config, ... }:
with lib;
with builtins;
let cfg = config.jainawm.ptray;
in {
options.jainawm.ptray = {
enable = mkEnableOption "Enable ptray support";
items = mkOption {
type = types.anything;
default = { };
};
style = mkOption {
type = types.listOf types.str;
default = [
"floating enable"
"opacity 0.97"
"resize set 80ppt 80ppt"
"move position 10ppt -10"
"sticky enable"
];
};
finalPackage = mkOption {
type = types.package;
readOnly = true;
};
};
config = mkIf cfg.enable {
wayland.windowManager.sway.config.window.commands = let
mkRules = let
style = concatStringsSep ",, " cfg.style;
mkRulesForClass = class: [
({
command = style;
criteria = { class = class; };
})
({
command = style;
criteria = { app_id = class; };
})
];
in name: item: map mkRulesForClass item.match_classes;
in lists.flatten (mapAttrsToList mkRules cfg.items);
wayland.windowManager.sway.config.startup =
[{ command = "${cfg.finalPackage}/bin/ptray"; }];
jainawm.ptray.finalPackage =
# prob not the right way to do this but i don't really care
let python = pkgs.python311;
in python.pkgs.buildPythonPackage {
name = "jwm-ptray";
version = "0.1";
src = ./ptray;
doCheck = false;
propagatedBuildInputs = with python.pkgs; [ inotify ];
};
xdg.configFile."ptray/config.json".text =
builtins.toJSON ({ items = cfg.items; });
home.packages = [ cfg.finalPackage ];
};
}

View file

@ -0,0 +1,18 @@
import os
import json
CONFIG_FILE = os.path.join(os.getenv('XDG_CONFIG_HOME'), 'ptray', 'config.json')
DESKTOP = os.getenv('XDG_CURRENT_DESKTOP')
if DESKTOP == 'i3' or DESKTOP == 'sway':
STATE_DIR = '{}.{}'.format(os.getenv('I3SOCK'), 'ptray')
elif DESKTOP == "hyprland":
STATE_DIR = '{}/{}.ptray'.format(os.getenv("XDG_RUNTIME_DIR"), os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))
else:
raise ValueError('Unsupported value of XDG_CURRENT_DESKTOP: {}'.format(DESKTOP))
def read_config(path=CONFIG_FILE):
with open(path, 'r') as f:
config = json.load(f)
return config

View file

@ -0,0 +1,117 @@
import os
import socket
import json
from typing import NamedTuple, Any
HYPR = "/tmp/hypr/{}".format(os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))
COMMANDSOCK = "{}/.socket.sock".format(HYPR)
EVENTSOCK = "{}/.socket2.sock".format(HYPR)
BUF_SIZE = 2 ** 20
class EventType(Enum):
WORKSPACE = "workspace"
FOCUSED_MON = "focusedmon"
ACTIVE_WINDOW = "activewindow"
ACTIVE_WINDOW_V2 = "activewindowv2"
FULLSCREEN = "fullscreen"
MONITOR_REMOVED = "monitorremoved"
MONITOR_ADDED = "monitoradded"
CREATE_WORKSPACE = "createworkspace"
DESTROY_WORKSPACE = "destroyworkspace"
MOVE_WORKSPACE = "moveworkspace"
ACTIVE_LAYOUT = "activelayout"
OPEN_WINDOW = "openwindow"
CLOSE_WINDOW = "closewindow"
MOVE_WINDOW = "movewindow"
OPEN_LAYER = "openlayer"
CLOSE_LAYER = "closelayer"
SUBMAP = "submap"
CHANGE_FLOATING_MODE = "changefloatingmode"
URGENT = "urgent"
MINIMIZE = "minimize"
SCREENCAST = "screencast"
WINDOW_TITLE = "windowtitle"
class Event(NamedTuple):
type : EventType
value : str
def subscribe(sockfile=EVENTSOCK):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_CLOEXEC)
sock.connect(sockfile)
while True:
msg = sock.recvmsg(BUF_SIZE)[0]
type, value = msg.split('>>', 1)
return Event(type=type, value=value)
def hyprctl(cmd, sockfile=COMMANDSOCK):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_CLOEXEC)
try:
sock.connect(sockfile)
header = 'j/'.encode('utf-8')
payload = cmd.encode('utf-8')
sock.sendmsg([header, payload])
msg = sock.recvmsg(BUF_SIZE)[0]
finally:
sock.close()
return json.loads(msg)
print(json.dumps(hyprctl('clients')))
class Monitor:
event_handler = None
tracked_classes = None
window_ids = None
def __init__(self, event_handler, tracked_classes):
self.event_handler = event_handler
self.tracked_classes = tracked_classes
self.window_ids = {name: set() for name in tracked_classes}
windows = hyprctl('clients')
monitors = hyprctl('monitors')
open_workspaces = [m['specialWorkspace']['id'] for m in monitors]
for window in windows:
for name: classes in tracked_classes.items():
for cls in classes:
if cls in window['class']:
id = window['address']
self.event_handler.on_create(name, id)
self.window_ids[name].add(id)
workspace_id = window['workspace']['id']
if workspace_id < 0:
self.event_handler.on_float(name, id)
if workspace_id in open_workspaces:
self.event_handler.on_visible(name, id)
else:
self.event_handler.on_invisible(name, id)
else:
self.event_handler.on_tile(name, id)
self.event_handler.on_visible(name, id)
def listen(self):
for event in subscribe():
self.handle_event(event)
def match_window_id_to_name(self, id):
for name, ids in self.window_ids.items():
if id in ids:
return name
return None
def handle_event(self, event):
if event == EventType.MOVE_WINDOW:
id, workspace = event.split(',', 1)
if workspace.startsWith('special'):
self.even
movewindow>>2700e80,special:3

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,30 @@
from i3sway.protocol import Connection, ConnType
class Client:
conn = None
def __init__(self):
self.conn = Connection()
def __enter__(self):
return self
def __exit__(self, *details):
self.conn.close()
def cmd_on_window(self, id, cmd):
if self.conn.type == ConnType.SWAY:
criteria = '[con_id={}]'.format(id)
else:
criteria = '[id={}]'.format(id)
return self.conn.exec('{} {}'.format(criteria, cmd))
def show_window(self, id):
self.cmd_on_window(id, "focus")
def hide_window(self, id):
self.cmd_on_window(id, "move to scratchpad")
def run(self, command):
self.conn.exec("exec {}".format(command))

View file

@ -0,0 +1,112 @@
from i3sway.protocol import Connection, MessageType
class Monitor:
tracked_classes = None
cmd_sock = None
subscription_sock = None
event_handler = None
conn_type = None
def __enter__(self):
return self
def __exit__(self, *details):
self.cmd_sock.close()
self.subscription_sock.close()
def __init__(self, event_handler, tracked_classes):
self.event_handler = event_handler
self.tracked_classes = tracked_classes
self.cmd_sock = Connection()
self.subscription_sock = Connection()
tree = self.cmd_sock.request(MessageType.GET_TREE).payload
def parse_tree(root):
if root.get('window') is not None or root.get('app_id') is not None:
# Application window
for name, classes in tracked_classes.items():
if self.container_matches_classes(root, classes):
id = str(root['id'])
self.event_handler.on_create(name, id)
if root['visible']:
self.event_handler.on_visible(name, id)
if root['type'] == 'floating_con':
self.event_handler.on_float(name, id)
elif root['type'] == 'con':
self.event_handler.on_tile(name, id)
else:
for node in root['nodes']:
parse_tree(node)
for node in root['floating_nodes']:
parse_tree(node)
parse_tree(tree)
def container_matches_classes(self, container, classes):
normalize = lambda v: v.lower() if v is not None else ''
app_id = container.get('app_id')
window_props = container.get('window_properties')
instance = window_props.get('instance') if window_props is not None else None
window_class = window_props.get('class') if window_props is not None else None
app_id = normalize(app_id)
instance = normalize(instance)
window_class = normalize(window_class)
for keyword in classes:
kw = normalize(keyword)
if kw in app_id or kw in instance or kw in window_class:
return True
return False
def listen(self):
for event in self.subscription_sock.subscribe(['window']):
self.handle_event(event)
def is_window_in_scratchpad(self, id):
def scan_tree(root, is_in_scratch):
if root['id'] == id:
return is_in_scratch
is_scratch = is_in_scratch or (
root['type'] == 'workspace' and root['name'] == '__i3_scratch'
)
for node in root['nodes'] + root['floating_nodes']:
result = scan_tree(node, is_scratch)
if result is not None:
return result
tree = self.cmd_sock.request(MessageType.GET_TREE)
return scan_tree(tree.payload, False)
def handle_event(self, event):
if event.type == MessageType.window:
return self.handle_window_event(event)
def handle_window_event(self, event):
container = event['container']
for name, classes in self.tracked_classes.items():
change = event['change']
if self.container_matches_classes(container, classes):
id = str(container['id'])
if change == 'new':
self.event_handler.on_create(name, id)
elif change == 'focus':
self.event_handler.on_visible(name, id)
elif change == 'close':
self.event_handler.on_destroy(name, id)
elif change == 'move':
if container['visible']:
self.event_handler.on_visible(name, id)
else:
self.event_handler.on_invisible(name, id)
elif change == 'floating':
if container['type'] == 'floating_con':
self.event_handler.on_float(name, id)
elif container['type'] == 'con':
self.event_handler.on_tile(name, id)

View file

@ -0,0 +1,133 @@
import json
import socket
import struct
import os
from enum import Enum, IntEnum
from typing import NamedTuple, Any
class ConnType(Enum):
SWAY = 'sway'
I3 = 'i3'
SWAYSOCK = os.getenv('SWAYSOCK')
I3SOCK = os.getenv('I3SOCK')
class MessageType(IntEnum):
# Commands / Replies
RUN_COMMAND = 0
GET_WORKSPACES = 1
SUBSCRIBE = 2
GET_OUTPUTS = 3
GET_TREE = 4
GET_MARKS = 5
GET_BAR_CONFIG = 6
GET_VERSION = 7
GET_BINDING_NODES = 8
GET_CONFIG = 9
SEND_TICK = 10
SYNC = 11
GET_BINDING_STATE = 12
GET_INPUTS = 100
GET_SEATS = 101
# Events
workspace = 0x80000000
mode = 0x80000002
window = 0x80000003
barconfig_update = 0x80000004
binding = 0x80000005
shutdown = 0x80000006
tick = 0x80000007
bar_state_update = 0x80000014
input = 0x80000015
class Message(NamedTuple):
type : MessageType
payload : Any
MAGIC = 'i3-ipc'.encode('utf-8')
HEADER_FORMAT = '=6sII'
HEADER_LEN = struct.calcsize(HEADER_FORMAT)
def accept(sock):
header = sock.recvmsg(Message.HEADER_LEN)[0]
magic, len, type = struct.unpack(Message.HEADER_FORMAT, header)
if magic != Message.MAGIC:
raise ValueError('Protocol error, expected magic value {}, got magic value {}' % (Message.MAGIC, magic))
payload_buf = sock.recvmsg(len)[0]
payload = json.loads(payload_buf)
return Message(type=type, payload=payload)
def send(self, sock):
if self.payload is None:
payload_buf = bytes([])
elif isinstance(self.payload, str):
payload_buf = self.payload.encode('utf-8')
else:
payload_buf = json.dumps(self.payload).encode('utf-8')
payload_len = len(payload_buf)
header = struct.pack(Message.HEADER_FORMAT, Message.MAGIC, payload_len, self.type)
sock.sendmsg([header, payload_buf])
def __getitem__(self, key):
return self.payload[key]
class Connection:
sock: socket.socket
type: ConnType
subscription_open: bool = False
def __init__(self, type=None, sockfile=None):
self.type = type
if sockfile is None:
if type == ConnType.SWAY or (type is None and SWAYSOCK is not None):
sockfile = SWAYSOCK
self.type = ConnType.SWAY
elif type == ConnType.I3 or (type is None and I3SOCK is not None):
sockfile = I3SOCK
self.type = ConnType.I3
else:
raise ValueError('No compatible window managers found')
self.sock = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,# | socket.SOCK_NONBLOCK | socket.SOCK_CLOEXEC,
)
self.sock.connect(sockfile)
def __enter__(self):
return self
def __exit__(self, *details):
self.close()
def close(self):
self.sock.close()
def subscribe(self, events: list):
if self.subscription_open:
raise ValueError('Subscription already open on socket')
reply = self.request(MessageType.SUBSCRIBE, events)
if not reply['success']:
raise ValueError('Subscription failed')
self.subscription_open = True
while True:
yield Message.accept(self.sock)
def exec(self, command: str):
return self.request(MessageType.RUN_COMMAND, command)
def request(self, type: MessageType, payload: Any = None) -> Message:
if self.subscription_open:
raise ValueError('Subscription open on socket')
message = Message(type=type, payload=payload)
message.send(self.sock)
reply = Message.accept(self.sock)
if reply.type != type:
print('Incorrect reply type', reply.type)
return reply

23
home-manager/jwm/ptray/ptray Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
from i3sway.monitor import Monitor
from config import read_config
from state import StateWriter
"""
{
"items": {
"discord": {
"match_class": [ "discord" "WebCord" ],
"exec": "webcord",
},
},
}
"""
config = read_config()
state = StateWriter(config)
class_map = {name: defn['match_classes'] for name, defn in config['items'].items()}
with Monitor(state, class_map) as wm:
wm.listen()

96
home-manager/jwm/ptray/ptrayctl Executable file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python
import os
import argparse
import inotify.adapters
from i3sway.commands import Client
from config import read_config
from state import open_file, format_path
parser = argparse.ArgumentParser(
prog='ptrayctl',
description='manages user pseudo-tray',
)
parser.add_argument('command', choices=['show', 'hide', 'watch', 'toggle'])
parser.add_argument('name', nargs='?')
parser.add_argument('-a', '--all', action='store_true', help='Include windows that are not floating')
args = parser.parse_args()
config = read_config()
def show_window(name):
with Client() as client:
is_open = False
with open_file(name, 'window_ids') as f:
for line in f:
is_open = True
client.show_window(int(line))
if not is_open:
with open_file(name, 'exec') as f:
client.run(f.read())
def hide_window(name):
if not args.all:
with open_file(name, 'tiled_ids') as f:
ignore_ids = f.read().split('\n')
else:
ignore_ids = []
with Client() as client:
with open_file(name, 'window_ids') as f:
for line in f:
if line not in ignore_ids:
client.hide_window(int(line))
def hide_all():
for name in config['items']:
hide_window(name)
def toggle_window(name):
with open_file(name, 'status') as f:
status = f.read()
if status == 'fg':
hide_window(name)
else:
show_window(name)
def watch_window(name):
i = inotify.adapters.Inotify()
i.add_watch(format_path(name, ''))
def read_status():
with open_file(name, 'status') as f:
return f.read()
status = read_status()
print(status, flush=True)
for event in i.event_gen(yield_nones=False):
(_, type_names, _, filename) = event
if 'IN_CLOSE_WRITE' in type_names and filename == 'status':
new_status = read_status()
if status != new_status:
print(new_status, flush=True)
status = new_status
if args.command == 'show':
if args.name is None:
raise ValueError('name must be specified for show commands')
show_window(args.name)
elif args.command == 'hide':
if args.name is None:
hide_all()
else:
hide_window(args.name)
elif args.command == 'toggle':
if args.name is None:
raise ValueError('name must be specified for toggle commands')
toggle_window(args.name)
elif args.command == 'watch':
if args.name is None:
raise ValueError('name must be specified for watch commands')
watch_window(args.name)

View file

@ -0,0 +1 @@
inotify

View file

@ -0,0 +1,10 @@
#!/usr/bin/env python
from distutils.core import setup
setup(
name='ptray',
version='0.1',
scripts=[ 'ptray', 'ptrayctl' ],
packages=['', 'i3sway'],
)

View file

@ -0,0 +1,85 @@
import os
from config import STATE_DIR
def format_path(name, property):
return '{}/{}/{}'.format(STATE_DIR, name, property)
def write_file(dir, basename, content):
os.makedirs('{}/{}'.format(STATE_DIR, dir), exist_ok=True)
path = format_path(dir, basename)
with open(path, 'w') as f:
f.write(content)
def open_file(dir, basename):
path = format_path(dir, basename)
return open(path, 'r')
def add_window(tracking: dict, name, window_id: str):
new_dict = tracking.copy()
new_dict[name] = new_dict.get(name, set()) | {window_id}
return new_dict
def remove_window(tracking: dict, name, window_id: str):
new_dict = tracking.copy()
new_dict[name] = new_dict.get(name, set()) - {window_id}
return new_dict
def read_config(path):
with open(path, 'r') as f:
config = json.load(f)
return config
class StateWriter:
tracked_ids = None
visible_ids = None
config = None
def __init__(self, config):
self.config = config
self.tracked_ids = {name: set() for name in config['items'].keys()}
self.visible_ids = {name: set() for name in config['items'].keys()}
self.tiled_ids = {name: set() for name in config['items'].keys()}
for name, defn in config['items'].items():
write_file(name, 'exec', defn['exec'])
write_file(name, 'status', '')
write_file(name, 'window_ids', '')
def update_window_state(self, name):
if len(self.visible_ids[name]) != 0:
write_file(name, 'status', 'fg')
elif len(self.tracked_ids[name]) != 0:
write_file(name, 'status', 'bg')
else:
write_file(name, 'status', '')
write_file(name, 'window_ids', '\n'.join(self.tracked_ids[name]))
write_file(name, 'tiled_ids', '\n'.join(self.tiled_ids[name]))
def on_visible(self, item_name, window_id, tiled=False):
self.visible_ids[item_name] = self.visible_ids[item_name] | {window_id}
self.update_window_state(item_name)
def on_invisible(self, item_name, window_id):
self.visible_ids[item_name] = self.visible_ids[item_name] - {window_id}
self.update_window_state(item_name)
def on_create(self, item_name, window_id):
self.tracked_ids[item_name] = self.tracked_ids[item_name] | {window_id}
self.update_window_state(item_name)
def on_destroy(self, item_name, window_id):
self.tracked_ids[item_name] = self.tracked_ids[item_name] - {window_id}
self.visible_ids[item_name] = self.visible_ids[item_name] - {window_id}
self.tiled_ids[item_name] = self.tiled_ids[item_name] - {window_id}
self.update_window_state(item_name)
def on_tile(self, item_name, window_id):
self.tiled_ids[item_name] = self.tiled_ids[item_name] | {window_id}
self.update_window_state(item_name)
def on_float(self, item_name, window_id):
self.tiled_ids[item_name] = self.tiled_ids[item_name] - {window_id}
self.update_window_state(item_name)

126
home-manager/jwm/rofi.nix Normal file
View file

@ -0,0 +1,126 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.jainawm;
cat = cfg.catppuccin;
in {
options.jainawm.rofi = {
enable = mkOption {
type = types.bool;
default = true;
};
font = mkOption { type = lib.hm.types.fontType; };
accents = {
primary = mkOption { type = types.str; };
secondary = mkOption { type = types.str; };
};
};
config = {
programs.rofi = let font = cfg.rofi.font;
in {
enable = true;
package = pkgs.rofi-wayland;
plugins = with pkgs; [ rofi-emoji ];
font = "${font.name} ${builtins.toString font.size}";
location = "top-left";
cycle = true;
terminal = cfg.terminal.binPath;
xoffset = 10;
yoffset = 10;
theme = let
font = cfg.fonts.wmOverlay;
inherit (config.lib.formats.rasi) mkLiteral;
in {
"*" = {
transparency = "real";
background-color = mkLiteral "#00000000";
text-color = mkLiteral cat.text;
};
configuration = {
show-icons = true;
# drun = {
# display-name = "";
# };
};
window = {
background-color = mkLiteral "${cat.base}cc";
border = mkLiteral "3px";
border-color = mkLiteral cfg.rofi.accents.primary;
border-radius = mkLiteral "10px";
x-offset = mkLiteral "10px";
y-offset = mkLiteral "10px";
width = mkLiteral "500px";
location = mkLiteral "northwest";
};
mainbox = { padding = mkLiteral "1em"; };
inputbar = {
margin = mkLiteral "0px 5px";
children = [ (mkLiteral "prompt") (mkLiteral "entry") ];
};
prompt = { vertical-align = mkLiteral "0.5"; };
entry = {
border-radius = mkLiteral "100%";
padding = mkLiteral "5px 15px";
margin = mkLiteral "0px 0px 0px 10px";
background-color = mkLiteral "${cat.overlay0}cc";
vertical-align = mkLiteral "0.5";
};
message = {
horizontal-align = mkLiteral "1.0";
text-color = mkLiteral cat.subtext0;
margin = mkLiteral "0px 5px";
};
textbox = {
horizontal-align = mkLiteral "inherit";
text-color = mkLiteral "inherit";
};
listview = {
margin = mkLiteral "15px 0px 0px 0px";
lines = 10;
fixed-height = false;
dynamic = true;
cycle = false;
};
element = {
border-radius = mkLiteral "4px";
padding = mkLiteral "5px";
};
"element.active" = {
#background-color = mkLiteral "${cat.overlay0}cc";
border = mkLiteral "2px";
border-color = mkLiteral cfg.rofi.accents.secondary;
};
"element.selected" = {
background-color = mkLiteral cfg.rofi.accents.primary;
text-color = mkLiteral cat.base;
};
element-icon = { size = mkLiteral "20px"; };
element-text = {
text-color = mkLiteral "inherit";
vertical-align = mkLiteral "0.5";
margin = mkLiteral "0px 0px 0px 5px";
};
};
};
};
}

213
home-manager/jwm/sway.nix Normal file
View file

@ -0,0 +1,213 @@
{ config, lib, pkgs, nixpkgs, ... }:
with lib;
let
cfg = config.jainawm;
ptray = config.jainawm.ptray.finalPackage;
in {
options.jainawm.sway = {
enable = mkEnableOption "Enable the use of swaywm, a tiling window manager";
useFx = mkEnableOption "Replace sway with swayfx";
};
config = let cat = cfg.catppuccin;
in mkIf cfg.enable {
home.packages = with pkgs; [ wlr-randr ];
wayland.windowManager.sway = {
enable = cfg.sway.enable;
# package = pkgs.sway.override {
# sway-unwrapped = pkgs.sway-unwrapped.override {
# wlroots_0_16 = pkgs.wlroots_0_16.overrideAttrs (final: prev: {
# pname = prev.pname + "-displaylink";
# patches = [
# (pkgs.fetchpatch {
# url =
# "https://gitlab.freedesktop.org/wlroots/wlroots/uploads/b4f932e370ed03d88f202191eaf60965/DisplayLink.patch";
# sha256 = "sha256-1HheLsOSnj4OMiA35QCHkWprTNgAeG2tXrGbaQyUrF4=";
# })
# ];
# });
# };
# };
package = let c = config.wayland.windowManager.sway;
in mkIf cfg.sway.useFx (pkgs.unstable.sway.override {
sway-unwrapped = pkgs.unstable.swayfx;
extraSessionCommands = c.extraSessionCommands;
extraOptions = c.extraOptions;
withBaseWrapper = c.wrapperFeatures.base;
withGtkWrapper = c.wrapperFeatures.gtk;
});
systemd.enable = true;
config = {
modifier = "Mod4";
bars = [{ command = "waybar"; }];
gaps.inner = 20;
fonts = let font = cfg.fonts.primary;
in {
names = [ font.name ];
# NOTE: add 0.0 to force conversion to floating point rational
# TODO: Need to cast integer to floating point, what's the right way?
size = font.size + 0.0;
};
colors = {
focused = {
border = cat.surface2;
background = cat.surface2;
text = cat.text;
indicator = cat.green;
childBorder = cat.surface2;
};
focusedInactive = {
border = cat.surface1;
background = cat.surface1;
text = cat.subtext1;
indicator = cat.teal;
childBorder = cat.surface1;
};
unfocused = {
border = cat.surface0;
background = cat.surface0;
text = cat.subtext0;
indicator = cat.teal;
childBorder = cat.surface0;
};
urgent = {
border = cat.red;
background = cat.maroon;
text = cat.text;
indicator = cat.red;
childBorder = cat.red;
};
};
menu = "${pkgs.rofi-wayland}/bin/rofi -show drun -modes drun,run";
terminal = cfg.terminal.binPath;
focus.mouseWarping = true;
focus.wrapping = "yes";
keybindings = let
modifier = config.wayland.windowManager.sway.config.modifier;
screenshotRegion = "jwm screenshot-region";
# swaymsg -t get_tree | jq '.. | select(type == "object") | select (has("type") and (.type == "con" or .type == "floating_con")) | select(.visible) | "\(.rect.x),\(.rect.y) \(.rect.width)x\(.rect.height) \(.name)"' -r | slurp -r | grim -g - - | wl-copy
volUp = "jwm vol 5";
volDown = "jwm vol -5";
volMute = "jwm vol toggle-mute";
brightUp = "jwm bright 5";
brightDown = "jwm bright -5";
musPlay = "playerctl play-pause";
in lib.mkOptionDefault {
"${modifier}+tab" = "workspace next";
"${modifier}+grave" = "workspace prev";
"${modifier}+q" = "exec ptrayctl hide";
"${modifier}+t" = "layout tabbed";
"${modifier}+p" = "mode command";
"${modifier}+i" = "mode app";
"--locked XF86MonBrightnessUp" = "exec ${brightUp}";
"--locked XF86MonBrightnessDown" = "exec ${brightDown}";
"--locked XF86AudioRaiseVolume" = "exec ${volUp}";
"--locked XF86AudioLowerVolume" = "exec ${volDown}";
"--locked XF86AudioMute" = "exec ${volMute}";
"--locked XF86AudioPlay" = "exec ${musPlay}";
"--locked XF86AudioNext" = "exec playerctl next";
"--locked XF86AudioPrev" = "exec playerctl previous";
"--locked XF86AudioStop" = "exec playerctl stop";
"${modifier}+shift+s" = "exec ${screenshotRegion}";
"${modifier}+minus" = "floating toggle";
"${modifier}+shift+equal" = "move to scratchpad";
"${modifier}+equal" =
"rofi -show scratchpad -modes 'scratchpad:~/.config/rofi/bin/scratchpad'";
};
input = {
"type:keyboard" = {
xkb_options = lib.strings.concatStringsSep "," [
"altwin:swap_lalt_lwin"
"altwin:menu_win"
];
};
"type:touchpad" = {
dwt = "enabled";
tap = "enabled";
natural_scroll = "enabled";
middle_emulation = "enabled";
drag = "enabled";
};
};
window.hideEdgeBorders = "smart";
modes = lib.mkOptionDefault {
app = let
discord = "${ptray}/bin/ptrayctl toggle discord";
keepass = "${ptray}/bin/ptrayctl toggle keepass";
web = "jwm web-launcher";
in {
Escape = "mode default";
Return = "mode default";
d = "exec ${discord},, mode default";
k = "exec ${keepass},, mode default";
w = "exec ${web},, mode default";
};
command = let
switch = "jwm switch-launcher";
micMute = "jwm mic toggle-mute";
lock = "jwm lock";
typeEmoji = "rofi -show emoji -emoji-mode stdout | xargs wtype";
in {
Escape = "mode default";
Return = "mode default";
"e" = "exec ${typeEmoji},, mode default";
m = "exec ${micMute},, mode default";
s = "exec ${switch},, mode default";
l = "exec ${lock},, mode default";
};
};
window.commands = [{
command = "floating enable,, sticky enable,, opacity 0.9";
criteria = { title = "Picture-in-Picture"; };
}];
output = { "*" = { bg = lib.mkDefault "${cfg.wallpaper} fill"; }; };
seat = {
"*" = { xcursor_theme = "Catppuccin-Mocha-Light-Cursors 32"; };
};
startup = [
#{ command = ''plhelper switch "$(plhelper who)"''; always = true; }
#{ command = "dropwind"; }
{ command = "mako"; }
{
command = "playerctld";
}
# {
# command =
# "${pkgs.xdg-desktop-portal-wlr}/libexec/xdg-desktop-portal-wlr";
# always = true;
# }
];
};
extraConfig = ''
hide_edge_borders --i3 smart
for_window [title=".*"] inhibit_idle fullscreen
'';
extraOptions = [ "--unsupported-gpu" ];
};
};
}

146
home-manager/jwm/waybar.css Normal file
View file

@ -0,0 +1,146 @@
* {
font-family: 'Iosevka Comfy Duo, Symbols Nerd Font, Font Awesome';
}
window#waybar {
background-color: rgba(0, 0, 0, 0);
box-shadow: inset 0px 8px 5px -5px rgba(17, 17, 27, 0.9);
}
window#waybar > box.horizontal {
margin: 8px 10px;
margin-bottom: 0px;
border-radius: 10px;
padding: 5px 10px;
background-color: rgba(17, 17, 27, 0.9);
/* background-color: @theme_unfocused_base_color; */
}
box.modules-left > widget > *,
box.modules-left > box,
box.modules-center > widget > *,
box.modules-center > box,
box.modules-right > widget > *,
box.modules-right > box
{
background-color: @borders;
border-radius: 10px;
margin: 0px 10px;
}
#tray > widget > image,
box.modules-left widget > *,
box.modules-center widget > *,
box.modules-right widget > * {
padding: 0px 8px;
}
box.modules-left box widget:first-child > *,
box.modules-center box widget:first-child > *,
box.modules-right box widget:first-child > * {
padding-left: 10px;
border-bottom-left-radius: 10px;
border-top-left-radius: 10px;
}
box.modules-left box widget:last-child > *,
box.modules-center box widget:last-child > *,
box.modules-right box widget:last-child > * {
padding-right: 10px;
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
}
/* Light up */
#custom-player-icon.playing,
#workspaces button.focused,
#workspaces button.active {
background-color: @theme_selected_bg_color;
color: @theme_selected_fg_color;
}
/* Grey out */
#custom-mpris.paused,
#pulseaudio.input.source-muted {
color: #a6adc8;
}
/* individual customizations */
#workspaces { padding: 0px; }
#workspaces button {
font-size: 90%;
min-width: 12px;
padding: 0px 8px;
border-radius: 10px;
}
#workspaces button.focused,
#workspaces button.active,
#workspaces button:hover {
font-size: 100%;
}
window#waybar.empty .modules-center > widget > * {
background-color: rgba(0, 0, 0, 0);
}
#idle_inhibitor.activated {
color: @theme_bg_color;
background-color: @theme_fg_color;
}
#tray { padding: 0px; }
#custom-discord.fg { background-color: #7289DA; /* blurple */ }
#custom-discord.bg { background-color: #4E5D94; /* dark blurple*/ }
#custom-keepass.fg { background-color: #E6A117; }
#custom-keepass.bg { background-color: #B37D12; }
#pulseaudio.output-icon.muted { color: @error_color; }
#wireplumber.output.muted, #pulseaudio.output.muted {
/*color: @theme_unfocused_text_color;*/
color: #a6adc8;
text-decoration: line-through;
}
#pulseaudio.input.source-muted { color: #a6adc8; }
#pulseaudio.output-icon {
padding-right: 0px;
font-size: 18px
}
#wireplumber.output, #pulseaudio.output {
padding-left: 5px;
}
#pulseaudio.input {
padding-left: 5px;
}
#battery {
padding-left: 4px;
padding-right: 0px;
}
#battery.secondary {
padding: 0px;
}
#battery.critical:not(.charging) {
/*color: #f53c3c;*/
color: @error_color;
/*color: #ffffff;*/
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes blink {
to {
background-color: @theme_fg_color;
color: @theme_bg_color;
}
}
#upower {
padding-left: 0px;
}

225
home-manager/jwm/waybar.nix Normal file
View file

@ -0,0 +1,225 @@
{ lib, pkgs, config, ... }:
with lib;
let
ptray = config.jainawm.ptray.finalPackage;
watchPtrayIcon = name: displayName:
pkgs.writeShellScript "watchPtrayIcon-${name}" ''
trap 'kill 0' EXIT
${ptray}/bin/ptrayctl watch ${name} | while read line; do
echo '{"text":"update","tooltip":"${displayName}","class":"'"$line"'"}'
done
'';
in {
options.jainawm.waybar = { enable = mkEnableOption "Enable waybar"; };
config = {
programs.waybar = {
enable = true;
style = builtins.readFile ./waybar.css;
#package = pkgs.unstable.waybar;
settings = [{
layer = "top";
position = "top";
mode = "dock";
name = "main";
modules-left = [
"sway/workspaces"
"sway/mode"
"group/musicplayer"
];
modules-center = [ "sway/window" ];
modules-right = [
"group/tray"
"group/volume"
"group/battery"
"clock"
];
"group/musicplayer" = {
orientation = "horizontal";
modules = [ "custom/player-icon" "custom/mpris" ];
};
"group/tray" = {
orientation = "horizontal";
modules =
[ "idle_inhibitor" "tray" "custom/discord" "custom/keepass" ];
};
"tray" = { };
"group/volume" = {
orientation = "horizontal";
modules =
[ "pulseaudio#output-icon" "pulseaudio#output" "pulseaudio#input" ];
};
"group/battery" = {
orientation = "horizontal";
modules = [ "battery" "upower" ];
};
"custom/player-icon" =
let playerctl = "${pkgs.playerctl}/bin/playerctl --player strawberry";
in {
format = "{icon}";
format-icons = "";
exec = ''
trap 'kill 0' EXIT
(
echo "";
${playerctl} status --follow --format \
'{"text":"music","class":"{{lc(status)}}","tooltip":"strawberry"}'
) | (
while read line; do
if [ -n "$line" ]; then
echo "$line"
else
echo '{"text":"music","tooltip":"strawberry"}'
fi
done
);
'';
on-click = ''
${pkgs.sway}/bin/swaymsg '[app_id=org.strawberrymusicplayer.] focus' >/dev/null \
|| swaymsg exec strawberry
'';
return-type = "json";
};
"custom/mpris" =
let playerctl = "${pkgs.playerctl}/bin/playerctl --player strawberry";
in {
format = "{icon} {}";
format-icons = {
playing = "";
paused = "";
};
exec = let
playerctlFormat = ''
{"title":"{{title}}","artist":"{{artist}}","album":"{{album}}","position":"{{duration(position)}}","length":"{{duration(mpris:length)}}","status":"{{lc(status)}}"}'';
jqFilter = ''
def escape: . | gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;");
try (
. + {
mainText: ("\([.title,.artist,.album] | map(select(length > 0)) | join(" - "))") | escape,
progressText: ((select((.position | length) > 0 and (.length | length > 0)) | "[\(.position)/\(.length)]" | escape) // "")
} | . + {
progress: ((. | select(.progressText | length > 0) | " <small>\(.progressText)</small>") // "")
} | {
text: (.mainText + .progress),
alt: (.status)
} | { text, alt, tooltip: (.text), class: (.alt) }
) catch ""
'';
mainFormat =
"{{title}} - {{artist}} - {{album}} <small>[{{duration(position)}}/{{duration(mpris:length)}}]</small>";
in ''
trap 'kill 0' EXIT
${playerctl} metadata --follow --format '${playerctlFormat}' | (
# playerctl has a bug where it only shows that a stopped stream has stopped after it starts again. but it does spit out an empty line. so we do a lot of work special-casing those
while read line; do
if [ -n "$line" ]; then echo "$line"; else echo '" "'; fi
done
) | jq '${jqFilter}' --unbuffered --compact-output --raw-output
'';
max-length = 80;
on-click = "${playerctl} play-pause";
on-click-middle = "${playerctl} previous";
on-click-right = "${playerctl} next";
return-type = "json";
};
idle_inhibitor = {
format = "{icon}";
format-icons = {
activated = "";
deactivated = "";
};
tooltip-format-activated = "Idle inhibitor enabled";
tooltip-format-deactivated = "Idle inhibitor disabled";
};
"custom/discord" = {
format = "";
exec = watchPtrayIcon "discord" "Discord";
on-click = "${ptray}/bin/ptrayctl toggle discord";
restart-interval = 5;
return-type = "json";
};
"custom/keepass" = {
format = "";
exec = watchPtrayIcon "keepass" "KeePassXC (Password Manager)";
# exec =
# "~/.config/waybar/scripts/watchstatus keepass KeePassXC 'KeePassXC (Password Manager)'";
#return-type = "json";
on-click = "${ptray}/bin/ptrayctl toggle keepass";
on-click-middle = "killall keepassxc";
restart-interval = 5;
return-type = "json";
};
"pulseaudio#output-icon" = {
format = "{icon}";
format-muted = "󰸈";
format-icons = {
headphone = "";
hands-free = "";
headset = "";
phone = "";
portable = "";
car = "";
default = [ "󰕿" "󰖀" "󰕾" ];
};
on-click = "jwm vol toggle-mute";
on-click-right = "dropwinctl toggle pavucontrol";
};
"pulseaudio#output" = {
format = "{volume}%";
on-click = "jwm vol toggle-mute";
on-click-right = "dropwinctl toggle pavucontrol";
};
"pulseaudio#input" = {
format = "{format_source}";
format-source = "";
format-source-muted = "";
on-click = "jwm mic toggle-mute";
on-click-right = "dropwinctl toggle pavucontrol";
};
battery = {
bat = "BAT0";
states = {
warning = 30;
critical = 15;
};
format = "{icon}";
rotate = 90;
format-charging = "";
tooltip-format = "Battery: {capacity}%";
format-icons = [ "" "" "" "" "" "" "" "" "" "" ];
};
upower = {
format = "{percentage}";
format-alt = "{time}";
icon-size = 1;
};
clock = {
tooltip-format = ''
<big>{:%Y %B}</big>
<tt><small>{calendar}</small></tt>'';
};
}];
};
};
}