qutebrowser: Own file
This commit is contained in:
parent
597b50ebef
commit
ecc6cb983d
21 changed files with 103 additions and 95 deletions
53
hm/desktop/batteryNotify.sh
Executable file
53
hm/desktop/batteryNotify.sh
Executable file
|
@ -0,0 +1,53 @@
|
|||
BATT="/sys/class/power_supply/BAT0"
|
||||
LOW=10
|
||||
CRIT=3
|
||||
LASTSTATE="$HOME/.cache/batteryState"
|
||||
|
||||
function setState() { # state [...notify-send arguments]
|
||||
state="$1"
|
||||
last="$(cat "$LASTSTATE" 2> /dev/null)"
|
||||
shift
|
||||
|
||||
echo "Battery state: $state"
|
||||
|
||||
if [ "$state" != "$last" ]
|
||||
then
|
||||
notify-send "$@"
|
||||
echo "$state" > "$LASTSTATE"
|
||||
fi
|
||||
}
|
||||
|
||||
function computeState() {
|
||||
acpiStatus="$(cat "$BATT/status")"
|
||||
acpiCapacity="$(cat "$BATT/capacity")"
|
||||
|
||||
if [ "$acpiStatus" == "Discharging" ]
|
||||
then
|
||||
if [ "$acpiCapacity" -le $CRIT ]
|
||||
then
|
||||
setState "CRIT" -u critical -i battery-caution "Battery level is critical" "$acpiCapacity %"
|
||||
elif [ "$acpiCapacity" -le $LOW ]
|
||||
then
|
||||
setState "LOW" -u critical -i battery-low "Battery level is low" "$acpiCapacity %"
|
||||
else
|
||||
setState "DISCHARGING" -i battery-good "Battery is discharging" "$acpiCapacity %"
|
||||
fi
|
||||
elif [ "$acpiStatus" == "Charging" ]
|
||||
then
|
||||
setState "CHARGING" -u normal -i battery-good-charging "Battery is charging" "$acpiCapacity %"
|
||||
elif [ "$acpiStatus" == "Full" ]
|
||||
then
|
||||
setState "FULL" -u low -i battery-full-charged "Battery is full" "$acpiCapacity %"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" == "-d" ]
|
||||
then
|
||||
while true
|
||||
do
|
||||
computeState
|
||||
sleep 10
|
||||
done
|
||||
else
|
||||
computeState
|
||||
fi
|
620
hm/desktop/default.nix
Normal file
620
hm/desktop/default.nix
Normal file
|
@ -0,0 +1,620 @@
|
|||
{ pkgs, config, lib, ... }:
|
||||
let
|
||||
nixGLIntelPrefix = "${pkgs.nixgl.nixVulkanIntel}/bin/nixVulkanIntel ${pkgs.nixgl.nixGLIntel}/bin/nixGLIntel ";
|
||||
wmPrefix = "${lib.optionalString config.frogeye.desktop.nixGLIntel nixGLIntelPrefix}";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./frobar
|
||||
./qutebrowser.nix
|
||||
];
|
||||
config = lib.mkIf config.frogeye.desktop.xorg {
|
||||
frogeye.shellAliases = {
|
||||
noise = ''${pkgs.sox}/bin/play -c 2 -n synth $'' + ''{1}noise'';
|
||||
beep = ''${pkgs.sox}/bin/play -n synth sine E5 sine A4 remix 1-2 fade 0.5 1.2 0.5 2> /dev/null'';
|
||||
|
||||
# n = "$HOME/.config/i3/terminal & disown"; # Not used anymore since alacritty daemon mode doesn't preserve environment variables
|
||||
x = "startx ${config.home.homeDirectory}/${config.xsession.scriptPath}; logout";
|
||||
# TODO Is it possible to not start nvidia stuff on nixOS?
|
||||
# nx = "nvidia-xrun ${config.xsession.scriptPath}; sudo systemctl start nvidia-xrun-pm; logout";
|
||||
};
|
||||
xsession = {
|
||||
enable = true;
|
||||
# Not using config.xdg.configHome because it needs to be $HOME-relative paths and path manipulation is hard
|
||||
scriptPath = ".config/xsession";
|
||||
profilePath = ".config/xprofile";
|
||||
windowManager = {
|
||||
command = lib.mkForce "${wmPrefix} ${config.xsession.windowManager.i3.package}/bin/i3";
|
||||
i3 = {
|
||||
enable = true;
|
||||
config =
|
||||
let
|
||||
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base00; b = base01; d = base00; }; # Black or White, depending on current theme
|
||||
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base0A; b = base0B; d = base00; }; # Green + Yellow
|
||||
lockColors = { a = "#82a401"; b = "#466c01"; d = "#648901"; }; # Old
|
||||
lockSvg = pkgs.writeText "lock.svg" "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\" height=\"50\" width=\"50\"><path fill=\"${lockColors.a}\" d=\"M0 50h50V0H0z\"/><path d=\"M0 0l50 50H25L0 25zm50 0v25L25 0z\" fill=\"${lockColors.b}\"/></svg>";
|
||||
lockPng = pkgs.runCommand "lock.png" { } "${pkgs.imagemagick}/bin/convert ${lockSvg} $out";
|
||||
locker = pkgs.writeShellScript "i3-locker"
|
||||
''
|
||||
# Remove SSH and GPG keys from keystores
|
||||
${pkgs.openssh}/bin/ssh-add -D
|
||||
echo RELOADAGENT | ${pkgs.gnupg}/bin/gpg-connect-agent
|
||||
${pkgs.coreutils}/bin/rm -rf "/tmp/cached_pass_$UID"
|
||||
|
||||
${pkgs.lightdm}/bin/dm-tool lock
|
||||
# TODO Does that work for all DMs?
|
||||
# TODO Might want to use i3lock on NixOS configs still?
|
||||
if [ $? -ne 0 ]; then
|
||||
if [ -d ${config.xdg.cacheHome}/lockpatterns ]
|
||||
then
|
||||
pattern=$(${pkgs.findutils} ${config.xdg.cacheHome}/lockpatterns | sort -R | head -1)
|
||||
else
|
||||
pattern=${lockPng}
|
||||
fi
|
||||
revert() {
|
||||
${pkgs.xorg.xset}/bin/xset dpms 0 0 0
|
||||
}
|
||||
trap revert SIGHUP SIGINT SIGTERM
|
||||
${pkgs.xorg.xset}/bin/xset dpms 5 5 5
|
||||
${pkgs.i3lock}/bin/i3lock --nofork --color ${builtins.substring 1 6 lockColors.d} --image=$pattern --tiling --ignore-empty-password
|
||||
revert
|
||||
fi
|
||||
'';
|
||||
focus = "exec ${ pkgs.writeShellScript "i3-focus-window"
|
||||
''
|
||||
WINDOW=`${pkgs.xdotool}/bin/xdotool getwindowfocus`
|
||||
eval `${pkgs.xdotool}/bin/xdotool getwindowgeometry --shell $WINDOW` # this brings in variables WIDTH and HEIGHT
|
||||
TX=`${pkgs.coreutils}/bin/expr $WIDTH / 2`
|
||||
TY=`${pkgs.coreutils}/bin/expr $HEIGHT / 2`
|
||||
${pkgs.xdotool}/bin/xdotool mousemove -window $WINDOW $TX $TY
|
||||
''
|
||||
}";
|
||||
mode_system = "[L] Vérouillage [E] Déconnexion [S] Veille [H] Hibernation [R] Redémarrage [P] Extinction";
|
||||
mode_resize = "Resize";
|
||||
mode_pres_main = "Presentation (main display)";
|
||||
mode_pres_sec = "Presentation (secondary display)";
|
||||
mode_screen = "Screen setup [A] Auto [L] Load [S] Save [R] Remove [D] Default";
|
||||
mode_temp = "Temperature [R] Red [D] Dust storm [C] Campfire [O] Normal [A] All nighter [B] Blue";
|
||||
fonts = config.stylix.fonts;
|
||||
in
|
||||
{
|
||||
modifier = "Mod4";
|
||||
fonts = {
|
||||
names = [ fonts.sansSerif.name ];
|
||||
};
|
||||
terminal = "alacritty";
|
||||
colors = let ignore = "#ff00ff"; in
|
||||
with config.lib.stylix.colors.withHashtag; lib.mkForce {
|
||||
focused = { border = base0B; background = base0B; text = base00; indicator = base00; childBorder = base0B; };
|
||||
focusedInactive = { border = base02; background = base02; text = base05; indicator = base02; childBorder = base02; };
|
||||
unfocused = { border = base05; background = base04; text = base00; indicator = base04; childBorder = base00; };
|
||||
urgent = { border = base0F; background = base08; text = base00; indicator = base08; childBorder = base0F; };
|
||||
placeholder = { border = ignore; background = base00; text = base05; indicator = ignore; childBorder = base00; };
|
||||
background = base07;
|
||||
# I set the color of the active tab as the the background color of the terminal so they merge together.
|
||||
};
|
||||
focus.followMouse = false;
|
||||
keybindings =
|
||||
let
|
||||
mod = config.xsession.windowManager.i3.config.modifier;
|
||||
rofi = "exec --no-startup-id ${config.programs.rofi.package}/bin/rofi";
|
||||
pactl = "exec ${pkgs.pulseaudio}/bin/pactl"; # TODO Use NixOS package if using NixOS
|
||||
screenshots_dir = config.xdg.userDirs.extraConfig.XDG_SCREENSHOTS_DIR;
|
||||
scrot = "${pkgs.scrot}/bin/scrot --exec '${pkgs.coreutils}/bin/mv $f ${screenshots_dir}/ && ${pkgs.optipng}/bin/optipng ${screenshots_dir}/$f'";
|
||||
in
|
||||
{
|
||||
# Compatibility layer for people coming from other backgrounds
|
||||
"Mod1+Tab" = "${rofi} -modi window -show window";
|
||||
"Mod1+F2" = "${rofi} -modi drun -show drun";
|
||||
"Mod1+F4" = "kill";
|
||||
# kill focused window
|
||||
"${mod}+z" = "kill";
|
||||
button2 = "kill";
|
||||
# Rofi
|
||||
"${mod}+c" = "exec --no-startup-id ${config.programs.rofi.pass.package}/bin/rofi-pass --last-used";
|
||||
# TODO Try autopass.cr
|
||||
# 23.11 config.programs.rofi.pass.package
|
||||
"${mod}+i" = "exec --no-startup-id ${pkgs.rofimoji}/bin/rofimoji";
|
||||
"${mod}+plus" = "${rofi} -modi ssh -show ssh";
|
||||
"${mod}+ù" = "${rofi} -modi ssh -show ssh -ssh-command '{terminal} -e {ssh-client} {host} -t \"sudo -s -E\"'";
|
||||
# TODO In which keyboard layout?
|
||||
"${mod}+Tab" = "${rofi} -modi window -show window";
|
||||
# start program launcher
|
||||
"${mod}+d" = "${rofi} -modi run -show run";
|
||||
"${mod}+Shift+d" = "${rofi} -modi drun -show drun";
|
||||
# Start Applications
|
||||
"${mod}+Return" = "exec ${
|
||||
pkgs.writeShellScript "terminal" "${config.programs.alacritty.package}/bin/alacritty msg create-window -e zsh || exec ${config.programs.alacritty.package}/bin/alacritty -e zsh"
|
||||
# -e zsh is for systems where I can't configure my user's shell
|
||||
# TODO Is a shell script even required?
|
||||
}";
|
||||
"${mod}+Shift+Return" = "exec ${config.programs.urxvt.package}/bin/urxvt";
|
||||
"${mod}+p" = "exec ${pkgs.xfce.thunar}/bin/thunar";
|
||||
# Volume control
|
||||
"XF86AudioRaiseVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ +5%";
|
||||
"XF86AudioLowerVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ -5%";
|
||||
"XF86AudioMute" = "${pactl} set-sink-mute @DEFAULT_SINK@ true";
|
||||
"${mod}+F7" = "${pactl} suspend-sink @DEFAULT_SINK@ 1; ${pactl} suspend-sink @DEFAULT_SINK@ 0"; # Re-synchronize bluetooth headset
|
||||
"${mod}+F11" = "exec ${pkgs.pavucontrol}/bin/pavucontrol";
|
||||
"${mod}+F12" = "exec ${pkgs.pavucontrol}/bin/pavucontrol";
|
||||
# TODO Find pacmixer?
|
||||
# Media control
|
||||
"XF86AudioPrev" = "exec ${pkgs.mpc-cli}/bin/mpc prev";
|
||||
"XF86AudioPlay" = "exec ${pkgs.mpc-cli}/bin/mpc toggle";
|
||||
"XF86AudioNext" = "exec ${pkgs.mpc-cli}/bin/mpc next";
|
||||
# Backlight
|
||||
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set +5%";
|
||||
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
|
||||
# Misc
|
||||
"${mod}+F10" = "exec ${ pkgs.writeShellScript "show-keyboard-layout"
|
||||
''
|
||||
layout=`${pkgs.xorg.setxkbmap}/bin/setxkbmap -query | ${pkgs.gnugrep}/bin/grep ^layout: | ${pkgs.gawk}/bin/awk '{ print $2 }'`
|
||||
${pkgs.libgnomekbd}/bin/gkbd-keyboard-display -l $layout
|
||||
''
|
||||
}";
|
||||
# Screenshots
|
||||
"Print" = "exec ${scrot} --focused";
|
||||
"${mod}+Print" = "exec ${scrot}";
|
||||
"Ctrl+Print" = "exec ${pkgs.coreutils}/bin/sleep 1 && ${scrot} --select";
|
||||
# TODO Try using bindsym --release instead of sleep
|
||||
# change focus
|
||||
"${mod}+h" = "focus left; ${focus}";
|
||||
"${mod}+j" = "focus down; ${focus}";
|
||||
"${mod}+k" = "focus up; ${focus}";
|
||||
"${mod}+l" = "focus right; ${focus}";
|
||||
# move focused window
|
||||
"${mod}+Shift+h" = "move left; ${focus}";
|
||||
"${mod}+Shift+j" = "move down; ${focus}";
|
||||
"${mod}+Shift+k" = "move up; ${focus}";
|
||||
"${mod}+Shift+l" = "move right; ${focus}";
|
||||
# workspace back and forth (with/without active container)
|
||||
"${mod}+b" = "workspace back_and_forth; ${focus}";
|
||||
"${mod}+Shift+b" = "move container to workspace back_and_forth; workspace back_and_forth; ${focus}";
|
||||
# Change container layout
|
||||
"${mod}+g" = "split h; ${focus}";
|
||||
"${mod}+v" = "split v; ${focus}";
|
||||
"${mod}+f" = "fullscreen toggle; ${focus}";
|
||||
"${mod}+s" = "layout stacking; ${focus}";
|
||||
"${mod}+w" = "layout tabbed; ${focus}";
|
||||
"${mod}+e" = "layout toggle split; ${focus}";
|
||||
"${mod}+Shift+space" = "floating toggle; ${focus}";
|
||||
# Focus container
|
||||
"${mod}+space" = "focus mode_toggle; ${focus}";
|
||||
"${mod}+a" = "focus parent; ${focus}";
|
||||
"${mod}+q" = "focus child; ${focus}";
|
||||
# Switch to workspace
|
||||
"${mod}+1" = "workspace 1; ${focus}";
|
||||
"${mod}+2" = "workspace 2; ${focus}";
|
||||
"${mod}+3" = "workspace 3; ${focus}";
|
||||
"${mod}+4" = "workspace 4; ${focus}";
|
||||
"${mod}+5" = "workspace 5; ${focus}";
|
||||
"${mod}+6" = "workspace 6; ${focus}";
|
||||
"${mod}+7" = "workspace 7; ${focus}";
|
||||
"${mod}+8" = "workspace 8; ${focus}";
|
||||
"${mod}+9" = "workspace 9; ${focus}";
|
||||
"${mod}+0" = "workspace 10; ${focus}";
|
||||
# TODO Prevent repetitions, see workspace assignation for example
|
||||
#navigate workspaces next / previous
|
||||
"${mod}+Ctrl+h" = "workspace prev_on_output; ${focus}";
|
||||
"${mod}+Ctrl+l" = "workspace next_on_output; ${focus}";
|
||||
"${mod}+Ctrl+j" = "workspace prev; ${focus}";
|
||||
"${mod}+Ctrl+k" = "workspace next; ${focus}";
|
||||
# Move to workspace next / previous with focused container
|
||||
"${mod}+Ctrl+Shift+h" = "move container to workspace prev_on_output; workspace prev_on_output; ${focus}";
|
||||
"${mod}+Ctrl+Shift+l" = "move container to workspace next_on_output; workspace next_on_output; ${focus}";
|
||||
"${mod}+Ctrl+Shift+j" = "move container to workspace prev; workspace prev; ${focus}";
|
||||
"${mod}+Ctrl+Shift+k" = "move container to workspace next; workspace next; ${focus}";
|
||||
# move focused container to workspace
|
||||
"${mod}+ctrl+1" = "move container to workspace 1; ${focus}";
|
||||
"${mod}+ctrl+2" = "move container to workspace 2; ${focus}";
|
||||
"${mod}+ctrl+3" = "move container to workspace 3; ${focus}";
|
||||
"${mod}+ctrl+4" = "move container to workspace 4; ${focus}";
|
||||
"${mod}+ctrl+5" = "move container to workspace 5; ${focus}";
|
||||
"${mod}+ctrl+6" = "move container to workspace 6; ${focus}";
|
||||
"${mod}+ctrl+7" = "move container to workspace 7; ${focus}";
|
||||
"${mod}+ctrl+8" = "move container to workspace 8; ${focus}";
|
||||
"${mod}+ctrl+9" = "move container to workspace 9; ${focus}";
|
||||
"${mod}+ctrl+0" = "move container to workspace 10; ${focus}";
|
||||
# move to workspace with focused container
|
||||
"${mod}+shift+1" = "move container to workspace 1; workspace 1; ${focus}";
|
||||
"${mod}+shift+2" = "move container to workspace 2; workspace 2; ${focus}";
|
||||
"${mod}+shift+3" = "move container to workspace 3; workspace 3; ${focus}";
|
||||
"${mod}+shift+4" = "move container to workspace 4; workspace 4; ${focus}";
|
||||
"${mod}+shift+5" = "move container to workspace 5; workspace 5; ${focus}";
|
||||
"${mod}+shift+6" = "move container to workspace 6; workspace 6; ${focus}";
|
||||
"${mod}+shift+7" = "move container to workspace 7; workspace 7; ${focus}";
|
||||
"${mod}+shift+8" = "move container to workspace 8; workspace 8; ${focus}";
|
||||
"${mod}+shift+9" = "move container to workspace 9; workspace 9; ${focus}";
|
||||
"${mod}+shift+0" = "move container to workspace 10; workspace 10; ${focus}";
|
||||
# move workspaces to screen (arrow keys)
|
||||
"${mod}+ctrl+shift+Right" = "move workspace to output right; ${focus}";
|
||||
"${mod}+ctrl+shift+Left" = "move workspace to output left; ${focus}";
|
||||
"${mod}+Ctrl+Shift+Up" = "move workspace to output above; ${focus}";
|
||||
"${mod}+Ctrl+Shift+Down" = "move workspace to output below; ${focus}";
|
||||
# i3 control
|
||||
"${mod}+Shift+c" = "reload";
|
||||
"${mod}+Shift+r" = "restart";
|
||||
"${mod}+Shift+e" = "exit";
|
||||
# Screen off commands
|
||||
"${mod}+F1" = "exec --no-startup-id ${pkgs.bash}/bin/sh -c \"${pkgs.coreutils}/bin/sleep .25 && ${pkgs.xorg.xset}/bin/xset dpms force off\"";
|
||||
# TODO --release?
|
||||
"${mod}+F4" = "exec --no-startup-id ${pkgs.xautolock}/bin/xautolock -disable";
|
||||
"${mod}+F5" = "exec --no-startup-id ${pkgs.xautolock}/bin/xautolock -enable";
|
||||
# Modes
|
||||
"${mod}+Escape" = "mode ${mode_system}";
|
||||
"${mod}+r" = "mode ${mode_resize}";
|
||||
"${mod}+Shift+p" = "mode ${mode_pres_main}";
|
||||
"${mod}+t" = "mode ${mode_screen}";
|
||||
"${mod}+y" = "mode ${mode_temp}";
|
||||
};
|
||||
modes = let return_bindings = {
|
||||
"Return" = "mode default";
|
||||
"Escape" = "mode default";
|
||||
}; in
|
||||
{
|
||||
"${mode_system}" = {
|
||||
"l" = "exec --no-startup-id exec ${locker}, mode default";
|
||||
"e" = "exit, mode default";
|
||||
"s" = "exec --no-startup-id exec ${locker} & ${pkgs.systemd}/bin/systemctl suspend --check-inhibitors=no, mode default";
|
||||
"h" = "exec --no-startup-id exec ${locker} & ${pkgs.systemd}/bin/systemctl hibernate, mode default";
|
||||
"r" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl reboot, mode default";
|
||||
"p" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl poweroff -i, mode default";
|
||||
} // return_bindings;
|
||||
"${mode_resize}" = {
|
||||
"h" = "resize shrink width 10 px or 10 ppt; ${focus}";
|
||||
"j" = "resize grow height 10 px or 10 ppt; ${focus}";
|
||||
"k" = "resize shrink height 10 px or 10 ppt; ${focus}";
|
||||
"l" = "resize grow width 10 px or 10 ppt; ${focus}";
|
||||
} // return_bindings;
|
||||
"${mode_pres_main}" = {
|
||||
"b" = "workspace 3, workspace 4, mode ${mode_pres_sec}";
|
||||
"q" = "mode default";
|
||||
"Return" = "mode default";
|
||||
};
|
||||
"${mode_pres_sec}" = {
|
||||
"b" = "workspace 1, workspace 2, mode ${mode_pres_main}";
|
||||
"q" = "mode default";
|
||||
"Return" = "mode default";
|
||||
};
|
||||
"${mode_screen}" =
|
||||
let
|
||||
builtin_configs = [ "off" "common" "clone-largest" "horizontal" "vertical" "horizontal-reverse" "vertical-reverse" ];
|
||||
autorandrmenu = { title, option, builtin ? false }: pkgs.writeShellScript "autorandrmenu"
|
||||
''
|
||||
shopt -s nullglob globstar
|
||||
profiles="${if builtin then lib.strings.concatLines builtin_configs else ""}$(${pkgs.autorandr}/bin/autorandr | ${pkgs.gawk}/bin/awk '{ print $1 }')"
|
||||
profile="$(echo "$profiles" | ${config.programs.rofi.package}/bin/rofi -dmenu -p "${title}")"
|
||||
[[ -n "$profile" ]] || exit
|
||||
${pkgs.autorandr}/bin/autorandr ${option} "$profile"
|
||||
'';
|
||||
in
|
||||
{
|
||||
"a" = "exec ${pkgs.autorandr}/bin/autorandr --change --force, mode default";
|
||||
"l" = "exec ${autorandrmenu {title="Load profile"; option="--load"; builtin = true;}}, mode default";
|
||||
"s" = "exec ${autorandrmenu {title="Save profile"; option="--save";}}, mode default";
|
||||
"r" = "exec ${autorandrmenu {title="Remove profile"; option="--remove";}}, mode default";
|
||||
"d" = "exec ${autorandrmenu {title="Default profile"; option="--default"; builtin = true;}}, mode default";
|
||||
} // return_bindings;
|
||||
"${mode_temp}" = {
|
||||
"r" = "exec ${pkgs.sct}/bin/sct 1000";
|
||||
"d" = "exec ${pkgs.sct}/bin/sct 2000";
|
||||
"c" = "exec ${pkgs.sct}/bin/sct 4500";
|
||||
"o" = "exec ${pkgs.sct}/bin/sct";
|
||||
"a" = "exec ${pkgs.sct}/bin/sct 8000";
|
||||
"b" = "exec ${pkgs.sct}/bin/sct 10000";
|
||||
} // return_bindings;
|
||||
};
|
||||
window = {
|
||||
hideEdgeBorders = "both";
|
||||
titlebar = false; # So that single-container screens are basically almost fullscreen
|
||||
commands = [
|
||||
# Open specific applications in floating mode
|
||||
{ criteria = { title = "^pdfpc.*"; window_role = "presenter"; }; command = "move to output left, fullscreen"; }
|
||||
{ criteria = { title = "^pdfpc.*"; window_role = "presentation"; }; command = "move to output right, fullscreen"; }
|
||||
# switch to workspace with urgent window automatically
|
||||
{ criteria = { urgent = "latest"; }; command = "focus"; }
|
||||
];
|
||||
};
|
||||
floating = {
|
||||
criteria = [
|
||||
{ title = "pacmixer"; }
|
||||
{ window_role = "pop-up"; }
|
||||
{ window_role = "task_dialog"; }
|
||||
];
|
||||
};
|
||||
startup = [
|
||||
# Lock screen after 10 minutes
|
||||
{ notification = false; command = "${pkgs.xautolock}/bin/xautolock -time 10 -locker '${pkgs.xorg.xset}/bin/xset dpms force standby' -killtime 1 -killer ${locker}"; }
|
||||
{
|
||||
notification = false;
|
||||
command = "${pkgs.writeShellApplication {
|
||||
name = "batteryNotify";
|
||||
runtimeInputs = with pkgs; [coreutils libnotify];
|
||||
text = builtins.readFile ./batteryNotify.sh;
|
||||
# TODO Use batsignal instead?
|
||||
# TODO Only on computers with battery
|
||||
}}/bin/batteryNotify";
|
||||
}
|
||||
# TODO There's a services.screen-locker.xautolock but not sure it can match the above command
|
||||
];
|
||||
workspaceLayout = "tabbed";
|
||||
focus.mouseWarping = true; # i3 only supports warping to workspace, hence ${focus}
|
||||
workspaceOutputAssign =
|
||||
let
|
||||
x11_screens = config.frogeye.desktop.x11_screens;
|
||||
workspaces = map (i: { name = toString i; key = toString (lib.mod i 10); }) (lib.lists.range 1 10);
|
||||
forEachWorkspace = f: map (w: f { w = w; workspace = ((builtins.elemAt workspaces w)); }) (lib.lists.range 0 ((builtins.length workspaces) - 1));
|
||||
in
|
||||
forEachWorkspace ({ w, workspace }: { output = builtins.elemAt x11_screens (lib.mod w (builtins.length x11_screens)); workspace = workspace.name; });
|
||||
};
|
||||
};
|
||||
};
|
||||
numlock.enable = config.frogeye.desktop.numlock;
|
||||
};
|
||||
|
||||
programs = {
|
||||
# Terminal
|
||||
alacritty = {
|
||||
# TODO Emojis. Or maybe they work on NixOS?
|
||||
# Arch (working) shows this with alacritty -vvv:
|
||||
# [TRACE] [crossfont] Got font path="/usr/share/fonts/twemoji/twemoji.ttf", index=0
|
||||
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: MONOCHROME | TARGET_MONO | COLOR, render_mode: "Mono", lcd_filter: 1 }
|
||||
# Nix (not working) shows this:
|
||||
# [TRACE] [crossfont] Got font path="/nix/store/872g3w9vcr5nh93r0m83a3yzmpvd2qrj-home-manager-path/share/fonts/truetype/TwitterColorEmoji-SVGinOT.ttf", index=0
|
||||
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: TARGET_LIGHT | COLOR, render_mode: "Lcd", lcd_filter: 1 }
|
||||
|
||||
enable = true;
|
||||
settings = {
|
||||
bell = {
|
||||
animation = "EaseOutExpo";
|
||||
color = "#000000";
|
||||
command = { program = "${pkgs.sox}/bin/play"; args = [ "-n" "synth" "sine" "C5" "sine" "E4" "remix" "1-2" "fade" "0.1" "0.2" "0.1" ]; };
|
||||
duration = 100;
|
||||
};
|
||||
cursor = { vi_mode_style = "Underline"; };
|
||||
env = {
|
||||
WINIT_X11_SCALE_FACTOR = "1";
|
||||
# Prevents Alacritty from resizing from one monitor to another.
|
||||
# Might cause issue on HiDPI screens but we'll get there when we get there
|
||||
};
|
||||
hints = {
|
||||
enabled = [
|
||||
{
|
||||
binding = { mods = "Control|Alt"; key = "F"; };
|
||||
command = "${pkgs.xdg-utils}/bin/xdg-open";
|
||||
mouse = { enabled = true; mods = "Control"; };
|
||||
post_processing = true;
|
||||
regex = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\\\s{-}\\\\^⟨⟩`]+";
|
||||
}
|
||||
];
|
||||
};
|
||||
key_bindings = [
|
||||
{ mode = "~Search"; mods = "Alt|Control"; key = "Space"; action = "ToggleViMode"; }
|
||||
{ mode = "Vi|~Search"; mods = "Control"; key = "K"; action = "ScrollHalfPageUp"; }
|
||||
{ mode = "Vi|~Search"; mods = "Control"; key = "J"; action = "ScrollHalfPageDown"; }
|
||||
{ mode = "~Vi"; mods = "Control|Alt"; key = "V"; action = "Paste"; }
|
||||
{ mods = "Control|Alt"; key = "C"; action = "Copy"; }
|
||||
{ mode = "~Search"; mods = "Control|Alt"; key = "F"; action = "SearchForward"; }
|
||||
{ mode = "~Search"; mods = "Control|Alt"; key = "B"; action = "SearchBackward"; }
|
||||
{ mode = "Vi|~Search"; mods = "Control|Alt"; key = "C"; action = "ClearSelection"; }
|
||||
];
|
||||
window = {
|
||||
dynamic_padding = false;
|
||||
dynamic_title = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
# Backup terminal
|
||||
urxvt = {
|
||||
enable = true;
|
||||
package = pkgs.rxvt-unicode-emoji;
|
||||
scroll = {
|
||||
bar.enable = false;
|
||||
};
|
||||
iso14755 = false; # Disable Ctrl+Shift default bindings
|
||||
keybindings = {
|
||||
"Shift-Control-C" = "eval:selection_to_clipboard";
|
||||
"Shift-Control-V" = "eval:paste_clipboard";
|
||||
# TODO Not sure resizing works, Nix doesn't have the package (urxvt-resize-font-git on Arch)
|
||||
"Control-KP_Subtract" = "resize-font:smaller";
|
||||
"Control-KP_Add" = "resize-font:bigger";
|
||||
};
|
||||
extraConfig = {
|
||||
"letterSpace" = 0;
|
||||
"perl-ext-common" = "resize-font,bell-command,readline,selection";
|
||||
"bell-command" = "${pkgs.sox}/bin/play -n synth sine C5 sine E4 remix 1-2 fade 0.1 0.2 0.1 &> /dev/null";
|
||||
};
|
||||
};
|
||||
rofi = {
|
||||
# TODO This theme template, that was used for Arch, looks much better:
|
||||
# https://gitlab.com/jordiorlando/base16-rofi/-/blob/master/templates/default.mustache
|
||||
enable = true;
|
||||
pass.enable = true;
|
||||
extraConfig = {
|
||||
lazy-grab = false;
|
||||
matching = "regex";
|
||||
};
|
||||
};
|
||||
autorandr = {
|
||||
enable = true;
|
||||
hooks.postswitch = {
|
||||
background = "${pkgs.feh}/bin/feh --no-fehbg --bg-fill ${config.stylix.image}";
|
||||
};
|
||||
};
|
||||
mpv = {
|
||||
enable = true;
|
||||
config = {
|
||||
audio-display = false;
|
||||
save-position-on-quit = true;
|
||||
osc = false; # Required by thumbnail script
|
||||
# Hardware acceleration (from https://nixos.wiki/wiki/Accelerated_Video_Playback#MPV)
|
||||
hwdec = "auto-safe";
|
||||
vo = "gpu";
|
||||
profile = "gpu-hq";
|
||||
};
|
||||
scripts = with pkgs.mpvScripts; [ thumbnail ];
|
||||
scriptOpts = {
|
||||
mpv_thumbnail_script = {
|
||||
autogenerate = false; # TODO It creates too many processes at once, crashing the system
|
||||
cache_directory = "/tmp/mpv_thumbs_${config.home.username}";
|
||||
mpv_hwdec = "auto-safe";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
xdg = {
|
||||
userDirs = {
|
||||
enable = true; # TODO Which ones do we want?
|
||||
createDirectories = true;
|
||||
# French, because then it there's a different initial for each, making navigation easier
|
||||
desktop = null;
|
||||
download = "${config.home.homeDirectory}/Téléchargements";
|
||||
music = "${config.home.homeDirectory}/Musiques";
|
||||
pictures = "${config.home.homeDirectory}/Images";
|
||||
publicShare = null;
|
||||
templates = null;
|
||||
videos = "${config.home.homeDirectory}/Vidéos";
|
||||
extraConfig = {
|
||||
XDG_SCREENSHOTS_DIR = "${config.home.homeDirectory}/Screenshots";
|
||||
};
|
||||
};
|
||||
configFile = {
|
||||
"pulse/client.conf" = {
|
||||
text = ''cookie-file = .config/pulse/pulse-cookie'';
|
||||
};
|
||||
"rofimoji.rc" = {
|
||||
text = ''
|
||||
skin-tone = neutral
|
||||
files = [emojis, math]
|
||||
action = clipboard
|
||||
'';
|
||||
};
|
||||
"vimpc/vimpcrc" = {
|
||||
text = ''
|
||||
map FF :browse<C-M>gg/
|
||||
map à :set add next<C-M>a:set add end<C-M>
|
||||
map @ :set add next<C-M>a:set add end<C-M>:next<C-M>
|
||||
map ° D:browse<C-M>A:shuffle<C-M>:play<C-M>:playlist<C-M>
|
||||
set songformat {%a - %b: %t}|{%f}$E$R $H[$H%l$H]$H
|
||||
set libraryformat %n \| {%t}|{%f}$E$R $H[$H%l$H]$H
|
||||
set ignorecase
|
||||
set sort library
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
services = {
|
||||
blueman-applet.enable = true;
|
||||
unclutter.enable = true;
|
||||
dunst =
|
||||
{
|
||||
enable = true;
|
||||
settings =
|
||||
# TODO Change dmenu for rofi, so we can use context
|
||||
with config.lib.stylix.colors.withHashtag; {
|
||||
global = {
|
||||
separator_color = lib.mkForce base05;
|
||||
idle_threshold = 120;
|
||||
markup = "full";
|
||||
max_icon_size = 48;
|
||||
# TODO Those shortcuts don't seem to work, maybe try:
|
||||
# > define shortcuts inside your window manager and bind them to dunstctl(1) commands
|
||||
close_all = "ctrl+mod4+n";
|
||||
close = "mod4+n";
|
||||
context = "mod1+mod4+n";
|
||||
history = "shift+mod4+n";
|
||||
};
|
||||
|
||||
urgency_low = {
|
||||
background = lib.mkForce base01;
|
||||
foreground = lib.mkForce base03;
|
||||
frame_color = lib.mkForce base05;
|
||||
};
|
||||
urgency_normal = {
|
||||
background = lib.mkForce base02;
|
||||
foreground = lib.mkForce base05;
|
||||
frame_color = lib.mkForce base05;
|
||||
};
|
||||
urgency_critical = {
|
||||
background = lib.mkForce base08;
|
||||
foreground = lib.mkForce base06;
|
||||
frame_color = lib.mkForce base05;
|
||||
};
|
||||
};
|
||||
};
|
||||
mpd = {
|
||||
enable = true;
|
||||
network = {
|
||||
listenAddress = "0.0.0.0"; # So it can be controlled from home
|
||||
# TODO ... and whoever is the Wi-Fi network I'm using, which, not great
|
||||
startWhenNeeded = true;
|
||||
};
|
||||
extraConfig = ''
|
||||
restore_paused "yes"
|
||||
'';
|
||||
};
|
||||
autorandr.enable = true;
|
||||
};
|
||||
|
||||
home = {
|
||||
file = {
|
||||
".face" = {
|
||||
# TODO Only works on pindakaas? See https://wiki.archlinux.org/title/LightDM#Changing_your_avatar
|
||||
source = pkgs.runCommand "face.png" { } "${pkgs.inkscape}/bin/inkscape ${./face.svg} -w 1024 -o $out";
|
||||
};
|
||||
};
|
||||
packages = with pkgs; [
|
||||
pavucontrol # Because can't use Win+F1X on Pinebook 🙃
|
||||
|
||||
# remote
|
||||
tigervnc
|
||||
|
||||
# music
|
||||
mpc-cli
|
||||
ashuffle
|
||||
vimpc
|
||||
|
||||
# multimedia common
|
||||
gimp
|
||||
inkscape
|
||||
libreoffice
|
||||
|
||||
# data management
|
||||
freefilesync
|
||||
|
||||
# browsers
|
||||
firefox
|
||||
|
||||
# fonts
|
||||
dejavu_fonts
|
||||
twemoji-color-font
|
||||
gnome.gedit
|
||||
feh
|
||||
zbar
|
||||
zathura
|
||||
meld
|
||||
python3Packages.magic
|
||||
|
||||
# x11-exclusive
|
||||
numlockx
|
||||
simplescreenrecorder
|
||||
trayer
|
||||
xclip
|
||||
keynav
|
||||
xorg.xinit
|
||||
# TODO Make this clean. Service?
|
||||
|
||||
|
||||
# organisation
|
||||
pass
|
||||
thunderbird
|
||||
];
|
||||
sessionVariables = {
|
||||
MPD_PORT = "${toString config.services.mpd.network.port}";
|
||||
ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; # Fixes an issue with sox (Cannot open shared library libasound_module_pcm_pulse.so)
|
||||
# UPST Patch this upstream like: https://github.com/NixOS/nixpkgs/blob/216b111fb87091632d077898df647d1438fc2edb/pkgs/applications/audio/espeak-ng/default.nix#L84
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
192
hm/desktop/face.svg
Normal file
192
hm/desktop/face.svg
Normal file
|
@ -0,0 +1,192 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
xml:space="preserve"
|
||||
width="500"
|
||||
height="500"
|
||||
viewBox="0 0 500 500.00002"
|
||||
enable-background="new"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata8"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs6"><linearGradient
|
||||
id="Gradient_1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="559.79999"
|
||||
y1="0.0125"
|
||||
x2="559.79999"
|
||||
y2="834.88751"
|
||||
spreadMethod="pad"><stop
|
||||
offset="0%"
|
||||
stop-color="#CCE474"
|
||||
id="stop2" /><stop
|
||||
offset="21.96078431372549%"
|
||||
stop-color="#CBE372"
|
||||
id="stop4-3" /><stop
|
||||
offset="29.80392156862745%"
|
||||
stop-color="#C8E26B"
|
||||
id="stop6-6" /><stop
|
||||
offset="35.68627450980392%"
|
||||
stop-color="#C3DE60"
|
||||
id="stop8" /><stop
|
||||
offset="40%"
|
||||
stop-color="#BBD94F"
|
||||
id="stop10" /><stop
|
||||
offset="43.92156862745098%"
|
||||
stop-color="#B1D339"
|
||||
id="stop12" /><stop
|
||||
offset="47.450980392156865%"
|
||||
stop-color="#A4CB1E"
|
||||
id="stop14" /><stop
|
||||
offset="49.80392156862745%"
|
||||
stop-color="#99C405"
|
||||
id="stop16" /><stop
|
||||
offset="54.11764705882353%"
|
||||
stop-color="#A2CC18"
|
||||
id="stop18-7" /><stop
|
||||
offset="59.21568627450981%"
|
||||
stop-color="#AAD329"
|
||||
id="stop20-5" /><stop
|
||||
offset="65.49019607843137%"
|
||||
stop-color="#B0D834"
|
||||
id="stop22" /><stop
|
||||
offset="74.90196078431373%"
|
||||
stop-color="#B3DB3B"
|
||||
id="stop24-3" /><stop
|
||||
offset="100%"
|
||||
stop-color="#B4DC3D"
|
||||
id="stop26" /></linearGradient><linearGradient
|
||||
xlink:href="#Gradient_1"
|
||||
id="linearGradient15891"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="559.79999"
|
||||
y1="0.0125"
|
||||
x2="559.79999"
|
||||
y2="834.88751"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="translate(-2962.3062,128.19426)" /><filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter1"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1.0052697"
|
||||
height="1.0081699"><feComponentTransfer
|
||||
id="feComponentTransfer1"><feFuncR
|
||||
id="feFuncR1"
|
||||
type="discrete"
|
||||
tableValues="1" /><feFuncG
|
||||
id="feFuncG1"
|
||||
type="discrete"
|
||||
tableValues="1" /><feFuncB
|
||||
id="feFuncB1"
|
||||
type="discrete"
|
||||
tableValues="1" /><feFuncA
|
||||
id="feFuncA1"
|
||||
type="identity"
|
||||
tableValues="1" /></feComponentTransfer><feOffset
|
||||
dx="2.4500000000000002"
|
||||
dy="2.4500000000000002"
|
||||
id="feOffset1"
|
||||
result="result1" /><feBlend
|
||||
mode="normal"
|
||||
id="feBlend1"
|
||||
in2="result1"
|
||||
in="SourceGraphic" /></filter></defs><g
|
||||
transform="translate(559.2858,-691.3027)"
|
||||
style="display:inline"
|
||||
id="g61596"><g
|
||||
style="display:inline"
|
||||
transform="matrix(0.44656812,0,0,0.59890998,763.58571,614.52588)"
|
||||
id="g38-5"><g
|
||||
id="g36"><g
|
||||
id="use34"><path
|
||||
style="fill:url(#linearGradient15891);stroke:none"
|
||||
id="path15887"
|
||||
d="m -1842.6562,963.04426 v -834.85 h -1119.65 v 834.85 z" /></g></g></g></g><g
|
||||
transform="translate(559.2858,-691.3027)"
|
||||
style="display:inline;filter:url(#filter1)"
|
||||
id="g16328"><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-278.83618,862.3163)"
|
||||
id="g16254"><path
|
||||
id="path16252"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -2.947,15.294 -22.128,72.379 -143.132,56.208 21.015,2.281 100.452,4.552 139.645,-83.752 -9.586,20.819 -37.7,69.634 -93.652,78.608 -71.51,11.466 -109.919,-12.516 -136.312,-22.286 -26.394,-9.772 -106.737,-56.449 -132.625,-65.167 -14.994,-5.05 -44.693,-18.094 -68.81,-38.721 16.273,14.494 39.126,29.005 71.287,42.604 52.334,22.129 161.277,95.281 248.44,95.186 C -31.643,62.588 -4.91,28.398 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-109.48077,846.84748)"
|
||||
id="g16262"><path
|
||||
id="path16260"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -7.293,-7.944 -11.633,-18.825 -10.715,-29.12 1.233,-13.799 13.076,-25.783 27.153,-25.487 7.211,0.153 13.912,3.428 18.067,8.137 13.956,15.822 -13.806,41.482 -22.333,11.651 -0.174,-0.603 -0.086,-1.489 0.197,-1.977 0.28,-0.488 0.649,-0.394 0.822,0.208 7.996,27.984 33.536,3.09 19.366,-8.968 -3.861,-3.283 -9.493,-5.549 -15.237,-5.999 -12.719,-0.989 -24.767,8.662 -26.664,21.394 -1.507,10.111 3.291,20.939 11.032,28.634 26.329,26.17 64.196,-2.748 57.46,-35.583 -4.432,-21.636 -21.984,-31.551 -41.565,-35.563 -3.841,-0.787 -7.749,-1.345 -11.638,-1.718 -3.883,-0.371 -7.767,-0.561 -11.561,-0.61 -0.023,0.003 -0.045,0.004 -0.068,0.003 h -0.004 v -0.004 c -0.518,-0.007 -0.21,-2.813 0.173,-2.809 h 0.006 l 0.008,0.001 v -0.006 l 0.006,0.001 0.037,0.005 c 3.808,0.051 7.701,0.241 11.603,0.614 3.928,0.375 7.855,0.935 11.69,1.72 19.902,4.081 37.779,14.33 42.407,36.89 C 67.324,-4.044 28.104,30.604 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-478.09959,997.90223)"
|
||||
id="g16266"><path
|
||||
id="path16264"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 2.649,73.746 -37.597,74.47 V 63.625 l 28.439,2.892 c -1.126,-3.375 -2.694,-6.71 -4.7,-10 -2.011,-3.296 -4.178,-6.35 -6.506,-9.161 -2.333,-2.812 -4.698,-5.342 -7.111,-7.59 -2.409,-2.248 -4.578,-4.098 -6.507,-5.542 -0.962,-0.802 -2.734,-1.768 -5.301,-2.893 -2.572,-1.122 -5.626,-1.566 -9.158,-1.327 -3.536,0.243 -7.35,1.49 -11.448,3.736 -4.096,2.254 -8.235,6.267 -12.412,12.05 -6.589,8.355 -11.687,16.749 -15.303,25.187 -3.615,8.434 -6.067,16.626 -7.35,24.582 -1.288,7.952 -1.566,15.543 -0.843,22.773 0.722,7.229 2.206,13.936 4.458,20.123 2.248,6.183 5.02,11.647 8.314,16.388 3.292,4.737 6.868,8.634 10.724,11.689 4.339,5.46 8.835,9.437 13.496,11.929 4.659,2.489 9.318,3.935 13.978,4.338 4.658,0.399 9.317,0.038 13.978,-1.085 4.658,-1.125 9.159,-2.651 13.495,-4.578 6.269,-2.733 10.443,-6.428 12.533,-11.086 2.087,-4.662 2.893,-9.361 2.411,-14.099 -0.484,-4.741 -1.808,-9.157 -3.976,-13.255 -0.426,-0.8 7.949,-2.648 9.279,0.242 2.248,4.898 3.855,10.08 4.819,15.543 0.482,3.534 -0.283,7.51 -2.29,11.931 -2.011,4.416 -5.022,8.593 -9.037,12.531 -4.018,3.935 -8.88,7.23 -14.58,9.881 -5.705,2.652 -12.014,4.015 -18.919,4.097 -6.91,0.079 -14.218,-1.567 -21.931,-4.94 -7.711,-3.374 -15.586,-9.08 -23.618,-17.111 -7.874,-8.036 -13.617,-17.231 -17.232,-27.595 -3.615,-10.362 -5.543,-20.929 -5.783,-31.691 -0.242,-10.765 1.001,-21.369 3.735,-31.813 2.73,-10.445 6.465,-19.802 11.206,-28.075 4.738,-8.277 10.322,-15.142 16.75,-20.605 6.424,-5.461 13.255,-8.596 20.485,-9.401 7.067,-0.802 13.092,-0.558 18.074,0.722 4.98,1.288 9.196,3.296 12.655,6.025 3.451,2.738 6.385,5.99 8.794,9.763 2.412,3.772 4.58,7.832 6.509,12.17 0.321,0.641 0.362,-0.806 0.118,-4.338 C -7.591,42.496 -7.916,37.996 -8.314,32.535 -8.717,27.076 -9.158,21.25 -9.641,15.064 -10.123,8.879 -10.446,3.537 -10.605,-0.963 l -16.387,-2.17 v -8.435 l 40.248,4.34 v 9.399 z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-476.29293,884.2114)"
|
||||
id="g16270"><path
|
||||
id="path16268"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 h 88.931 v -24.1 h -6.99 l 0.723,14.219 -26.992,0.964 c -3.375,0.158 -6.711,0.279 -10.002,0.361 -3.294,0.08 -6.229,0.2 -8.795,0.362 -2.573,0.158 -4.663,0.279 -6.266,0.362 -1.607,0.078 -2.493,0.12 -2.651,0.12 -0.161,0 -0.241,-1.167 -0.241,-3.495 0,-2.331 0.038,-5.422 0.121,-9.278 0.079,-3.856 0.158,-8.236 0.239,-13.135 0.081,-4.903 0.2,-9.922 0.362,-15.062 0.32,-12.05 0.724,-25.629 1.206,-40.73 l 15.182,0.242 -0.482,13.497 7.473,0.722 2.889,-32.295 -11.086,-0.481 v 9.64 l -13.495,-0.964 1.205,-54.466 53.504,2.409 2.409,17.835 h 7.469 l -2.893,-30.124 -90.372,-1.205 v 6.505 l 18.555,4.58 L 18.559,-7.472 0,-6.266 Z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-398.22755,907.86829)"
|
||||
id="g16274"><path
|
||||
id="path16272"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -8.035,-13.013 -12.892,-26.592 -14.578,-40.728 -1.689,-14.141 0.361,-28.521 6.142,-43.139 2.734,-6.748 6.426,-12.212 11.089,-16.387 4.657,-4.182 9.88,-6.87 15.666,-8.075 5.783,-1.205 11.808,-0.805 18.073,1.205 6.266,2.008 12.293,5.823 18.075,11.447 6.266,6.104 10.564,13.334 12.893,21.69 2.328,8.354 3.213,17.072 2.651,26.149 -0.565,9.076 -2.33,17.992 -5.302,26.752 -2.973,8.754 -6.627,16.587 -10.964,23.497 -6.108,9.64 -11.568,16.184 -16.389,19.641 -4.82,3.453 -9.362,4.617 -13.616,3.494 C 19.481,24.421 15.462,21.488 11.688,16.75 7.912,12.01 4.015,6.424 0,0 m 25.307,-118.331 c -5.626,-0.321 -10.725,-0.12 -15.305,0.604 -4.577,0.722 -8.797,2.567 -12.652,5.541 -3.855,2.972 -7.352,7.473 -10.484,13.497 -3.133,6.025 -6.066,14.255 -8.796,24.702 -1.927,6.906 -2.771,14.581 -2.529,23.017 0.24,8.434 1.241,16.869 3.012,25.304 1.766,8.435 4.174,16.629 7.23,24.582 3.049,7.954 6.542,14.98 10.485,21.087 3.934,6.105 8.15,11.003 12.65,14.702 4.497,3.694 9.075,5.543 13.738,5.543 6.905,0 13.256,-2.132 19.037,-6.387 5.786,-4.259 10.927,-9.843 15.424,-16.749 4.497,-6.91 8.235,-14.664 11.206,-23.257 2.974,-8.596 5.099,-17.231 6.387,-25.907 2.088,-13.82 2.851,-25.547 2.292,-35.186 -0.565,-9.639 -2.049,-17.634 -4.459,-23.979 -2.411,-6.349 -5.507,-11.29 -9.28,-14.822 -3.777,-3.537 -7.872,-6.188 -12.29,-7.953 -4.42,-1.769 -8.837,-2.891 -13.255,-3.375 -4.42,-0.482 -8.557,-0.804 -12.411,-0.964" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-308.19442,972.57512)"
|
||||
id="g16278"><path
|
||||
id="path16276"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 13.495,0.963 0.482,139.54 -16.388,-4.579 0.483,7.471 c 5.782,2.085 12.773,4.537 20.965,7.35 8.195,2.809 16.667,5.219 25.427,7.23 8.755,2.007 17.351,3.292 25.786,3.855 8.435,0.562 15.786,-0.281 22.053,-2.529 2.409,-0.644 4.134,-2.091 5.181,-4.339 1.041,-2.251 1.362,-4.62 0.966,-7.109 -0.404,-2.493 -1.449,-4.699 -3.134,-6.628 -1.688,-1.927 -3.976,-2.891 -6.87,-2.891 -3.215,0 -5.784,0.519 -7.712,1.566 -1.927,1.043 -3.215,2.328 -3.854,3.856 -0.646,1.525 -0.723,3.254 -0.243,5.181 0.482,1.929 1.447,3.774 2.892,5.544 -7.07,-0.324 -13.617,-1.006 -19.642,-2.049 -6.026,-1.047 -11.447,-2.169 -16.267,-3.374 -4.82,-1.205 -8.918,-2.372 -12.293,-3.495 -3.372,-1.126 -6.023,-2.01 -7.953,-2.65 l 0.483,-70.373 20.003,1.206 0.722,11.086 h 7.231 c 0.16,0 -0.479,-10.446 -1.928,-31.331 H 43.378 L 43.86,63.864 23.617,63.141 22.411,0.963 36.873,3.132 36.391,-6.99 0,-8.677 Z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-219.83209,884.2114)"
|
||||
id="g16282"><path
|
||||
id="path16280"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 h 49.885 l 10.47,-9.084 -4.684,0.167 c -3.375,0.158 -6.71,0.279 -10.002,0.361 -3.295,0.08 -6.23,0.2 -8.796,0.362 -2.573,0.158 -4.664,0.279 -6.266,0.362 -1.607,0.078 -2.493,0.12 -2.652,0.12 -0.159,0 -0.24,-1.167 -0.24,-3.495 0,-2.331 0.038,-5.422 0.122,-9.278 0.076,-3.856 0.155,-8.236 0.238,-13.135 0.081,-4.903 0.2,-9.922 0.361,-15.062 0.321,-12.05 0.723,-25.629 1.206,-40.73 l 15.183,0.242 -0.482,13.497 7.471,0.722 2.891,-32.295 -11.085,-0.481 v 9.64 l -13.496,-0.964 1.205,-54.466 53.503,2.409 2.409,17.835 h 7.471 l -2.89,-30.124 -90.376,-1.205 v 6.505 l 18.556,4.58 L 18.558,-7.472 0,-6.266 Z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-159.16119,886.15987)"
|
||||
id="g16286"><path
|
||||
id="path16284"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 29.401,8.676 0.483,-9.639 -7.712,-2.411 22.894,-59.526 21.933,54.225 -8.678,2.169 v 5.061 l 30.126,-1.688 -2.652,-9.398 -10.603,1.686 -26.028,-61.454 0.723,-94.954 h 13.496 v -7.953 l -33.982,0.962 v 7.715 l 9.158,-0.724 0.482,97.605 L 12.29,-4.337 3.132,-6.266 Z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-184.16345,869.72649)"
|
||||
id="g16290"><path
|
||||
id="path16288"
|
||||
style="fill:none;stroke:#000000;stroke-width:5.647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 0,0 c 1.893,-1.663 16.513,-5.652 12.795,-16.931 7.208,6.008 9.155,15.147 18.24,21.18 -2.542,-6.545 -0.932,-20.71 -9.036,-24.252 11.145,-1.657 12.241,4.784 27.421,1.396 -7.338,-5.168 -15.408,-8.757 -24.21,-10.77 8.244,-3.75 11.209,-12.681 13.916,-20.437 -5.719,4.094 -19.832,6.022 -21.26,14.763 1.331,-4.41 1.125,-6.078 -0.617,-5.001 3.59,-8.32 -0.205,-16.921 -3.404,-24.493 -2.058,8.77 -8.713,18.589 -6.417,28.009 -2.685,-8.641 -11.243,-12.684 -18.585,-16.313 5.516,10.125 5.512,15.098 12.813,23.587 -8.9,-1.635 -16.451,4.071 -23.071,8.895 7.009,-0.121 19.761,6.331 25.879,-0.055 C 0.182,-14.278 -1.308,-7.472 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-305.49907,981.53571)"
|
||||
id="g16294"><path
|
||||
id="path16292"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 4.755,-1.007 9.469,-2.193 14.142,-3.511 18.184,-5.283 36.631,-12.659 50.298,-26.651 13.577,-13.793 18.948,-35.559 11.741,-53.607 -3.405,-8.812 -11.209,-15.85 -20.306,-18.239 -4.448,-1.356 -9.171,-2.15 -13.873,-1.612 -4.68,0.483 -9.287,2.654 -12.342,6.294 -6.202,7.472 -5.775,18.259 -2.063,26.611 3.757,8.577 11.085,15.316 19.656,18.967 13.895,5.885 29.805,4.483 43.718,-0.444 14.039,-5.036 26.726,-13.3 37.762,-23.199 11.003,-9.955 20.529,-21.65 27.404,-34.873 3.433,-6.603 6.135,-13.624 7.679,-20.959 1.556,-7.374 1.301,-15.12 -0.717,-22.381 -3.793,-14.55 -15.627,-27.205 -30.63,-30.362 -15.067,-3.389 -29.951,1.978 -43.591,7.065 -13.71,5.469 -27.918,9.745 -42.643,11.184 -7.147,0.483 -15.263,0.852 -21.09,-3.774 -2.829,-2.373 -3.799,-6.282 -3.476,-9.914 0.293,-3.682 1.477,-7.252 2.897,-10.672 -1.924,4.724 -3.719,9.79 -2.941,14.925 0.672,5.35 5.812,8.763 10.638,9.733 12.215,2.447 24.69,-0.243 36.711,-2.968 12.161,-3.008 23.569,-8.292 35.511,-11.566 11.773,-3.388 24.812,-4.314 35.771,1.339 10.95,5.406 18.599,16.416 21.09,28.382 2.791,12.091 -0.594,24.631 -5.799,35.877 -5.281,11.297 -12.677,21.57 -21.175,30.663 -16.995,17.892 -39.089,32.508 -63.608,34.736 -8.876,0.628 -18.068,-0.611 -26.043,-4.697 -7.959,-3.998 -14.476,-10.99 -17.419,-19.42 -2.984,-8.196 -2.421,-18.538 4.076,-24.726 6.573,-6.263 16.747,-5.526 24.808,-2.541 8.456,2.659 15.134,9.372 18.084,17.577 3.065,8.171 3.194,17.283 1.317,25.756 -0.948,4.243 -2.427,8.376 -4.499,12.153 -2.107,3.701 -4.737,7.398 -7.56,10.558 -5.797,6.443 -13.188,11.342 -20.911,15.385 -16.578,8.404 -35.09,13.086 -53.696,16.241 -18.649,3.069 -37.696,4.515 -56.594,4.735 -18.966,0.245 -38.159,0.008 -56.93,3.326 -9.339,1.681 -18.627,4.286 -26.987,8.839 -8.35,4.489 -15.53,11.324 -19.688,19.679 -1.085,2.044 -3.255,7.268 -4.193,12.679 -1.099,5.378 -1.149,10.774 -1.314,12.744 0.548,-8.169 2.38,-16.405 6.301,-23.647 3.86,-7.274 9.967,-13.184 17.134,-17.276 14.518,-8.204 31.899,-10.19 48.906,-11.124 C -87.282,6.4 -69.918,7.266 -52.275,6.453 -34.79,5.628 -17.177,3.708 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-359.11142,1021.4165)"
|
||||
id="g16298"><path
|
||||
id="path16296"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 3.397,-1.412 6.911,-2.492 10.495,-3.112 13.817,-2.814 28.983,4.758 36.493,16.6 3.674,5.884 5.109,13.63 2.693,20.016 -2.589,6.435 -9.718,10.167 -16.77,10.812 C 26.011,44.98 18.542,43.211 13.303,38.429 10.719,36.058 8.704,32.941 8.271,29.442 7.827,25.944 8.977,22.371 10.779,19.267 14.497,13.021 20.21,8.135 26.107,3.844 c 5.962,-4.275 12.381,-7.919 19.004,-11.087 5.305,-2.511 10.732,-4.793 16.311,-6.534 5.52,-1.641 11.504,-2.247 17.196,-3.632 11.493,-2.481 22.909,-5.584 33.808,-10.138 10.873,-4.554 21.226,-10.576 30.164,-18.351 8.97,-7.728 16.514,-17.027 22.62,-27.121 13.462,-19.712 16.185,-45.646 9.357,-68.252 -3.434,-11.303 -9.591,-22.087 -18.893,-29.615 -9.24,-7.611 -21.479,-10.945 -33.257,-10.132 8.222,-0.491 16.481,1.343 23.432,4.909 8.62,4.291 15.564,11.512 20.362,19.887 4.839,8.399 7.685,17.926 8.945,27.625 1.221,9.714 0.868,19.697 -1.296,29.29 -1.977,9.663 -6.729,18.435 -11.973,26.911 -10.449,16.917 -25.416,30.976 -43.197,39.774 -17.714,9.06 -37.179,13.361 -56.267,17.515 -13.645,4.57 -26.488,11.216 -38.145,19.552 -5.706,4.275 -11.3,9.056 -14.891,15.437 -1.74,3.166 -2.795,6.927 -2.143,10.646 0.644,3.72 2.849,6.944 5.537,9.429 5.525,4.988 13.191,7.011 20.497,6.485 C 40.482,45.91 48.312,42.526 51.883,35.432 55.271,28.342 54.19,20.204 50.949,13.501 47.602,6.759 42.062,1.368 35.701,-2.395 28.977,-6.342 20.977,-8.77 12.849,-7.928 4.939,-7.2 -2.636,-4.423 -9.455,-0.626 c -13.7,7.705 -24.68,19.052 -34.056,31.104 -18.53,24.412 -31.581,52.29 -39.022,80.488 -1.085,3.501 -3.356,17.301 -3.598,20.435 2.238,-12.869 6.646,-25.442 11.748,-37.818 C -69.21,81.217 -63.119,69.065 -56.134,57.36 -49.122,45.684 -41.241,34.39 -32.056,24.283 -22.903,14.246 -12.326,5.164 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-496.15832,937.16902)"
|
||||
id="g16302"><path
|
||||
id="path16300"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 0,0 c -0.977,-2.42 -2.724,-3.975 -5.139,-3.018 -2.419,0.957 -3.087,3.466 -2.009,7.53 2.051,6.483 8.06,10.28 14.171,15.72 C 9.03,12.7 11.041,5.17 8.991,-1.316 8.786,-4.605 5.396,-6.064 3.751,-5.978 1.336,-5.021 0.564,-4.154 0,0 m -15.43,17.332 c -3.287,0.177 -4.829,1.911 -4.729,3.555 -0.669,2.508 1.951,4.84 4.466,5.531 6.68,1.284 13.925,-1.586 21.172,-4.454 C -0.632,16.525 -5.969,10.22 -11.775,9.71 c -3.389,-1.463 -6.577,0.359 -7.25,2.872 -0.668,2.506 0.204,3.284 3.595,4.75 m 8.001,22.651 c -1.44,3.377 -2.109,5.888 -0.364,7.443 1.749,1.551 4.934,-0.271 7.246,-2.87 3.858,-4.332 5.868,-11.865 7.002,-20.171 -8.118,2.091 -16.238,4.184 -20.096,8.515 -2.313,2.601 -2.882,6.753 -1.135,8.307 1.746,1.555 4.161,0.598 7.347,-1.224 m 23.428,5.321 c 0.976,2.422 2.724,3.973 5.137,3.018 2.412,-0.956 3.084,-3.466 2.01,-7.53 C 21.094,34.307 15.083,30.512 8.974,25.071 6.963,32.602 4.955,40.135 7.006,46.618 c 0.205,3.289 3.591,4.75 5.236,4.664 2.415,-0.96 3.19,-1.825 3.757,-5.978 M 31.426,27.971 c 3.284,-0.177 4.831,-1.909 4.729,-3.554 0.667,-2.509 -1.95,-4.841 -4.466,-5.529 -6.68,-1.285 -13.927,1.583 -21.173,4.453 6.111,5.438 11.45,11.744 17.257,12.253 3.389,1.463 6.574,-0.36 7.245,-2.87 0.67,-2.509 -0.204,-3.287 -3.592,-4.753 M 23.424,5.32 c 1.439,-3.378 2.113,-5.885 0.368,-7.442 -1.747,-1.552 -4.935,0.27 -7.251,2.87 C 12.684,5.08 10.676,12.612 9.542,20.921 17.659,18.827 25.779,16.736 29.637,12.403 31.95,9.804 32.52,5.649 30.773,4.094 29.027,2.54 26.612,3.499 23.424,5.32" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-235.13854,898.12704)"
|
||||
id="g16306"><path
|
||||
id="path16304"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -4.821,2.569 -9.923,4.496 -15.305,5.784 -5.383,1.283 -10.404,2.169 -15.062,2.652 -5.303,0.639 -10.602,0.801 -15.905,0.48 l 1.445,-77.119 c 7.068,0.483 13.817,1.284 20.246,2.41 5.301,0.965 10.602,2.369 15.904,4.217 5.301,1.846 9.236,4.218 11.809,7.11 2.567,2.893 4.737,6.868 6.508,11.929 1.765,5.062 2.73,10.323 2.892,15.786 0.157,5.461 -0.686,10.642 -2.531,15.544 C 8.152,-6.307 4.82,-2.572 0,0 m -66.276,-128.212 11.087,-0.482 -2.65,137.37 h -13.016 l -0.962,9.881 c 17.027,0.964 32.373,0.482 46.031,-1.447 5.782,-0.963 11.481,-2.25 17.109,-3.855 5.62,-1.608 10.685,-3.777 15.184,-6.507 4.495,-2.734 8.232,-6.108 11.208,-10.122 2.969,-4.018 4.614,-8.838 4.936,-14.461 0.484,-7.711 0.244,-14.459 -0.721,-20.242 -0.965,-5.785 -2.33,-10.767 -4.099,-14.943 -1.769,-4.18 -3.936,-7.633 -6.503,-10.363 -2.574,-2.734 -5.146,-4.982 -7.713,-6.747 -6.428,-4.181 -13.738,-6.188 -21.931,-6.025 l 54.463,-73.505 h 11.812 l 1.686,-8.918 -33.018,-2.17 -0.479,6.991 10.602,1.204 -57.117,72.781 -14.46,-1.927 v -46.995 l 11.086,0.965 v -8.918 l -32.052,0.482 z" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-290.17403,883.09827)"
|
||||
id="g16322"><path
|
||||
id="path16320"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -1.046,2.248 -2.772,3.694 -5.182,4.338 -6.264,2.248 -13.616,3.092 -22.048,2.53 -8.436,-0.564 -17.033,-1.849 -25.791,-3.855 -8.756,-2.012 -17.23,-4.421 -25.422,-7.23 -8.195,-2.813 -15.183,-5.265 -20.969,-7.351 l -0.482,-7.471 16.388,4.579 -0.481,-139.54 -13.496,-0.962 v -8.677 l 36.39,1.686 0.483,10.122 -14.461,-2.169 1.205,62.179 0.242,9.399 -0.481,70.372 c 1.926,0.64 4.577,1.525 7.953,2.65 3.372,1.123 7.469,2.291 12.289,3.496 4.819,1.205 10.245,2.327 16.266,3.373 6.026,1.043 12.573,1.725 19.643,2.049 -1.445,-1.77 -2.41,-3.614 -2.891,-5.543 -0.481,-1.928 -0.405,-3.657 0.241,-5.182 0.639,-1.528 1.927,-2.812 3.854,-3.855 1.929,-1.047 4.498,-1.567 7.714,-1.567 2.892,0 5.181,0.964 6.869,2.892 1.688,1.928 2.729,4.135 3.132,6.628 C 1.365,-4.62 1.045,-2.252 0,0" /></g><g
|
||||
transform="matrix(0.57741006,0,0,-0.57741006,-165.14449,937.08189)"
|
||||
id="g16326"><path
|
||||
id="path16324"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C 22.756,-46.462 103.04,-47.09 103.04,-47.09 13.691,-47.872 0,0 0,0 m -99.337,-33.74 -0.013,-0.428 c -0.282,-11.702 -7.741,-19.501 -17.459,-20.42 -9.699,-0.923 -17.428,5.868 -17.126,18.404 0.305,12.534 8.49,24.57 18.356,32.178 8.289,-7.974 16.495,-19.297 16.242,-29.734 m -193.03,35.08 c 17.189,9.15 49.441,15.952 73.138,17.77 37.651,3.162 78.074,-3.446 99.464,-21.019 -11.731,-8.605 -24.16,-22.297 -24.482,-35.679 -0.333,-13.793 13.964,-22.856 27.331,-21.592 12.154,1.156 23.305,11.385 23.64,25.162 0.283,11.723 -9.111,23.742 -18.603,32.026 12.931,8.741 31.863,15.95 56.18,18.254 20.639,1.955 39.915,-2.89 45.18,-10.74 2.945,-3.475 1.061,-6.145 2.888,-5.974 l 0.67,0.205 c 1.55,-2.236 4.476,-7.174 7.834,-16.512 4.984,-13.875 27.627,-36.002 78.85,-33.898 0,0 20.259,-1.476 28.273,2.091 0,0 38.35,0.905 46.58,6.699 0,0 -24.95,-7.212 -52.941,6.203 -27.987,13.417 -51.337,41.257 -89.088,39.938 0,0 -18.556,-0.639 -23.477,7.48 l 0.252,-0.946 c -8.283,6.583 -25.672,11.105 -44.92,9.637 -23.692,-2.245 -46.914,-10.287 -59.863,-19.431 -22.557,19.123 -64.225,25.19 -103.672,22.28 -24.898,-1.928 -57.771,-8.386 -78.594,-18.288 z" /></g></g></svg>
|
After Width: | Height: | Size: 22 KiB |
94
hm/desktop/frobar/.dev/barng.py
Executable file
94
hm/desktop/frobar/.dev/barng.py
Executable file
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import typing
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# CORE
|
||||
|
||||
|
||||
class Notifier:
|
||||
pass
|
||||
|
||||
|
||||
class Section:
|
||||
def __init__(self) -> None:
|
||||
self.text = b"(Loading)"
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self) -> None:
|
||||
self.bar: "Bar"
|
||||
self.section = Section()
|
||||
self.sections = [self.section]
|
||||
|
||||
|
||||
class Alignment:
|
||||
def __init__(self, *modules: Module) -> None:
|
||||
self.bar: "Bar"
|
||||
self.modules = modules
|
||||
for module in modules:
|
||||
module.bar = self.bar
|
||||
|
||||
|
||||
class Screen:
|
||||
def __init__(self, left: Alignment = Alignment(), right: Alignment = Alignment()) -> None:
|
||||
self.bar: "Bar"
|
||||
self.left = left
|
||||
self.left.bar = self.bar
|
||||
self.right = right or Alignment()
|
||||
self.right.bar = self.bar
|
||||
|
||||
|
||||
class Bar:
|
||||
def __init__(self, *screens: Screen) -> None:
|
||||
self.screens = screens
|
||||
for screen in screens:
|
||||
screen.bar = self
|
||||
self.process = subprocess.Popen(["lemonbar"], stdin=subprocess.PIPE)
|
||||
|
||||
def display(self) -> None:
|
||||
string = b""
|
||||
for s, screen in enumerate(self.screens):
|
||||
string += b"%%{S%d}" % s
|
||||
for control, alignment in [(b'%{l}', screen.left), (b'%{r}', screen.right)]:
|
||||
string += control
|
||||
for module in alignment.modules:
|
||||
for section in module.sections:
|
||||
string += b"<%b> |" % section.text
|
||||
|
||||
string += b"\n"
|
||||
print(string)
|
||||
assert self.process.stdin
|
||||
self.process.stdin.write(string)
|
||||
self.process.stdin.flush()
|
||||
|
||||
def run(self) -> None:
|
||||
while True:
|
||||
self.display()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# REUSABLE
|
||||
|
||||
class ClockNotifier(Notifier):
|
||||
def run(self) -> None:
|
||||
while True:
|
||||
def __init__(self, text: bytes):
|
||||
super().__init__()
|
||||
self.section.text = text
|
||||
|
||||
class StaticModule(Module):
|
||||
def __init__(self, text: bytes):
|
||||
super().__init__()
|
||||
self.section.text = text
|
||||
|
||||
|
||||
# USER
|
||||
|
||||
if __name__ == "__main__":
|
||||
bar = Bar(
|
||||
Screen(Alignment(StaticModule(b"A"))),
|
||||
Screen(Alignment(StaticModule(b"B"))),
|
||||
)
|
||||
bar.run()
|
199
hm/desktop/frobar/.dev/oldbar.py
Executable file
199
hm/desktop/frobar/.dev/oldbar.py
Executable file
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Debugging script
|
||||
"""
|
||||
|
||||
import i3ipc
|
||||
import os
|
||||
import psutil
|
||||
|
||||
# import alsaaudio
|
||||
from time import time
|
||||
import subprocess
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
lemonbar = subprocess.Popen(["lemonbar", "-b"], stdin=subprocess.PIPE)
|
||||
|
||||
# Utils
|
||||
def upChart(p):
|
||||
block = " ▁▂▃▄▅▆▇█"
|
||||
return block[round(p * (len(block) - 1))]
|
||||
|
||||
|
||||
def humanSizeOf(num, suffix="B"): # TODO Credit
|
||||
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.0f%2s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.0f%2s%s" % (num, "Yi", suffix)
|
||||
|
||||
|
||||
# Values
|
||||
mode = ""
|
||||
container = i3.get_tree().find_focused()
|
||||
workspaces = i3.get_workspaces()
|
||||
outputs = i3.get_outputs()
|
||||
|
||||
username = os.environ["USER"]
|
||||
hostname = os.environ["HOSTNAME"]
|
||||
if "-" in hostname:
|
||||
hostname = hostname.split("-")[-1]
|
||||
|
||||
oldNetIO = dict()
|
||||
oldTime = time()
|
||||
|
||||
|
||||
def update():
|
||||
activeOutputs = sorted(
|
||||
sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y),
|
||||
key=lambda o: o.rect.x,
|
||||
)
|
||||
z = ""
|
||||
for aOutput in range(len(activeOutputs)):
|
||||
output = activeOutputs[aOutput]
|
||||
# Mode || Workspaces
|
||||
t = []
|
||||
if mode != "":
|
||||
t.append(mode)
|
||||
else:
|
||||
t.append(
|
||||
" ".join(
|
||||
[
|
||||
(w.name.upper() if w.focused else w.name)
|
||||
for w in workspaces
|
||||
if w.output == output.name
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Windows Title
|
||||
# if container:
|
||||
# t.append(container.name)
|
||||
|
||||
# CPU
|
||||
t.append(
|
||||
"C" + "".join([upChart(p / 100) for p in psutil.cpu_percent(percpu=True)])
|
||||
)
|
||||
|
||||
# Memory
|
||||
t.append(
|
||||
"M"
|
||||
+ str(round(psutil.virtual_memory().percent))
|
||||
+ "% "
|
||||
+ "S"
|
||||
+ str(round(psutil.swap_memory().percent))
|
||||
+ "%"
|
||||
)
|
||||
|
||||
# Disks
|
||||
d = []
|
||||
for disk in psutil.disk_partitions():
|
||||
e = ""
|
||||
if disk.device.startswith("/dev/sd"):
|
||||
e += "S" + disk.device[-2:].upper()
|
||||
elif disk.device.startswith("/dev/mmcblk"):
|
||||
e += "M" + disk.device[-3] + disk.device[-1]
|
||||
else:
|
||||
e += "?"
|
||||
e += " "
|
||||
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + "%"
|
||||
d.append(e)
|
||||
t.append(" ".join(d))
|
||||
|
||||
# Network
|
||||
netStats = psutil.net_if_stats()
|
||||
netIO = psutil.net_io_counters(pernic=True)
|
||||
net = []
|
||||
for iface in filter(lambda i: i != "lo" and netStats[i].isup, netStats.keys()):
|
||||
s = ""
|
||||
if iface.startswith("eth"):
|
||||
s += "E"
|
||||
elif iface.startswith("wlan"):
|
||||
s += "W"
|
||||
else:
|
||||
s += "?"
|
||||
|
||||
s += " "
|
||||
now = time()
|
||||
global oldNetIO, oldTime
|
||||
|
||||
sent = (
|
||||
(oldNetIO[iface].bytes_sent if iface in oldNetIO else 0)
|
||||
- (netIO[iface].bytes_sent if iface in netIO else 0)
|
||||
) / (oldTime - now)
|
||||
recv = (
|
||||
(oldNetIO[iface].bytes_recv if iface in oldNetIO else 0)
|
||||
- (netIO[iface].bytes_recv if iface in netIO else 0)
|
||||
) / (oldTime - now)
|
||||
s += (
|
||||
"↓"
|
||||
+ humanSizeOf(abs(recv), "B/s")
|
||||
+ " ↑"
|
||||
+ humanSizeOf(abs(sent), "B/s")
|
||||
)
|
||||
|
||||
oldNetIO = netIO
|
||||
oldTime = now
|
||||
|
||||
net.append(s)
|
||||
t.append(" ".join(net))
|
||||
|
||||
# Battery
|
||||
if os.path.isdir("/sys/class/power_supply/BAT0"):
|
||||
with open("/sys/class/power_supply/BAT0/charge_now") as f:
|
||||
charge_now = int(f.read())
|
||||
with open("/sys/class/power_supply/BAT0/charge_full_design") as f:
|
||||
charge_full = int(f.read())
|
||||
t.append("B" + str(round(100 * charge_now / charge_full)) + "%")
|
||||
|
||||
# Volume
|
||||
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
|
||||
|
||||
t.append(username + "@" + hostname)
|
||||
|
||||
# print(' - '.join(t))
|
||||
# t = [output.name]
|
||||
|
||||
z += " - ".join(t) + "%{S" + str(aOutput + 1) + "}"
|
||||
# lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
|
||||
# lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
|
||||
|
||||
lemonbar.stdin.write(bytes(z + "\n", "utf-8"))
|
||||
lemonbar.stdin.flush()
|
||||
|
||||
|
||||
# Event listeners
|
||||
def on_mode(i3, e):
|
||||
global mode
|
||||
if e.change == "default":
|
||||
mode = ""
|
||||
else:
|
||||
mode = e.change
|
||||
update()
|
||||
|
||||
|
||||
i3.on("mode", on_mode)
|
||||
|
||||
# def on_window_focus(i3, e):
|
||||
# global container
|
||||
# container = e.container
|
||||
# update()
|
||||
#
|
||||
# i3.on("window::focus", on_window_focus)
|
||||
|
||||
|
||||
def on_workspace_focus(i3, e):
|
||||
global workspaces
|
||||
workspaces = i3.get_workspaces()
|
||||
update()
|
||||
|
||||
|
||||
i3.on("workspace::focus", on_workspace_focus)
|
||||
|
||||
# Starting
|
||||
|
||||
update()
|
||||
|
||||
|
||||
i3.main()
|
327
hm/desktop/frobar/.dev/pip.py
Executable file
327
hm/desktop/frobar/.dev/pip.py
Executable file
|
@ -0,0 +1,327 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Beautiful script
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import datetime
|
||||
import os
|
||||
import multiprocessing
|
||||
import i3ipc
|
||||
import difflib
|
||||
|
||||
# Constants
|
||||
FONT = "DejaVuSansMono Nerd Font Mono"
|
||||
|
||||
# TODO Update to be in sync with base16
|
||||
thm = [
|
||||
"#002b36",
|
||||
"#dc322f",
|
||||
"#859900",
|
||||
"#b58900",
|
||||
"#268bd2",
|
||||
"#6c71c4",
|
||||
"#2aa198",
|
||||
"#93a1a1",
|
||||
"#657b83",
|
||||
"#dc322f",
|
||||
"#859900",
|
||||
"#b58900",
|
||||
"#268bd2",
|
||||
"#6c71c4",
|
||||
"#2aa198",
|
||||
"#fdf6e3",
|
||||
]
|
||||
fg = "#93a1a1"
|
||||
bg = "#002b36"
|
||||
|
||||
THEMES = {
|
||||
"CENTER": (fg, bg),
|
||||
"DEFAULT": (thm[0], thm[8]),
|
||||
"1": (thm[0], thm[9]),
|
||||
"2": (thm[0], thm[10]),
|
||||
"3": (thm[0], thm[11]),
|
||||
"4": (thm[0], thm[12]),
|
||||
"5": (thm[0], thm[13]),
|
||||
"6": (thm[0], thm[14]),
|
||||
"7": (thm[0], thm[15]),
|
||||
}
|
||||
|
||||
# Utils
|
||||
|
||||
|
||||
def fitText(text, size):
|
||||
"""
|
||||
Add spaces or cut a string to be `size` characters long
|
||||
"""
|
||||
if size > 0:
|
||||
t = len(text)
|
||||
if t >= size:
|
||||
return text[:size]
|
||||
else:
|
||||
diff = size - t
|
||||
return text + " " * diff
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def fgColor(theme):
|
||||
global THEMES
|
||||
return THEMES[theme][0]
|
||||
|
||||
|
||||
def bgColor(theme):
|
||||
global THEMES
|
||||
return THEMES[theme][1]
|
||||
|
||||
|
||||
class Section:
|
||||
def __init__(self, theme="DEFAULT"):
|
||||
self.text = ""
|
||||
self.size = 0
|
||||
self.toSize = 0
|
||||
self.theme = theme
|
||||
self.visible = False
|
||||
self.name = ""
|
||||
|
||||
def update(self, text):
|
||||
if text == "":
|
||||
self.toSize = 0
|
||||
else:
|
||||
if len(text) < len(self.text):
|
||||
self.text = text + self.text[len(text) :]
|
||||
else:
|
||||
self.text = text
|
||||
self.toSize = len(text) + 3
|
||||
|
||||
def updateSize(self):
|
||||
"""
|
||||
Set the size for the next frame of animation
|
||||
Return if another frame is needed
|
||||
"""
|
||||
if self.toSize > self.size:
|
||||
self.size += 1
|
||||
elif self.toSize < self.size:
|
||||
self.size -= 1
|
||||
self.visible = self.size
|
||||
return self.toSize == self.size
|
||||
|
||||
def draw(self, left=True, nextTheme="DEFAULT"):
|
||||
s = ""
|
||||
if self.visible:
|
||||
if not left:
|
||||
if self.theme == nextTheme:
|
||||
s += ""
|
||||
else:
|
||||
s += "%{F" + bgColor(self.theme) + "}"
|
||||
s += "%{B" + bgColor(nextTheme) + "}"
|
||||
s += ""
|
||||
s += "%{F" + fgColor(self.theme) + "}"
|
||||
s += "%{B" + bgColor(self.theme) + "}"
|
||||
s += " " if self.size > 1 else ""
|
||||
s += fitText(self.text, self.size - 3)
|
||||
s += " " if self.size > 2 else ""
|
||||
if left:
|
||||
if self.theme == nextTheme:
|
||||
s += ""
|
||||
else:
|
||||
s += "%{F" + bgColor(self.theme) + "}"
|
||||
s += "%{B" + bgColor(nextTheme) + "}"
|
||||
s += ""
|
||||
return s
|
||||
|
||||
|
||||
# Section definition
|
||||
sTime = Section("3")
|
||||
|
||||
hostname = os.environ["HOSTNAME"].split(".")[0]
|
||||
sHost = Section("2")
|
||||
sHost.update(
|
||||
os.environ["USER"] + "@" + hostname.split("-")[-1] if "-" in hostname else hostname
|
||||
)
|
||||
|
||||
|
||||
# Groups definition
|
||||
gLeft = []
|
||||
gRight = [sTime, sHost]
|
||||
|
||||
# Bar handling
|
||||
bar = subprocess.Popen(["lemonbar", "-f", FONT, "-b"], stdin=subprocess.PIPE)
|
||||
|
||||
|
||||
def updateBar():
|
||||
global timeLastUpdate, timeUpdate
|
||||
global gLeft, gRight
|
||||
global outputs
|
||||
|
||||
text = ""
|
||||
for oi in range(len(outputs)):
|
||||
output = outputs[oi]
|
||||
gLeftFiltered = list(
|
||||
filter(
|
||||
lambda s: s.visible and (not s.output or s.output == output.name), gLeft
|
||||
)
|
||||
)
|
||||
tLeft = ""
|
||||
l = len(gLeftFiltered)
|
||||
for gi in range(l):
|
||||
g = gLeftFiltered[gi]
|
||||
# Next visible section for transition
|
||||
nextTheme = gLeftFiltered[gi + 1].theme if gi + 1 < l else "CENTER"
|
||||
tLeft = tLeft + g.draw(True, nextTheme)
|
||||
|
||||
tRight = ""
|
||||
for gi in range(len(gRight)):
|
||||
g = gRight[gi]
|
||||
nextTheme = "CENTER"
|
||||
for gn in gRight[gi + 1 :]:
|
||||
if gn.visible:
|
||||
nextTheme = gn.theme
|
||||
break
|
||||
tRight = g.draw(False, nextTheme) + tRight
|
||||
text += (
|
||||
"%{l}"
|
||||
+ tLeft
|
||||
+ "%{r}"
|
||||
+ tRight
|
||||
+ "%{B"
|
||||
+ bgColor("CENTER")
|
||||
+ "}"
|
||||
+ "%{S"
|
||||
+ str(oi + 1)
|
||||
+ "}"
|
||||
)
|
||||
|
||||
bar.stdin.write(bytes(text + "\n", "utf-8"))
|
||||
bar.stdin.flush()
|
||||
|
||||
|
||||
# Values
|
||||
i3 = i3ipc.Connection()
|
||||
outputs = []
|
||||
|
||||
|
||||
def on_output():
|
||||
global outputs
|
||||
outputs = sorted(
|
||||
sorted(
|
||||
list(filter(lambda o: o.active, i3.get_outputs())), key=lambda o: o.rect.y
|
||||
),
|
||||
key=lambda o: o.rect.x,
|
||||
)
|
||||
|
||||
|
||||
on_output()
|
||||
|
||||
|
||||
def on_workspace_focus():
|
||||
global i3
|
||||
global gLeft
|
||||
workspaces = i3.get_workspaces()
|
||||
wNames = [w.name for w in workspaces]
|
||||
sNames = [s.name for s in gLeft]
|
||||
|
||||
newGLeft = []
|
||||
|
||||
def actuate(section, workspace):
|
||||
if workspace:
|
||||
section.name = workspace.name
|
||||
section.output = workspace.output
|
||||
if workspace.visible:
|
||||
section.update(workspace.name)
|
||||
else:
|
||||
section.update(workspace.name.split(" ")[0])
|
||||
|
||||
if workspace.focused:
|
||||
section.theme = "4"
|
||||
elif workspace.urgent:
|
||||
section.theme = "1"
|
||||
else:
|
||||
section.theme = "6"
|
||||
else:
|
||||
section.update("")
|
||||
section.theme = "6"
|
||||
|
||||
for tag, i, j, k, l in difflib.SequenceMatcher(None, sNames, wNames).get_opcodes():
|
||||
if tag == "equal": # If the workspaces didn't changed
|
||||
for a in range(j - i):
|
||||
workspace = workspaces[k + a]
|
||||
section = gLeft[i + a]
|
||||
actuate(section, workspace)
|
||||
newGLeft.append(section)
|
||||
if tag in ("delete", "replace"): # If the workspaces were removed
|
||||
for section in gLeft[i:j]:
|
||||
if section.visible:
|
||||
actuate(section, None)
|
||||
newGLeft.append(section)
|
||||
else:
|
||||
del section
|
||||
if tag in ("insert", "replace"): # If the workspaces were removed
|
||||
for workspace in workspaces[k:l]:
|
||||
section = Section()
|
||||
actuate(section, workspace)
|
||||
newGLeft.append(section)
|
||||
gLeft = newGLeft
|
||||
|
||||
updateBar()
|
||||
|
||||
|
||||
on_workspace_focus()
|
||||
|
||||
|
||||
def i3events(i3childPipe):
|
||||
global i3
|
||||
|
||||
# Proxy functions
|
||||
def on_workspace_focus(i3, e):
|
||||
global i3childPipe
|
||||
i3childPipe.send("on_workspace_focus")
|
||||
|
||||
i3.on("workspace::focus", on_workspace_focus)
|
||||
|
||||
def on_output(i3, e):
|
||||
global i3childPipe
|
||||
i3childPipe.send("on_output")
|
||||
|
||||
i3.on("output", on_output)
|
||||
|
||||
i3.main()
|
||||
|
||||
|
||||
i3parentPipe, i3childPipe = multiprocessing.Pipe()
|
||||
i3process = multiprocessing.Process(target=i3events, args=(i3childPipe,))
|
||||
i3process.start()
|
||||
|
||||
|
||||
def updateValues():
|
||||
# Time
|
||||
now = datetime.datetime.now()
|
||||
sTime.update(now.strftime("%x %X"))
|
||||
|
||||
|
||||
def updateAnimation():
|
||||
for s in set(gLeft + gRight):
|
||||
s.updateSize()
|
||||
updateBar()
|
||||
|
||||
|
||||
lastUpdate = 0
|
||||
while True:
|
||||
now = time.time()
|
||||
if i3parentPipe.poll():
|
||||
msg = i3parentPipe.recv()
|
||||
if msg == "on_workspace_focus":
|
||||
on_workspace_focus()
|
||||
elif msg == "on_output":
|
||||
on_output()
|
||||
# TODO Restart lemonbar
|
||||
else:
|
||||
print(msg)
|
||||
updateAnimation()
|
||||
if now >= lastUpdate + 1:
|
||||
updateValues()
|
||||
lastUpdate = now
|
||||
|
||||
time.sleep(0.05)
|
10
hm/desktop/frobar/.dev/x.py
Executable file
10
hm/desktop/frobar/.dev/x.py
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import Xlib.display
|
||||
|
||||
dis = Xlib.display.Display()
|
||||
|
||||
nb = dis.screen_count()
|
||||
|
||||
for s in range(nb):
|
||||
print(s)
|
3
hm/desktop/frobar/.gitignore
vendored
Normal file
3
hm/desktop/frobar/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
dist/*
|
||||
frobar.egg-info/*
|
||||
__pycache__
|
48
hm/desktop/frobar/default.nix
Normal file
48
hm/desktop/frobar/default.nix
Normal file
|
@ -0,0 +1,48 @@
|
|||
{ pkgs ? import <nixpkgs> { config = { }; overlays = [ ]; }, ... }:
|
||||
# Tried using pyproject.nix but mpd2 dependency wouldn't resolve,
|
||||
# is called pyton-mpd2 on PyPi but mpd2 in nixpkgs.
|
||||
let
|
||||
frobar = pkgs.python3Packages.buildPythonApplication {
|
||||
pname = "frobar";
|
||||
version = "2.0";
|
||||
|
||||
runtimeInputs = with pkgs; [ lemonbar-xft wirelesstools ];
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
coloredlogs
|
||||
notmuch
|
||||
i3ipc
|
||||
mpd2
|
||||
psutil
|
||||
pulsectl
|
||||
pyinotify
|
||||
];
|
||||
makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath (with pkgs; [ lemonbar-xft wirelesstools ])}" ];
|
||||
|
||||
src = ./.;
|
||||
};
|
||||
in
|
||||
{
|
||||
config = {
|
||||
xsession.windowManager.i3.config.bars = [ ];
|
||||
programs.autorandr.hooks.postswitch = {
|
||||
frobar = "${pkgs.systemd}/bin/systemctl --user restart frobar";
|
||||
};
|
||||
systemd.user.services.frobar = {
|
||||
Unit = {
|
||||
Description = "frobar";
|
||||
After = [ "graphical-session-pre.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
Service = {
|
||||
# Wait for i3 to start. Can't use ExecStartPre because otherwise it blocks graphical-session.target, and there's nothing i3/systemd
|
||||
# TODO Do that better
|
||||
ExecStart = ''${pkgs.bash}/bin/bash -c "while ! ${pkgs.i3}/bin/i3-msg; do ${pkgs.coreutils}/bin/sleep 1; done; ${frobar}/bin/frobar"'';
|
||||
};
|
||||
|
||||
Install = { WantedBy = [ "graphical-session.target" ]; };
|
||||
};
|
||||
};
|
||||
}
|
||||
# TODO Connection with i3 is lost on start sometimes, more often than with Arch?
|
||||
# TODO Restore ability to build frobar with nix-build
|
64
hm/desktop/frobar/frobar/__init__.py
Normal file
64
hm/desktop/frobar/frobar/__init__.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
from frobar.providers import *
|
||||
|
||||
# TODO If multiple screen, expand the sections and share them
|
||||
# TODO Graceful exit
|
||||
|
||||
def run():
|
||||
Bar.init()
|
||||
Updater.init()
|
||||
|
||||
WORKSPACE_THEME = 0
|
||||
FOCUS_THEME = 3
|
||||
URGENT_THEME = 1
|
||||
CUSTOM_SUFFIXES = "▲■"
|
||||
|
||||
customNames = dict()
|
||||
for i in range(len(CUSTOM_SUFFIXES)):
|
||||
short = str(i + 1)
|
||||
full = short + " " + CUSTOM_SUFFIXES[i]
|
||||
customNames[short] = full
|
||||
Bar.addSectionAll(
|
||||
I3WorkspacesProvider(
|
||||
theme=WORKSPACE_THEME,
|
||||
themeFocus=FOCUS_THEME,
|
||||
themeUrgent=URGENT_THEME,
|
||||
themeMode=URGENT_THEME,
|
||||
customNames=customNames,
|
||||
),
|
||||
BarGroupType.LEFT,
|
||||
)
|
||||
|
||||
# TODO Middle
|
||||
Bar.addSectionAll(MpdProvider(theme=7), BarGroupType.LEFT)
|
||||
# Bar.addSectionAll(I3WindowTitleProvider(), BarGroupType.LEFT)
|
||||
|
||||
# TODO Computer modes
|
||||
|
||||
SYSTEM_THEME = 2
|
||||
DANGER_THEME = FOCUS_THEME
|
||||
CRITICAL_THEME = URGENT_THEME
|
||||
Bar.addSectionAll(CpuProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(RamProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(TemperatureProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT)
|
||||
|
||||
# Peripherals
|
||||
PERIPHERAL_THEME = 5
|
||||
NETWORK_THEME = 4
|
||||
# TODO Disk space provider
|
||||
# TODO Screen (connected, autorandr configuration, bbswitch) provider
|
||||
Bar.addSectionAll(PulseaudioProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(RfkillProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(NetworkProvider(theme=NETWORK_THEME), BarGroupType.RIGHT)
|
||||
|
||||
# Personal
|
||||
PERSONAL_THEME = 0
|
||||
# Bar.addSectionAll(KeystoreProvider(theme=PERSONAL_THEME), BarGroupType.RIGHT)
|
||||
# Bar.addSectionAll(NotmuchUnreadProvider(dir='~/.mail/', theme=PERSONAL_THEME), BarGroupType.RIGHT)
|
||||
# Bar.addSectionAll(TodoProvider(dir='~/.vdirsyncer/currentCalendars/', theme=PERSONAL_THEME), BarGroupType.RIGHT)
|
||||
|
||||
TIME_THEME = 6
|
||||
Bar.addSectionAll(TimeProvider(theme=TIME_THEME), BarGroupType.RIGHT)
|
||||
|
||||
# Bar.run()
|
723
hm/desktop/frobar/frobar/display.py
Normal file
723
hm/desktop/frobar/frobar/display.py
Normal file
|
@ -0,0 +1,723 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
import coloredlogs
|
||||
import i3ipc
|
||||
|
||||
from frobar.notbusy import notBusy
|
||||
|
||||
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
# TODO Allow deletion of Bar, BarGroup and Section for screen changes
|
||||
# IDEA Use i3 ipc events rather than relying on xrandr or Xlib (less portable
|
||||
# but easier)
|
||||
# TODO Optimize to use write() calls instead of string concatenation (writing
|
||||
# BarGroup strings should be a good compromise)
|
||||
# TODO Use bytes rather than strings
|
||||
# TODO Use default colors of lemonbar sometimes
|
||||
# TODO Adapt bar height with font height
|
||||
# TODO OPTI Static text objects that update its parents if modified
|
||||
# TODO forceSize and changeText are different
|
||||
|
||||
|
||||
class BarGroupType(enum.Enum):
|
||||
LEFT = 0
|
||||
RIGHT = 1
|
||||
# TODO Middle
|
||||
# MID_LEFT = 2
|
||||
# MID_RIGHT = 3
|
||||
|
||||
|
||||
class BarStdoutThread(threading.Thread):
|
||||
def run(self) -> None:
|
||||
while Bar.running:
|
||||
handle = Bar.process.stdout.readline().strip()
|
||||
if not len(handle):
|
||||
Bar.stop()
|
||||
if handle not in Bar.actionsH2F:
|
||||
log.error("Unknown action: {}".format(handle))
|
||||
continue
|
||||
function = Bar.actionsH2F[handle]
|
||||
function()
|
||||
|
||||
|
||||
class Bar:
|
||||
"""
|
||||
One bar for each screen
|
||||
"""
|
||||
|
||||
# Constants
|
||||
FONTS = ["DejaVuSansM Nerd Font"]
|
||||
FONTSIZE = 10
|
||||
|
||||
@staticmethod
|
||||
def init() -> None:
|
||||
Bar.running = True
|
||||
Section.init()
|
||||
|
||||
cmd = ["lemonbar", "-b", "-a", "64"]
|
||||
for font in Bar.FONTS:
|
||||
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
|
||||
Bar.process = subprocess.Popen(
|
||||
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||
)
|
||||
Bar.stdoutThread = BarStdoutThread()
|
||||
Bar.stdoutThread.start()
|
||||
|
||||
# Debug
|
||||
Bar(0)
|
||||
# Bar(1)
|
||||
|
||||
@staticmethod
|
||||
def stop() -> None:
|
||||
Bar.running = False
|
||||
Bar.process.kill()
|
||||
|
||||
# TODO This is not really the best way to do it I guess
|
||||
os.killpg(os.getpid(), signal.SIGTERM)
|
||||
|
||||
@staticmethod
|
||||
def run() -> None:
|
||||
Bar.forever()
|
||||
i3 = i3ipc.Connection()
|
||||
|
||||
def doStop(*args) -> None:
|
||||
Bar.stop()
|
||||
print(88)
|
||||
|
||||
try:
|
||||
i3.on("ipc_shutdown", doStop)
|
||||
i3.main()
|
||||
except BaseException:
|
||||
print(93)
|
||||
Bar.stop()
|
||||
|
||||
# Class globals
|
||||
everyone = set()
|
||||
string = ""
|
||||
process = None
|
||||
running = False
|
||||
|
||||
nextHandle = 0
|
||||
actionsF2H = dict()
|
||||
actionsH2F = dict()
|
||||
|
||||
@staticmethod
|
||||
def getFunctionHandle(function):
|
||||
assert callable(function)
|
||||
if function in Bar.actionsF2H.keys():
|
||||
return Bar.actionsF2H[function]
|
||||
|
||||
handle = "{:x}".format(Bar.nextHandle).encode()
|
||||
Bar.nextHandle += 1
|
||||
|
||||
Bar.actionsF2H[function] = handle
|
||||
Bar.actionsH2F[handle] = function
|
||||
|
||||
return handle
|
||||
|
||||
@staticmethod
|
||||
def forever():
|
||||
Bar.process.wait()
|
||||
Bar.stop()
|
||||
|
||||
def __init__(self, screen):
|
||||
assert isinstance(screen, int)
|
||||
self.screen = "%{S" + str(screen) + "}"
|
||||
self.groups = dict()
|
||||
|
||||
for groupType in BarGroupType:
|
||||
group = BarGroup(groupType, self)
|
||||
self.groups[groupType] = group
|
||||
|
||||
self.childsChanged = False
|
||||
|
||||
self.everyone.add(self)
|
||||
|
||||
@staticmethod
|
||||
def addSectionAll(section, group, screens=None):
|
||||
"""
|
||||
.. note::
|
||||
Add the section before updating it for the first time.
|
||||
"""
|
||||
assert isinstance(section, Section)
|
||||
assert isinstance(group, BarGroupType)
|
||||
# TODO screens selection
|
||||
for bar in Bar.everyone:
|
||||
bar.addSection(section, group=group)
|
||||
|
||||
def addSection(self, section, group):
|
||||
assert isinstance(section, Section)
|
||||
assert isinstance(group, BarGroupType)
|
||||
self.groups[group].addSection(section)
|
||||
|
||||
def update(self):
|
||||
if self.childsChanged:
|
||||
self.string = self.screen
|
||||
self.string += self.groups[BarGroupType.LEFT].string
|
||||
self.string += self.groups[BarGroupType.RIGHT].string
|
||||
|
||||
self.childsChanged = False
|
||||
|
||||
@staticmethod
|
||||
def updateAll():
|
||||
if Bar.running:
|
||||
Bar.string = ""
|
||||
for bar in Bar.everyone:
|
||||
bar.update()
|
||||
Bar.string += bar.string
|
||||
# Color for empty sections
|
||||
Bar.string += BarGroup.color(*Section.EMPTY)
|
||||
|
||||
# print(Bar.string)
|
||||
Bar.process.stdin.write(bytes(Bar.string + "\n", "utf-8"))
|
||||
Bar.process.stdin.flush()
|
||||
|
||||
|
||||
class BarGroup:
|
||||
"""
|
||||
One for each group of each bar
|
||||
"""
|
||||
|
||||
everyone = set()
|
||||
|
||||
def __init__(self, groupType, parent):
|
||||
assert isinstance(groupType, BarGroupType)
|
||||
assert isinstance(parent, Bar)
|
||||
|
||||
self.groupType = groupType
|
||||
self.parent = parent
|
||||
|
||||
self.sections = list()
|
||||
self.string = ""
|
||||
self.parts = []
|
||||
|
||||
#: One of the sections that had their theme or visibility changed
|
||||
self.childsThemeChanged = False
|
||||
|
||||
#: One of the sections that had their text (maybe their size) changed
|
||||
self.childsTextChanged = False
|
||||
|
||||
BarGroup.everyone.add(self)
|
||||
|
||||
def addSection(self, section):
|
||||
self.sections.append(section)
|
||||
section.addParent(self)
|
||||
|
||||
def addSectionAfter(self, sectionRef, section):
|
||||
index = self.sections.index(sectionRef)
|
||||
self.sections.insert(index + 1, section)
|
||||
section.addParent(self)
|
||||
|
||||
ALIGNS = {BarGroupType.LEFT: "%{l}", BarGroupType.RIGHT: "%{r}"}
|
||||
|
||||
@staticmethod
|
||||
def fgColor(color):
|
||||
return "%{F" + (color or "-") + "}"
|
||||
|
||||
@staticmethod
|
||||
def bgColor(color):
|
||||
return "%{B" + (color or "-") + "}"
|
||||
|
||||
@staticmethod
|
||||
def color(fg, bg):
|
||||
return BarGroup.fgColor(fg) + BarGroup.bgColor(bg)
|
||||
|
||||
def update(self):
|
||||
if self.childsThemeChanged:
|
||||
parts = [BarGroup.ALIGNS[self.groupType]]
|
||||
|
||||
secs = [sec for sec in self.sections if sec.visible]
|
||||
lenS = len(secs)
|
||||
for s in range(lenS):
|
||||
sec = secs[s]
|
||||
theme = Section.THEMES[sec.theme]
|
||||
if self.groupType == BarGroupType.LEFT:
|
||||
oSec = secs[s + 1] if s < lenS - 1 else None
|
||||
else:
|
||||
oSec = secs[s - 1] if s > 0 else None
|
||||
oTheme = (
|
||||
Section.THEMES[oSec.theme] if oSec is not None else Section.EMPTY
|
||||
)
|
||||
|
||||
if self.groupType == BarGroupType.LEFT:
|
||||
if s == 0:
|
||||
parts.append(BarGroup.bgColor(theme[1]))
|
||||
parts.append(BarGroup.fgColor(theme[0]))
|
||||
parts.append(sec)
|
||||
if theme == oTheme:
|
||||
parts.append("")
|
||||
else:
|
||||
parts.append(BarGroup.color(theme[1], oTheme[1]) + "")
|
||||
else:
|
||||
if theme is oTheme:
|
||||
parts.append("")
|
||||
else:
|
||||
parts.append(BarGroup.fgColor(theme[1]) + "")
|
||||
parts.append(BarGroup.color(*theme))
|
||||
parts.append(sec)
|
||||
|
||||
# TODO OPTI Concatenate successive strings
|
||||
self.parts = parts
|
||||
|
||||
if self.childsTextChanged or self.childsThemeChanged:
|
||||
self.string = ""
|
||||
for part in self.parts:
|
||||
if isinstance(part, str):
|
||||
self.string += part
|
||||
elif isinstance(part, Section):
|
||||
self.string += part.curText
|
||||
|
||||
self.parent.childsChanged = True
|
||||
|
||||
self.childsThemeChanged = False
|
||||
self.childsTextChanged = False
|
||||
|
||||
@staticmethod
|
||||
def updateAll():
|
||||
for group in BarGroup.everyone:
|
||||
group.update()
|
||||
Bar.updateAll()
|
||||
|
||||
|
||||
class SectionThread(threading.Thread):
|
||||
ANIMATION_START = 0.025
|
||||
ANIMATION_STOP = 0.001
|
||||
ANIMATION_EVOLUTION = 0.9
|
||||
|
||||
def run(self):
|
||||
while Section.somethingChanged.wait():
|
||||
notBusy.wait()
|
||||
Section.updateAll()
|
||||
animTime = self.ANIMATION_START
|
||||
frameTime = time.perf_counter()
|
||||
while len(Section.sizeChanging) > 0:
|
||||
frameTime += animTime
|
||||
curTime = time.perf_counter()
|
||||
sleepTime = frameTime - curTime
|
||||
time.sleep(sleepTime if sleepTime > 0 else 0)
|
||||
Section.updateAll()
|
||||
animTime *= self.ANIMATION_EVOLUTION
|
||||
if animTime < self.ANIMATION_STOP:
|
||||
animTime = self.ANIMATION_STOP
|
||||
|
||||
|
||||
class Section:
|
||||
# TODO Update all of that to base16
|
||||
# COLORS = ['#272822', '#383830', '#49483e', '#75715e', '#a59f85', '#f8f8f2',
|
||||
# '#f5f4f1', '#f9f8f5', '#f92672', '#fd971f', '#f4bf75', '#a6e22e',
|
||||
# '#a1efe4', '#66d9ef', '#ae81ff', '#cc6633']
|
||||
COLORS = [
|
||||
"#181818",
|
||||
"#AB4642",
|
||||
"#A1B56C",
|
||||
"#F7CA88",
|
||||
"#7CAFC2",
|
||||
"#BA8BAF",
|
||||
"#86C1B9",
|
||||
"#D8D8D8",
|
||||
"#585858",
|
||||
"#AB4642",
|
||||
"#A1B56C",
|
||||
"#F7CA88",
|
||||
"#7CAFC2",
|
||||
"#BA8BAF",
|
||||
"#86C1B9",
|
||||
"#F8F8F8",
|
||||
]
|
||||
FGCOLOR = "#F8F8F2"
|
||||
BGCOLOR = "#272822"
|
||||
|
||||
THEMES = list()
|
||||
EMPTY = (FGCOLOR, BGCOLOR)
|
||||
|
||||
ICON = None
|
||||
PERSISTENT = False
|
||||
|
||||
#: Sections that do not have their destination size
|
||||
sizeChanging = set()
|
||||
updateThread = SectionThread(daemon=True)
|
||||
somethingChanged = threading.Event()
|
||||
lastChosenTheme = 0
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
for t in range(8, 16):
|
||||
Section.THEMES.append((Section.COLORS[0], Section.COLORS[t]))
|
||||
|
||||
Section.updateThread.start()
|
||||
|
||||
def __init__(self, theme=None):
|
||||
#: Displayed section
|
||||
#: Note: A section can be empty and displayed!
|
||||
self.visible = False
|
||||
|
||||
if theme is None:
|
||||
theme = Section.lastChosenTheme
|
||||
Section.lastChosenTheme = (Section.lastChosenTheme + 1) % len(
|
||||
Section.THEMES
|
||||
)
|
||||
self.theme = theme
|
||||
|
||||
#: Displayed text
|
||||
self.curText = ""
|
||||
#: Displayed text size
|
||||
self.curSize = 0
|
||||
|
||||
#: Destination text
|
||||
self.dstText = Text(" ", Text(), " ")
|
||||
#: Destination size
|
||||
self.dstSize = 0
|
||||
|
||||
#: Groups that have this section
|
||||
self.parents = set()
|
||||
|
||||
self.icon = self.ICON
|
||||
self.persistent = self.PERSISTENT
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return "<{}><{}>{:01d}{}{:02d}/{:02d}".format(
|
||||
self.curText,
|
||||
self.dstText,
|
||||
self.theme,
|
||||
"+" if self.visible else "-",
|
||||
self.curSize,
|
||||
self.dstSize,
|
||||
)
|
||||
except:
|
||||
return super().__str__()
|
||||
|
||||
def addParent(self, parent):
|
||||
self.parents.add(parent)
|
||||
|
||||
def appendAfter(self, section):
|
||||
assert len(self.parents)
|
||||
for parent in self.parents:
|
||||
parent.addSectionAfter(self, section)
|
||||
|
||||
def informParentsThemeChanged(self):
|
||||
for parent in self.parents:
|
||||
parent.childsThemeChanged = True
|
||||
|
||||
def informParentsTextChanged(self):
|
||||
for parent in self.parents:
|
||||
parent.childsTextChanged = True
|
||||
|
||||
def updateText(self, text):
|
||||
if isinstance(text, str):
|
||||
text = Text(text)
|
||||
elif isinstance(text, Text) and not len(text.elements):
|
||||
text = None
|
||||
|
||||
self.dstText[0] = (
|
||||
None
|
||||
if (text is None and not self.persistent)
|
||||
else ((" " + self.icon + " ") if self.icon else " ")
|
||||
)
|
||||
self.dstText[1] = text
|
||||
self.dstText[2] = (
|
||||
" " if self.dstText[1] is not None and len(self.dstText[1]) else None
|
||||
)
|
||||
|
||||
self.dstSize = len(self.dstText)
|
||||
self.dstText.setSection(self)
|
||||
|
||||
if self.curSize == self.dstSize:
|
||||
if self.dstSize > 0:
|
||||
self.curText = str(self.dstText)
|
||||
self.informParentsTextChanged()
|
||||
else:
|
||||
Section.sizeChanging.add(self)
|
||||
Section.somethingChanged.set()
|
||||
|
||||
def setDecorators(self, **kwargs):
|
||||
self.dstText.setDecorators(**kwargs)
|
||||
self.curText = str(self.dstText)
|
||||
self.informParentsTextChanged()
|
||||
Section.somethingChanged.set()
|
||||
|
||||
def updateTheme(self, theme):
|
||||
assert isinstance(theme, int)
|
||||
assert theme < len(Section.THEMES)
|
||||
if theme == self.theme:
|
||||
return
|
||||
self.theme = theme
|
||||
self.informParentsThemeChanged()
|
||||
Section.somethingChanged.set()
|
||||
|
||||
def updateVisibility(self, visibility):
|
||||
assert isinstance(visibility, bool)
|
||||
|
||||
self.visible = visibility
|
||||
self.informParentsThemeChanged()
|
||||
Section.somethingChanged.set()
|
||||
|
||||
@staticmethod
|
||||
def fit(text, size):
|
||||
t = len(text)
|
||||
return text[:size] if t >= size else text + [" "] * (size - t)
|
||||
|
||||
def update(self):
|
||||
# TODO Might profit of a better logic
|
||||
if not self.visible:
|
||||
self.updateVisibility(True)
|
||||
return
|
||||
|
||||
if self.dstSize > self.curSize:
|
||||
self.curSize += 1
|
||||
elif self.dstSize < self.curSize:
|
||||
self.curSize -= 1
|
||||
else:
|
||||
# Visibility toggling must be done one step after curSize = 0
|
||||
if self.dstSize == 0:
|
||||
self.updateVisibility(False)
|
||||
Section.sizeChanging.remove(self)
|
||||
return
|
||||
|
||||
self.curText = self.dstText.text(size=self.curSize, pad=True)
|
||||
self.informParentsTextChanged()
|
||||
|
||||
@staticmethod
|
||||
def updateAll():
|
||||
"""
|
||||
Process all sections for text size changes
|
||||
"""
|
||||
|
||||
for sizeChanging in Section.sizeChanging.copy():
|
||||
sizeChanging.update()
|
||||
|
||||
BarGroup.updateAll()
|
||||
|
||||
Section.somethingChanged.clear()
|
||||
|
||||
@staticmethod
|
||||
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"):
|
||||
if p > 1:
|
||||
return ramp[-1]
|
||||
elif p < 0:
|
||||
return ramp[0]
|
||||
else:
|
||||
return ramp[round(p * (len(ramp) - 1))]
|
||||
|
||||
|
||||
class StatefulSection(Section):
|
||||
# TODO FEAT Allow to temporary expand the section (e.g. when important change)
|
||||
NUMBER_STATES = None
|
||||
DEFAULT_STATE = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Section.__init__(self, *args, **kwargs)
|
||||
self.state = self.DEFAULT_STATE
|
||||
if hasattr(self, "onChangeState"):
|
||||
self.onChangeState(self.state)
|
||||
self.setDecorators(
|
||||
clickLeft=self.incrementState, clickRight=self.decrementState
|
||||
)
|
||||
|
||||
def incrementState(self):
|
||||
newState = min(self.state + 1, self.NUMBER_STATES - 1)
|
||||
self.changeState(newState)
|
||||
|
||||
def decrementState(self):
|
||||
newState = max(self.state - 1, 0)
|
||||
self.changeState(newState)
|
||||
|
||||
def changeState(self, state):
|
||||
assert isinstance(state, int)
|
||||
assert state < self.NUMBER_STATES
|
||||
self.state = state
|
||||
if hasattr(self, "onChangeState"):
|
||||
self.onChangeState(state)
|
||||
self.refreshData()
|
||||
|
||||
|
||||
class ColorCountsSection(StatefulSection):
|
||||
# TODO FEAT Blend colors when not expanded
|
||||
# TODO FEAT Blend colors with importance of count
|
||||
# TODO FEAT Allow icons instead of counts
|
||||
NUMBER_STATES = 3
|
||||
COLORABLE_ICON = "?"
|
||||
|
||||
def __init__(self, theme=None):
|
||||
StatefulSection.__init__(self, theme=theme)
|
||||
|
||||
def fetcher(self):
|
||||
counts = self.subfetcher()
|
||||
# Nothing
|
||||
if not len(counts):
|
||||
return None
|
||||
# Icon colored
|
||||
elif self.state == 0 and len(counts) == 1:
|
||||
count, color = counts[0]
|
||||
return Text(self.COLORABLE_ICON, fg=color)
|
||||
# Icon
|
||||
elif self.state == 0 and len(counts) > 1:
|
||||
return Text(self.COLORABLE_ICON)
|
||||
# Icon + Total
|
||||
elif self.state == 1 and len(counts) > 1:
|
||||
total = sum([count for count, color in counts])
|
||||
return Text(self.COLORABLE_ICON, " ", total)
|
||||
# Icon + Counts
|
||||
else:
|
||||
text = Text(self.COLORABLE_ICON)
|
||||
for count, color in counts:
|
||||
text.append(" ", Text(count, fg=color))
|
||||
return text
|
||||
|
||||
|
||||
class Text:
|
||||
def _setElements(self, elements):
|
||||
# TODO OPTI Concatenate consecutrive string
|
||||
self.elements = list(elements)
|
||||
|
||||
def _setDecorators(self, decorators):
|
||||
# TODO OPTI Convert no decorator to strings
|
||||
self.decorators = decorators
|
||||
self.prefix = None
|
||||
self.suffix = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._setElements(args)
|
||||
self._setDecorators(kwargs)
|
||||
self.section = None
|
||||
|
||||
def append(self, *args):
|
||||
self._setElements(self.elements + list(args))
|
||||
|
||||
def prepend(self, *args):
|
||||
self._setElements(list(args) + self.elements)
|
||||
|
||||
def setElements(self, *args):
|
||||
self._setElements(args)
|
||||
|
||||
def setDecorators(self, **kwargs):
|
||||
self._setDecorators(kwargs)
|
||||
|
||||
def setSection(self, section):
|
||||
assert isinstance(section, Section)
|
||||
self.section = section
|
||||
for element in self.elements:
|
||||
if isinstance(element, Text):
|
||||
element.setSection(section)
|
||||
|
||||
def _genFixs(self):
|
||||
if self.prefix is not None and self.suffix is not None:
|
||||
return
|
||||
|
||||
self.prefix = ""
|
||||
self.suffix = ""
|
||||
|
||||
def nest(prefix, suffix):
|
||||
self.prefix = self.prefix + "%{" + prefix + "}"
|
||||
self.suffix = "%{" + suffix + "}" + self.suffix
|
||||
|
||||
def getColor(val):
|
||||
# TODO Allow themes
|
||||
assert isinstance(val, str) and len(val) == 7
|
||||
return val
|
||||
|
||||
def button(number, function):
|
||||
handle = Bar.getFunctionHandle(function)
|
||||
nest("A" + number + ":" + handle.decode() + ":", "A" + number)
|
||||
|
||||
for key, val in self.decorators.items():
|
||||
if val is None:
|
||||
continue
|
||||
if key == "fg":
|
||||
reset = self.section.THEMES[self.section.theme][0]
|
||||
nest("F" + getColor(val), "F" + reset)
|
||||
elif key == "bg":
|
||||
reset = self.section.THEMES[self.section.theme][1]
|
||||
nest("B" + getColor(val), "B" + reset)
|
||||
elif key == "clickLeft":
|
||||
button("1", val)
|
||||
elif key == "clickMiddle":
|
||||
button("2", val)
|
||||
elif key == "clickRight":
|
||||
button("3", val)
|
||||
elif key == "scrollUp":
|
||||
button("4", val)
|
||||
elif key == "scrollDown":
|
||||
button("5", val)
|
||||
else:
|
||||
log.warn("Unkown decorator: {}".format(key))
|
||||
|
||||
def _text(self, size=None, pad=False):
|
||||
self._genFixs()
|
||||
curString = self.prefix
|
||||
curSize = 0
|
||||
remSize = size
|
||||
|
||||
for element in self.elements:
|
||||
if element is None:
|
||||
continue
|
||||
elif isinstance(element, Text):
|
||||
newString, newSize = element._text(size=remSize)
|
||||
else:
|
||||
newString = str(element)
|
||||
if remSize is not None:
|
||||
newString = newString[:remSize]
|
||||
newSize = len(newString)
|
||||
|
||||
curString += newString
|
||||
curSize += newSize
|
||||
|
||||
if remSize is not None:
|
||||
remSize -= newSize
|
||||
if remSize <= 0:
|
||||
break
|
||||
|
||||
curString += self.suffix
|
||||
|
||||
if pad and remSize > 0:
|
||||
curString += " " * remSize
|
||||
curSize += remSize
|
||||
|
||||
if size is not None:
|
||||
if pad:
|
||||
assert size == curSize
|
||||
else:
|
||||
assert size >= curSize
|
||||
return curString, curSize
|
||||
|
||||
def text(self, *args, **kwargs):
|
||||
string, size = self._text(*args, **kwargs)
|
||||
return string
|
||||
|
||||
def __str__(self):
|
||||
self._genFixs()
|
||||
curString = self.prefix
|
||||
for element in self.elements:
|
||||
if element is None:
|
||||
continue
|
||||
else:
|
||||
curString += str(element)
|
||||
curString += self.suffix
|
||||
return curString
|
||||
|
||||
def __len__(self):
|
||||
curSize = 0
|
||||
for element in self.elements:
|
||||
if element is None:
|
||||
continue
|
||||
elif isinstance(element, Text):
|
||||
curSize += len(element)
|
||||
else:
|
||||
curSize += len(str(element))
|
||||
return curSize
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.elements[index]
|
||||
|
||||
def __setitem__(self, index, data):
|
||||
self.elements[index] = data
|
5
hm/desktop/frobar/frobar/notbusy.py
Normal file
5
hm/desktop/frobar/frobar/notbusy.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import threading
|
||||
|
||||
notBusy = threading.Event()
|
816
hm/desktop/frobar/frobar/providers.py
Normal file
816
hm/desktop/frobar/frobar/providers.py
Normal file
|
@ -0,0 +1,816 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
import coloredlogs
|
||||
import mpd
|
||||
import notmuch
|
||||
import psutil
|
||||
import pulsectl
|
||||
|
||||
from frobar.display import *
|
||||
from frobar.updaters import *
|
||||
|
||||
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||
log = logging.getLogger()
|
||||
|
||||
# TODO Generator class (for I3WorkspacesProvider, NetworkProvider and later
|
||||
# PulseaudioProvider and MpdProvider)
|
||||
|
||||
|
||||
def humanSize(num):
|
||||
"""
|
||||
Returns a string of width 3+3
|
||||
"""
|
||||
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
|
||||
if abs(num) < 1000:
|
||||
if num >= 10:
|
||||
return "{:3d}{}".format(int(num), unit)
|
||||
else:
|
||||
return "{:.1f}{}".format(num, unit)
|
||||
num /= 1024.0
|
||||
return "{:d}YiB".format(num)
|
||||
|
||||
|
||||
def randomColor(seed=0):
|
||||
random.seed(seed)
|
||||
return "#{:02x}{:02x}{:02x}".format(*[random.randint(0, 255) for _ in range(3)])
|
||||
|
||||
|
||||
class TimeProvider(StatefulSection, PeriodicUpdater):
|
||||
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
|
||||
NUMBER_STATES = len(FORMATS)
|
||||
DEFAULT_STATE = 1
|
||||
|
||||
def fetcher(self):
|
||||
now = datetime.datetime.now()
|
||||
return now.strftime(self.FORMATS[self.state])
|
||||
|
||||
def __init__(self, theme=None):
|
||||
PeriodicUpdater.__init__(self)
|
||||
StatefulSection.__init__(self, theme)
|
||||
self.changeInterval(1) # TODO OPTI When state < 1
|
||||
|
||||
|
||||
class AlertLevel(enum.Enum):
|
||||
NORMAL = 0
|
||||
WARNING = 1
|
||||
DANGER = 2
|
||||
|
||||
|
||||
class AlertingSection(StatefulSection):
|
||||
# TODO EASE Correct settings for themes
|
||||
THEMES = {AlertLevel.NORMAL: 2, AlertLevel.WARNING: 3, AlertLevel.DANGER: 1}
|
||||
PERSISTENT = True
|
||||
|
||||
def getLevel(self, quantity):
|
||||
if quantity > self.dangerThresold:
|
||||
return AlertLevel.DANGER
|
||||
elif quantity > self.warningThresold:
|
||||
return AlertLevel.WARNING
|
||||
else:
|
||||
return AlertLevel.NORMAL
|
||||
|
||||
def updateLevel(self, quantity):
|
||||
self.level = self.getLevel(quantity)
|
||||
self.updateTheme(self.THEMES[self.level])
|
||||
if self.level == AlertLevel.NORMAL:
|
||||
return
|
||||
# TODO Temporary update state
|
||||
|
||||
def __init__(self, theme):
|
||||
StatefulSection.__init__(self, theme)
|
||||
self.dangerThresold = 0.90
|
||||
self.warningThresold = 0.75
|
||||
|
||||
|
||||
class CpuProvider(AlertingSection, PeriodicUpdater):
|
||||
NUMBER_STATES = 3
|
||||
ICON = ""
|
||||
|
||||
def fetcher(self):
|
||||
percent = psutil.cpu_percent(percpu=False)
|
||||
self.updateLevel(percent / 100)
|
||||
if self.state >= 2:
|
||||
percents = psutil.cpu_percent(percpu=True)
|
||||
return "".join([Section.ramp(p / 100) for p in percents])
|
||||
elif self.state >= 1:
|
||||
return Section.ramp(percent / 100)
|
||||
|
||||
def __init__(self, theme=None):
|
||||
AlertingSection.__init__(self, theme)
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(1)
|
||||
|
||||
|
||||
class RamProvider(AlertingSection, PeriodicUpdater):
|
||||
"""
|
||||
Shows free RAM
|
||||
"""
|
||||
|
||||
NUMBER_STATES = 4
|
||||
ICON = ""
|
||||
|
||||
def fetcher(self):
|
||||
mem = psutil.virtual_memory()
|
||||
freePerc = mem.percent / 100
|
||||
self.updateLevel(freePerc)
|
||||
|
||||
if self.state < 1:
|
||||
return None
|
||||
|
||||
text = Text(Section.ramp(freePerc))
|
||||
if self.state >= 2:
|
||||
freeStr = humanSize(mem.total - mem.available)
|
||||
text.append(freeStr)
|
||||
if self.state >= 3:
|
||||
totalStr = humanSize(mem.total)
|
||||
text.append("/", totalStr)
|
||||
|
||||
return text
|
||||
|
||||
def __init__(self, theme=None):
|
||||
AlertingSection.__init__(self, theme)
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(1)
|
||||
|
||||
|
||||
class TemperatureProvider(AlertingSection, PeriodicUpdater):
|
||||
NUMBER_STATES = 2
|
||||
RAMP = ""
|
||||
|
||||
def fetcher(self):
|
||||
allTemp = psutil.sensors_temperatures()
|
||||
if "coretemp" not in allTemp:
|
||||
# TODO Opti Remove interval
|
||||
return ""
|
||||
temp = allTemp["coretemp"][0]
|
||||
|
||||
self.warningThresold = temp.high
|
||||
self.dangerThresold = temp.critical
|
||||
self.updateLevel(temp.current)
|
||||
|
||||
self.icon = Section.ramp(temp.current / temp.high, self.RAMP)
|
||||
if self.state >= 1:
|
||||
return "{:.0f}°C".format(temp.current)
|
||||
|
||||
def __init__(self, theme=None):
|
||||
AlertingSection.__init__(self, theme)
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class BatteryProvider(AlertingSection, PeriodicUpdater):
|
||||
# TODO Support ACPID for events
|
||||
NUMBER_STATES = 3
|
||||
RAMP = ""
|
||||
|
||||
def fetcher(self):
|
||||
bat = psutil.sensors_battery()
|
||||
if not bat:
|
||||
self.icon = None
|
||||
return None
|
||||
|
||||
self.icon = ("" if bat.power_plugged else "") + Section.ramp(
|
||||
bat.percent / 100, self.RAMP
|
||||
)
|
||||
|
||||
self.updateLevel(1 - bat.percent / 100)
|
||||
|
||||
if self.state < 1:
|
||||
return
|
||||
|
||||
t = Text("{:.0f}%".format(bat.percent))
|
||||
|
||||
if self.state < 2:
|
||||
return t
|
||||
|
||||
h = int(bat.secsleft / 3600)
|
||||
m = int((bat.secsleft - h * 3600) / 60)
|
||||
t.append(" ({:d}:{:02d})".format(h, m))
|
||||
return t
|
||||
|
||||
def __init__(self, theme=None):
|
||||
AlertingSection.__init__(self, theme)
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class PulseaudioProvider(StatefulSection, ThreadedUpdater):
|
||||
NUMBER_STATES = 3
|
||||
DEFAULT_STATE = 1
|
||||
|
||||
def __init__(self, theme=None):
|
||||
ThreadedUpdater.__init__(self)
|
||||
StatefulSection.__init__(self, theme)
|
||||
self.pulseEvents = pulsectl.Pulse("event-handler")
|
||||
|
||||
self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink)
|
||||
self.pulseEvents.event_callback_set(self.handleEvent)
|
||||
self.start()
|
||||
self.refreshData()
|
||||
|
||||
def fetcher(self):
|
||||
sinks = []
|
||||
with pulsectl.Pulse("list-sinks") as pulse:
|
||||
for sink in pulse.sink_list():
|
||||
if sink.port_active.name == "analog-output-headphones":
|
||||
icon = ""
|
||||
elif sink.port_active.name == "analog-output-speaker":
|
||||
icon = "" if sink.mute else ""
|
||||
elif sink.port_active.name == "headset-output":
|
||||
icon = ""
|
||||
else:
|
||||
icon = "?"
|
||||
vol = pulse.volume_get_all_chans(sink)
|
||||
fg = (sink.mute and "#333333") or (vol > 1 and "#FF0000") or None
|
||||
|
||||
t = Text(icon, fg=fg)
|
||||
sinks.append(t)
|
||||
|
||||
if self.state < 1:
|
||||
continue
|
||||
|
||||
if self.state < 2:
|
||||
if not sink.mute:
|
||||
ramp = " "
|
||||
while vol >= 0:
|
||||
ramp += self.ramp(vol if vol < 1 else 1)
|
||||
vol -= 1
|
||||
t.append(ramp)
|
||||
else:
|
||||
t.append(" {:2.0f}%".format(vol * 100))
|
||||
|
||||
return Text(*sinks)
|
||||
|
||||
def loop(self):
|
||||
self.pulseEvents.event_listen()
|
||||
|
||||
def handleEvent(self, ev):
|
||||
self.refreshData()
|
||||
|
||||
|
||||
class NetworkProviderSection(StatefulSection, Updater):
|
||||
NUMBER_STATES = 5
|
||||
DEFAULT_STATE = 1
|
||||
|
||||
def actType(self):
|
||||
self.ssid = None
|
||||
if self.iface.startswith("eth") or self.iface.startswith("enp"):
|
||||
if "u" in self.iface:
|
||||
self.icon = ""
|
||||
else:
|
||||
self.icon = ""
|
||||
elif self.iface.startswith("wlan") or self.iface.startswith("wl"):
|
||||
self.icon = ""
|
||||
if self.showSsid:
|
||||
cmd = ["iwgetid", self.iface, "--raw"]
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||
self.ssid = p.stdout.strip().decode()
|
||||
elif self.iface.startswith("tun") or self.iface.startswith("tap"):
|
||||
self.icon = ""
|
||||
elif self.iface.startswith("docker"):
|
||||
self.icon = ""
|
||||
elif self.iface.startswith("veth"):
|
||||
self.icon = ""
|
||||
elif self.iface.startswith("vboxnet"):
|
||||
self.icon = ""
|
||||
else:
|
||||
self.icon = "?"
|
||||
|
||||
def getAddresses(self):
|
||||
ipv4 = None
|
||||
ipv6 = None
|
||||
for address in self.parent.addrs[self.iface]:
|
||||
if address.family == socket.AF_INET:
|
||||
ipv4 = address
|
||||
elif address.family == socket.AF_INET6:
|
||||
ipv6 = address
|
||||
return ipv4, ipv6
|
||||
|
||||
def fetcher(self):
|
||||
self.icon = None
|
||||
self.persistent = False
|
||||
if (
|
||||
self.iface not in self.parent.stats
|
||||
or not self.parent.stats[self.iface].isup
|
||||
or self.iface.startswith("lo")
|
||||
):
|
||||
return None
|
||||
|
||||
# Get addresses
|
||||
ipv4, ipv6 = self.getAddresses()
|
||||
if ipv4 is None and ipv6 is None:
|
||||
return None
|
||||
|
||||
text = []
|
||||
self.persistent = True
|
||||
self.actType()
|
||||
|
||||
if self.showSsid and self.ssid:
|
||||
text.append(self.ssid)
|
||||
|
||||
if self.showAddress:
|
||||
if ipv4:
|
||||
netStrFull = "{}/{}".format(ipv4.address, ipv4.netmask)
|
||||
addr = ipaddress.IPv4Network(netStrFull, strict=False)
|
||||
addrStr = "{}/{}".format(ipv4.address, addr.prefixlen)
|
||||
text.append(addrStr)
|
||||
# TODO IPV6
|
||||
# if ipv6:
|
||||
# text += ' ' + ipv6.address
|
||||
|
||||
if self.showSpeed:
|
||||
recvDiff = (
|
||||
self.parent.IO[self.iface].bytes_recv
|
||||
- self.parent.prevIO[self.iface].bytes_recv
|
||||
)
|
||||
sentDiff = (
|
||||
self.parent.IO[self.iface].bytes_sent
|
||||
- self.parent.prevIO[self.iface].bytes_sent
|
||||
)
|
||||
recvDiff /= self.parent.dt
|
||||
sentDiff /= self.parent.dt
|
||||
text.append("↓{}↑{}".format(humanSize(recvDiff), humanSize(sentDiff)))
|
||||
|
||||
if self.showTransfer:
|
||||
text.append(
|
||||
"⇓{}⇑{}".format(
|
||||
humanSize(self.parent.IO[self.iface].bytes_recv),
|
||||
humanSize(self.parent.IO[self.iface].bytes_sent),
|
||||
)
|
||||
)
|
||||
|
||||
return " ".join(text)
|
||||
|
||||
def onChangeState(self, state):
|
||||
self.showSsid = state >= 1
|
||||
self.showAddress = state >= 2
|
||||
self.showSpeed = state >= 3
|
||||
self.showTransfer = state >= 4
|
||||
|
||||
def __init__(self, iface, parent):
|
||||
Updater.__init__(self)
|
||||
StatefulSection.__init__(self, theme=parent.theme)
|
||||
self.iface = iface
|
||||
self.parent = parent
|
||||
|
||||
|
||||
class NetworkProvider(Section, PeriodicUpdater):
|
||||
def fetchData(self):
|
||||
self.prev = self.last
|
||||
self.prevIO = self.IO
|
||||
|
||||
self.stats = psutil.net_if_stats()
|
||||
self.addrs = psutil.net_if_addrs()
|
||||
self.IO = psutil.net_io_counters(pernic=True)
|
||||
self.ifaces = self.stats.keys()
|
||||
|
||||
self.last = time.perf_counter()
|
||||
self.dt = self.last - self.prev
|
||||
|
||||
def fetcher(self):
|
||||
self.fetchData()
|
||||
|
||||
# Add missing sections
|
||||
lastSection = self
|
||||
for iface in sorted(list(self.ifaces)):
|
||||
if iface not in self.sections.keys():
|
||||
section = NetworkProviderSection(iface, self)
|
||||
lastSection.appendAfter(section)
|
||||
self.sections[iface] = section
|
||||
else:
|
||||
section = self.sections[iface]
|
||||
lastSection = section
|
||||
|
||||
# Refresh section text
|
||||
for section in self.sections.values():
|
||||
section.refreshData()
|
||||
|
||||
return None
|
||||
|
||||
def addParent(self, parent):
|
||||
self.parents.add(parent)
|
||||
self.refreshData()
|
||||
|
||||
def __init__(self, theme=None):
|
||||
PeriodicUpdater.__init__(self)
|
||||
Section.__init__(self, theme)
|
||||
|
||||
self.sections = dict()
|
||||
self.last = 0
|
||||
self.IO = dict()
|
||||
self.fetchData()
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class RfkillProvider(Section, PeriodicUpdater):
|
||||
# TODO FEAT rfkill doesn't seem to indicate that the hardware switch is
|
||||
# toggled
|
||||
PATH = "/sys/class/rfkill"
|
||||
|
||||
def fetcher(self):
|
||||
t = Text()
|
||||
for device in os.listdir(self.PATH):
|
||||
with open(os.path.join(self.PATH, device, "soft"), "rb") as f:
|
||||
softBlocked = f.read().strip() != b"0"
|
||||
with open(os.path.join(self.PATH, device, "hard"), "rb") as f:
|
||||
hardBlocked = f.read().strip() != b"0"
|
||||
|
||||
if not hardBlocked and not softBlocked:
|
||||
continue
|
||||
|
||||
with open(os.path.join(self.PATH, device, "type"), "rb") as f:
|
||||
typ = f.read().strip()
|
||||
|
||||
fg = (hardBlocked and "#CCCCCC") or (softBlocked and "#FF0000")
|
||||
if typ == b"wlan":
|
||||
icon = ""
|
||||
elif typ == b"bluetooth":
|
||||
icon = ""
|
||||
else:
|
||||
icon = "?"
|
||||
|
||||
t.append(Text(icon, fg=fg))
|
||||
return t
|
||||
|
||||
def __init__(self, theme=None):
|
||||
PeriodicUpdater.__init__(self)
|
||||
Section.__init__(self, theme)
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class SshAgentProvider(PeriodicUpdater):
|
||||
def fetcher(self):
|
||||
cmd = ["ssh-add", "-l"]
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
text = Text()
|
||||
for line in proc.stdout.split(b"\n"):
|
||||
if not len(line):
|
||||
continue
|
||||
fingerprint = line.split()[1]
|
||||
text.append(Text("", fg=randomColor(seed=fingerprint)))
|
||||
return text
|
||||
|
||||
def __init__(self):
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class GpgAgentProvider(PeriodicUpdater):
|
||||
def fetcher(self):
|
||||
cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"]
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
# proc = subprocess.run(cmd)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
text = Text()
|
||||
for line in proc.stdout.split(b"\n"):
|
||||
if not len(line) or line == b"OK":
|
||||
continue
|
||||
spli = line.split()
|
||||
if spli[6] != b"1":
|
||||
continue
|
||||
keygrip = spli[2]
|
||||
text.append(Text("", fg=randomColor(seed=keygrip)))
|
||||
return text
|
||||
|
||||
def __init__(self):
|
||||
PeriodicUpdater.__init__(self)
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class KeystoreProvider(Section, MergedUpdater):
|
||||
# TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless
|
||||
ICON = ""
|
||||
|
||||
def __init__(self, theme=None):
|
||||
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
|
||||
Section.__init__(self, theme)
|
||||
|
||||
|
||||
class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
|
||||
COLORABLE_ICON = ""
|
||||
|
||||
def subfetcher(self):
|
||||
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
|
||||
counts = []
|
||||
for account in self.accounts:
|
||||
queryStr = "folder:/{}/ and tag:unread".format(account)
|
||||
query = notmuch.Query(db, queryStr)
|
||||
nbMsgs = query.count_messages()
|
||||
if account == "frogeye":
|
||||
global q
|
||||
q = query
|
||||
if nbMsgs < 1:
|
||||
continue
|
||||
counts.append((nbMsgs, self.colors[account]))
|
||||
# db.close()
|
||||
return counts
|
||||
|
||||
def __init__(self, dir="~/.mail/", theme=None):
|
||||
PeriodicUpdater.__init__(self)
|
||||
ColorCountsSection.__init__(self, theme)
|
||||
|
||||
self.dir = os.path.realpath(os.path.expanduser(dir))
|
||||
assert os.path.isdir(self.dir)
|
||||
|
||||
# Fetching account list
|
||||
self.accounts = sorted(
|
||||
[a for a in os.listdir(self.dir) if not a.startswith(".")]
|
||||
)
|
||||
# Fetching colors
|
||||
self.colors = dict()
|
||||
for account in self.accounts:
|
||||
filename = os.path.join(self.dir, account, "color")
|
||||
with open(filename, "r") as f:
|
||||
color = f.read().strip()
|
||||
self.colors[account] = color
|
||||
|
||||
self.addPath(os.path.join(self.dir, ".notmuch", "xapian"))
|
||||
|
||||
|
||||
class TodoProvider(ColorCountsSection, InotifyUpdater):
|
||||
# TODO OPT/UX Maybe we could get more data from the todoman python module
|
||||
# TODO OPT Specific callback for specific directory
|
||||
COLORABLE_ICON = ""
|
||||
|
||||
def updateCalendarList(self):
|
||||
calendars = sorted(os.listdir(self.dir))
|
||||
for calendar in calendars:
|
||||
# If the calendar wasn't in the list
|
||||
if calendar not in self.calendars:
|
||||
self.addPath(os.path.join(self.dir, calendar), refresh=False)
|
||||
|
||||
# Fetching name
|
||||
path = os.path.join(self.dir, calendar, "displayname")
|
||||
with open(path, "r") as f:
|
||||
self.names[calendar] = f.read().strip()
|
||||
|
||||
# Fetching color
|
||||
path = os.path.join(self.dir, calendar, "color")
|
||||
with open(path, "r") as f:
|
||||
self.colors[calendar] = f.read().strip()
|
||||
self.calendars = calendars
|
||||
|
||||
def __init__(self, dir, theme=None):
|
||||
"""
|
||||
:parm str dir: [main]path value in todoman.conf
|
||||
"""
|
||||
InotifyUpdater.__init__(self)
|
||||
ColorCountsSection.__init__(self, theme=theme)
|
||||
self.dir = os.path.realpath(os.path.expanduser(dir))
|
||||
assert os.path.isdir(self.dir)
|
||||
|
||||
self.calendars = []
|
||||
self.colors = dict()
|
||||
self.names = dict()
|
||||
self.updateCalendarList()
|
||||
self.refreshData()
|
||||
|
||||
def countUndone(self, calendar):
|
||||
cmd = ["todo", "--porcelain", "list"]
|
||||
if calendar:
|
||||
cmd.append(self.names[calendar])
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||
data = json.loads(proc.stdout)
|
||||
return len(data)
|
||||
|
||||
def subfetcher(self):
|
||||
counts = []
|
||||
|
||||
# TODO This an ugly optimisation that cuts on features, but todoman
|
||||
# calls are very expensive so we keep that in the meanwhile
|
||||
if self.state < 2:
|
||||
c = self.countUndone(None)
|
||||
if c > 0:
|
||||
counts.append((c, "#00000"))
|
||||
counts.append((0, "#FFFFF"))
|
||||
return counts
|
||||
# Optimisation ends here
|
||||
|
||||
for calendar in self.calendars:
|
||||
c = self.countUndone(calendar)
|
||||
if c <= 0:
|
||||
continue
|
||||
counts.append((c, self.colors[calendar]))
|
||||
return counts
|
||||
|
||||
|
||||
class I3WindowTitleProvider(Section, I3Updater):
|
||||
# TODO FEAT To make this available from start, we need to find the
|
||||
# `focused=True` element following the `focus` array
|
||||
# TODO Feat Make this output dependant if wanted
|
||||
def on_window(self, i3, e):
|
||||
self.updateText(e.container.name)
|
||||
|
||||
def __init__(self, theme=None):
|
||||
I3Updater.__init__(self)
|
||||
Section.__init__(self, theme=theme)
|
||||
self.on("window", self.on_window)
|
||||
|
||||
|
||||
class I3WorkspacesProviderSection(Section):
|
||||
def selectTheme(self):
|
||||
if self.urgent:
|
||||
return self.parent.themeUrgent
|
||||
elif self.focused:
|
||||
return self.parent.themeFocus
|
||||
else:
|
||||
return self.parent.themeNormal
|
||||
|
||||
# TODO On mode change the state (shown / hidden) gets overriden so every
|
||||
# tab is shown
|
||||
|
||||
def show(self):
|
||||
self.updateTheme(self.selectTheme())
|
||||
self.updateText(self.fullName if self.focused else self.shortName)
|
||||
|
||||
def changeState(self, focused, urgent):
|
||||
self.focused = focused
|
||||
self.urgent = urgent
|
||||
self.show()
|
||||
|
||||
def setName(self, name):
|
||||
self.shortName = name
|
||||
self.fullName = (
|
||||
self.parent.customNames[name] if name in self.parent.customNames else name
|
||||
)
|
||||
|
||||
def switchTo(self):
|
||||
self.parent.i3.command("workspace {}".format(self.shortName))
|
||||
|
||||
def __init__(self, name, parent):
|
||||
Section.__init__(self)
|
||||
self.parent = parent
|
||||
self.setName(name)
|
||||
self.setDecorators(clickLeft=self.switchTo)
|
||||
self.tempText = None
|
||||
|
||||
def empty(self):
|
||||
self.updateTheme(self.parent.themeNormal)
|
||||
self.updateText(None)
|
||||
|
||||
def tempShow(self):
|
||||
self.updateText(self.tempText)
|
||||
|
||||
def tempEmpty(self):
|
||||
self.tempText = self.dstText[1]
|
||||
self.updateText(None)
|
||||
|
||||
|
||||
class I3WorkspacesProvider(Section, I3Updater):
|
||||
# TODO FEAT Multi-screen
|
||||
|
||||
def initialPopulation(self, parent):
|
||||
"""
|
||||
Called on init
|
||||
Can't reuse addWorkspace since i3.get_workspaces() gives dict and not
|
||||
ConObjects
|
||||
"""
|
||||
workspaces = self.i3.get_workspaces()
|
||||
lastSection = self.modeSection
|
||||
for workspace in workspaces:
|
||||
# if parent.display != workspace["display"]:
|
||||
# continue
|
||||
|
||||
section = I3WorkspacesProviderSection(workspace.name, self)
|
||||
section.focused = workspace.focused
|
||||
section.urgent = workspace.urgent
|
||||
section.show()
|
||||
parent.addSectionAfter(lastSection, section)
|
||||
self.sections[workspace.num] = section
|
||||
|
||||
lastSection = section
|
||||
|
||||
def on_workspace_init(self, i3, e):
|
||||
workspace = e.current
|
||||
i = workspace.num
|
||||
if i in self.sections:
|
||||
section = self.sections[i]
|
||||
else:
|
||||
# Find the section just before
|
||||
while i not in self.sections.keys() and i > 0:
|
||||
i -= 1
|
||||
prevSection = self.sections[i] if i != 0 else self.modeSection
|
||||
|
||||
section = I3WorkspacesProviderSection(workspace.name, self)
|
||||
prevSection.appendAfter(section)
|
||||
self.sections[workspace.num] = section
|
||||
section.focused = workspace.focused
|
||||
section.urgent = workspace.urgent
|
||||
section.show()
|
||||
|
||||
def on_workspace_empty(self, i3, e):
|
||||
self.sections[e.current.num].empty()
|
||||
|
||||
def on_workspace_focus(self, i3, e):
|
||||
self.sections[e.old.num].focused = False
|
||||
self.sections[e.old.num].show()
|
||||
self.sections[e.current.num].focused = True
|
||||
self.sections[e.current.num].show()
|
||||
|
||||
def on_workspace_urgent(self, i3, e):
|
||||
self.sections[e.current.num].urgent = e.current.urgent
|
||||
self.sections[e.current.num].show()
|
||||
|
||||
def on_workspace_rename(self, i3, e):
|
||||
self.sections[e.current.num].setName(e.name)
|
||||
self.sections[e.current.num].show()
|
||||
|
||||
def on_mode(self, i3, e):
|
||||
if e.change == "default":
|
||||
self.modeSection.updateText(None)
|
||||
for section in self.sections.values():
|
||||
section.tempShow()
|
||||
else:
|
||||
self.modeSection.updateText(e.change)
|
||||
for section in self.sections.values():
|
||||
section.tempEmpty()
|
||||
|
||||
def __init__(
|
||||
self, theme=0, themeFocus=3, themeUrgent=1, themeMode=2, customNames=dict()
|
||||
):
|
||||
I3Updater.__init__(self)
|
||||
Section.__init__(self)
|
||||
self.themeNormal = theme
|
||||
self.themeFocus = themeFocus
|
||||
self.themeUrgent = themeUrgent
|
||||
self.customNames = customNames
|
||||
|
||||
self.sections = dict()
|
||||
self.on("workspace::init", self.on_workspace_init)
|
||||
self.on("workspace::focus", self.on_workspace_focus)
|
||||
self.on("workspace::empty", self.on_workspace_empty)
|
||||
self.on("workspace::urgent", self.on_workspace_urgent)
|
||||
self.on("workspace::rename", self.on_workspace_rename)
|
||||
# TODO Un-handled/tested: reload, rename, restored, move
|
||||
|
||||
self.on("mode", self.on_mode)
|
||||
self.modeSection = Section(theme=themeMode)
|
||||
|
||||
def addParent(self, parent):
|
||||
self.parents.add(parent)
|
||||
parent.addSection(self.modeSection)
|
||||
self.initialPopulation(parent)
|
||||
|
||||
|
||||
class MpdProvider(Section, ThreadedUpdater):
|
||||
# TODO FEAT More informations and controls
|
||||
|
||||
MAX_LENGTH = 50
|
||||
|
||||
def connect(self):
|
||||
self.mpd.connect("localhost", 6600)
|
||||
|
||||
def __init__(self, theme=None):
|
||||
ThreadedUpdater.__init__(self)
|
||||
Section.__init__(self, theme)
|
||||
|
||||
self.mpd = mpd.MPDClient()
|
||||
self.connect()
|
||||
self.refreshData()
|
||||
self.start()
|
||||
|
||||
def fetcher(self):
|
||||
stat = self.mpd.status()
|
||||
if not len(stat) or stat["state"] == "stop":
|
||||
return None
|
||||
|
||||
cur = self.mpd.currentsong()
|
||||
if not len(cur):
|
||||
return None
|
||||
|
||||
infos = []
|
||||
|
||||
def tryAdd(field):
|
||||
if field in cur:
|
||||
infos.append(cur[field])
|
||||
|
||||
tryAdd("title")
|
||||
tryAdd("album")
|
||||
tryAdd("artist")
|
||||
|
||||
infosStr = " - ".join(infos)
|
||||
if len(infosStr) > MpdProvider.MAX_LENGTH:
|
||||
infosStr = infosStr[: MpdProvider.MAX_LENGTH - 1] + "…"
|
||||
|
||||
return " {}".format(infosStr)
|
||||
|
||||
def loop(self):
|
||||
try:
|
||||
self.mpd.idle("player")
|
||||
self.refreshData()
|
||||
except mpd.base.ConnectionError as e:
|
||||
log.warn(e, exc_info=True)
|
||||
self.connect()
|
||||
except BaseException as e:
|
||||
log.error(e, exc_info=True)
|
271
hm/desktop/frobar/frobar/updaters.py
Normal file
271
hm/desktop/frobar/frobar/updaters.py
Normal file
|
@ -0,0 +1,271 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import coloredlogs
|
||||
import i3ipc
|
||||
import pyinotify
|
||||
|
||||
from frobar.display import Text
|
||||
from frobar.notbusy import notBusy
|
||||
|
||||
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||
log = logging.getLogger()
|
||||
|
||||
# TODO Sync bar update with PeriodicUpdater updates
|
||||
|
||||
|
||||
|
||||
class Updater:
|
||||
@staticmethod
|
||||
def init():
|
||||
PeriodicUpdater.init()
|
||||
InotifyUpdater.init()
|
||||
notBusy.set()
|
||||
|
||||
def updateText(self, text):
|
||||
print(text)
|
||||
|
||||
def fetcher(self):
|
||||
return "{} refreshed".format(self)
|
||||
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def refreshData(self):
|
||||
# TODO OPTI Maybe discard the refresh if there's already another one?
|
||||
self.lock.acquire()
|
||||
try:
|
||||
data = self.fetcher()
|
||||
except BaseException as e:
|
||||
log.error(e, exc_info=True)
|
||||
data = ""
|
||||
self.updateText(data)
|
||||
self.lock.release()
|
||||
|
||||
|
||||
class PeriodicUpdaterThread(threading.Thread):
|
||||
def run(self):
|
||||
# TODO Sync with system clock
|
||||
counter = 0
|
||||
while True:
|
||||
notBusy.set()
|
||||
if PeriodicUpdater.intervalsChanged.wait(
|
||||
timeout=PeriodicUpdater.intervalStep
|
||||
):
|
||||
# ↑ sleeps here
|
||||
notBusy.clear()
|
||||
PeriodicUpdater.intervalsChanged.clear()
|
||||
counter = 0
|
||||
for providerList in PeriodicUpdater.intervals.copy().values():
|
||||
for provider in providerList.copy():
|
||||
provider.refreshData()
|
||||
else:
|
||||
notBusy.clear()
|
||||
counter += PeriodicUpdater.intervalStep
|
||||
counter = counter % PeriodicUpdater.intervalLoop
|
||||
for interval in PeriodicUpdater.intervals.keys():
|
||||
if counter % interval == 0:
|
||||
for provider in PeriodicUpdater.intervals[interval]:
|
||||
provider.refreshData()
|
||||
|
||||
|
||||
class PeriodicUpdater(Updater):
|
||||
"""
|
||||
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
|
||||
"""
|
||||
|
||||
intervals = dict()
|
||||
intervalStep = None
|
||||
intervalLoop = None
|
||||
updateThread = PeriodicUpdaterThread(daemon=True)
|
||||
intervalsChanged = threading.Event()
|
||||
|
||||
@staticmethod
|
||||
def gcds(*args):
|
||||
return functools.reduce(math.gcd, args)
|
||||
|
||||
@staticmethod
|
||||
def lcm(a, b):
|
||||
"""Return lowest common multiple."""
|
||||
return a * b // math.gcd(a, b)
|
||||
|
||||
@staticmethod
|
||||
def lcms(*args):
|
||||
"""Return lowest common multiple."""
|
||||
return functools.reduce(PeriodicUpdater.lcm, args)
|
||||
|
||||
@staticmethod
|
||||
def updateIntervals():
|
||||
intervalsList = list(PeriodicUpdater.intervals.keys())
|
||||
PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList)
|
||||
PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList)
|
||||
PeriodicUpdater.intervalsChanged.set()
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
PeriodicUpdater.updateThread.start()
|
||||
|
||||
def __init__(self):
|
||||
Updater.__init__(self)
|
||||
self.interval = None
|
||||
|
||||
def changeInterval(self, interval):
|
||||
assert isinstance(interval, int)
|
||||
|
||||
if self.interval is not None:
|
||||
PeriodicUpdater.intervals[self.interval].remove(self)
|
||||
|
||||
self.interval = interval
|
||||
|
||||
if interval not in PeriodicUpdater.intervals:
|
||||
PeriodicUpdater.intervals[interval] = set()
|
||||
PeriodicUpdater.intervals[interval].add(self)
|
||||
|
||||
PeriodicUpdater.updateIntervals()
|
||||
|
||||
|
||||
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
|
||||
def process_default(self, event):
|
||||
# DEBUG
|
||||
# from pprint import pprint
|
||||
# pprint(event.__dict__)
|
||||
# return
|
||||
|
||||
assert event.path in InotifyUpdater.paths
|
||||
|
||||
if 0 in InotifyUpdater.paths[event.path]:
|
||||
for provider in InotifyUpdater.paths[event.path][0]:
|
||||
provider.refreshData()
|
||||
|
||||
if event.name in InotifyUpdater.paths[event.path]:
|
||||
for provider in InotifyUpdater.paths[event.path][event.name]:
|
||||
provider.refreshData()
|
||||
|
||||
|
||||
class InotifyUpdater(Updater):
|
||||
"""
|
||||
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
|
||||
"""
|
||||
|
||||
wm = pyinotify.WatchManager()
|
||||
paths = dict()
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
notifier = pyinotify.ThreadedNotifier(
|
||||
InotifyUpdater.wm, InotifyUpdaterEventHandler()
|
||||
)
|
||||
notifier.start()
|
||||
|
||||
# TODO Mask for folders
|
||||
MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE
|
||||
|
||||
def addPath(self, path, refresh=True):
|
||||
path = os.path.realpath(os.path.expanduser(path))
|
||||
|
||||
# Detect if file or folder
|
||||
if os.path.isdir(path):
|
||||
self.dirpath = path
|
||||
# 0: Directory watcher
|
||||
self.filename = 0
|
||||
elif os.path.isfile(path):
|
||||
self.dirpath = os.path.dirname(path)
|
||||
self.filename = os.path.basename(path)
|
||||
else:
|
||||
raise FileNotFoundError("No such file or directory: '{}'".format(path))
|
||||
|
||||
# Register watch action
|
||||
if self.dirpath not in InotifyUpdater.paths:
|
||||
InotifyUpdater.paths[self.dirpath] = dict()
|
||||
if self.filename not in InotifyUpdater.paths[self.dirpath]:
|
||||
InotifyUpdater.paths[self.dirpath][self.filename] = set()
|
||||
InotifyUpdater.paths[self.dirpath][self.filename].add(self)
|
||||
|
||||
# Add watch
|
||||
InotifyUpdater.wm.add_watch(self.dirpath, InotifyUpdater.MASK)
|
||||
|
||||
if refresh:
|
||||
self.refreshData()
|
||||
|
||||
|
||||
class ThreadedUpdaterThread(threading.Thread):
|
||||
def __init__(self, updater, *args, **kwargs):
|
||||
self.updater = updater
|
||||
threading.Thread.__init__(self, *args, **kwargs)
|
||||
self.looping = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while self.looping:
|
||||
self.updater.loop()
|
||||
except BaseException as e:
|
||||
log.error("Error with {}".format(self.updater))
|
||||
log.error(e, exc_info=True)
|
||||
self.updater.updateText("")
|
||||
|
||||
|
||||
class ThreadedUpdater(Updater):
|
||||
"""
|
||||
Must implement loop(), and call start()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Updater.__init__(self)
|
||||
self.thread = ThreadedUpdaterThread(self, daemon=True)
|
||||
|
||||
def loop(self):
|
||||
self.refreshData()
|
||||
time.sleep(10)
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
||||
|
||||
|
||||
class I3Updater(ThreadedUpdater):
|
||||
# TODO OPTI One i3 connection for all
|
||||
|
||||
def __init__(self):
|
||||
ThreadedUpdater.__init__(self)
|
||||
self.i3 = i3ipc.Connection()
|
||||
self.start()
|
||||
|
||||
def on(self, event, function):
|
||||
self.i3.on(event, function)
|
||||
|
||||
def loop(self):
|
||||
self.i3.main()
|
||||
|
||||
|
||||
class MergedUpdater(Updater):
|
||||
# TODO OPTI Do not update until end of periodic batch
|
||||
def fetcher(self):
|
||||
text = Text()
|
||||
for updater in self.updaters:
|
||||
text.append(self.texts[updater])
|
||||
if not len(text):
|
||||
return None
|
||||
return text
|
||||
|
||||
def __init__(self, *args):
|
||||
Updater.__init__(self)
|
||||
|
||||
self.updaters = []
|
||||
self.texts = dict()
|
||||
|
||||
for updater in args:
|
||||
assert isinstance(updater, Updater)
|
||||
|
||||
def newUpdateText(updater, text):
|
||||
self.texts[updater] = text
|
||||
self.refreshData()
|
||||
|
||||
updater.updateText = newUpdateText.__get__(updater, Updater)
|
||||
|
||||
self.updaters.append(updater)
|
||||
self.texts[updater] = ""
|
20
hm/desktop/frobar/setup.py
Normal file
20
hm/desktop/frobar/setup.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="frobar",
|
||||
version="2.0",
|
||||
install_requires=[
|
||||
"coloredlogs",
|
||||
"notmuch",
|
||||
"i3ipc",
|
||||
"python-mpd2",
|
||||
"psutil",
|
||||
"pulsectl",
|
||||
"pyinotify",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"frobar = frobar:run",
|
||||
]
|
||||
},
|
||||
)
|
91
hm/desktop/qutebrowser.nix
Normal file
91
hm/desktop/qutebrowser.nix
Normal file
|
@ -0,0 +1,91 @@
|
|||
{ pkgs, lib, config, ... }:
|
||||
{
|
||||
config = lib.mkIf config.frogeye.desktop.xorg {
|
||||
home.sessionVariables = {
|
||||
BROWSER = "qutebrowser";
|
||||
};
|
||||
programs.qutebrowser = {
|
||||
enable = true;
|
||||
keyBindings = {
|
||||
normal = {
|
||||
# Match tab behaviour to i3. Not that I use tabs.
|
||||
"H" = "tab-prev";
|
||||
"J" = "back";
|
||||
"K" = "forward";
|
||||
"L" = "tab-next";
|
||||
# "T" = null;
|
||||
"af" = "spawn --userscript freshrss"; # TODO Broken?
|
||||
"as" = "spawn --userscript shaarli"; # TODO I don't use shaarli anymore
|
||||
# "d" = null;
|
||||
"u" = "undo --window";
|
||||
# TODO Unbind d and T (?)
|
||||
};
|
||||
};
|
||||
loadAutoconfig = true;
|
||||
searchEngines = rec {
|
||||
DEFAULT = ecosia;
|
||||
alpinep = "https://pkgs.alpinelinux.org/packages?name={}&branch=edge";
|
||||
ampwhat = "http://www.amp-what.com/unicode/search/{}";
|
||||
arch = "https://wiki.archlinux.org/?search={}";
|
||||
archp = "https://www.archlinux.org/packages/?q={}";
|
||||
aur = "https://aur.archlinux.org/packages/?K={}";
|
||||
aw = ampwhat;
|
||||
ddg = duckduckgo;
|
||||
dockerhub = "https://hub.docker.com/search/?isAutomated=0&isOfficial=0&page=1&pullCount=0&q={}&starCount=0";
|
||||
duckduckgo = "https://duckduckgo.com/?q={}&ia=web";
|
||||
ecosia = "https://www.ecosia.org/search?q={}";
|
||||
gfr = "https://www.google.fr/search?hl=fr&q={}";
|
||||
g = google;
|
||||
gh = github;
|
||||
gi = "http://images.google.com/search?q={}";
|
||||
giphy = "https://giphy.com/search/{}";
|
||||
github = "https://github.com/search?q={}";
|
||||
google = "https://www.google.fr/search?q={}";
|
||||
invidious = "https://invidious.frogeye.fr/search?q={}";
|
||||
inv = invidious;
|
||||
npm = "https://www.npmjs.com/search?q={}";
|
||||
q = qwant;
|
||||
qwant = "https://www.qwant.com/?t=web&q={}";
|
||||
wolfram = "https://www.wolframalpha.com/input/?i={}";
|
||||
youtube = "https://www.youtube.com/results?search_query={}";
|
||||
yt = youtube;
|
||||
};
|
||||
settings = {
|
||||
downloads.location.prompt = false;
|
||||
tabs = {
|
||||
show = "never";
|
||||
tabs_are_windows = true;
|
||||
};
|
||||
url = rec {
|
||||
open_base_url = true;
|
||||
start_pages = lib.mkDefault "https://geoffrey.frogeye.fr/blank.html";
|
||||
default_page = start_pages;
|
||||
};
|
||||
content = {
|
||||
# I had this setting below, not sure if it did something special
|
||||
# config.set("content.cookies.accept", "no-3rdparty", "chrome://*/*")
|
||||
cookies.accept = "no-3rdparty";
|
||||
prefers_reduced_motion = true;
|
||||
headers.accept_language = "fr-FR, fr;q=0.9, en-GB;q=0.8, en-US;q=0.7, en;q=0.6";
|
||||
tls.certificate_errors = "ask-block-thirdparty";
|
||||
};
|
||||
editor.command = [ "${pkgs.neovide}/bin/neovide" "--" "-f" "{file}" "-c" "normal {line}G{column0}l" ];
|
||||
# TODO Doesn't work on Arch. Does it even load the right profile on Nix?
|
||||
# TODO spellcheck.languages = ["fr-FR" "en-GB" "en-US"];
|
||||
};
|
||||
};
|
||||
xdg.mimeApps = {
|
||||
enable = true;
|
||||
defaultApplications = {
|
||||
"text/html" = "org.qutebrowser.qutebrowser.desktop";
|
||||
"x-scheme-handler/http" = "org.qutebrowser.qutebrowser.desktop";
|
||||
"x-scheme-handler/https" = "org.qutebrowser.qutebrowser.desktop";
|
||||
"x-scheme-handler/about" = "org.qutebrowser.qutebrowser.desktop";
|
||||
"x-scheme-handler/unknown" = "org.qutebrowser.qutebrowser.desktop";
|
||||
};
|
||||
};
|
||||
xsession.windowManager.i3.config.keybindings = {
|
||||
"${config.xsession.windowManager.i3.config.modifier}+m" = "exec ${config.programs.qutebrowser.package}/bin/qutebrowser --override-restore";
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue