├── lockwrap ├── .gitignore ├── screenshot01.png ├── screenshot02.png ├── screenshot03.png ├── menu_launcher.sh ├── scripts ├── infomenu ├── brightnessmenu ├── wmmenu ├── killmenu ├── dvdmenu ├── menumenu ├── lib │ ├── dmenurc │ ├── menu_helpers.sh │ └── i3.py ├── runmenu ├── cpumenu ├── netmenu ├── monitormenu ├── wallpapermenu ├── shutdownmenu ├── i3windowSelect.py └── mpcmenu ├── perl ├── brightness.pl ├── menu.pl ├── kill.pl ├── net.pl ├── cpugov.pl ├── shutdown.pl ├── service.pl ├── pwsafe.pl ├── run.pl ├── MenuSuite.pm └── mpc.pl ├── testmenu.sh └── README.md /lockwrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | flock -n /tmp/menusuite.lock -c "$*" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .flagged 2 | /scripts/__pycache__/ 3 | /scripts/lib/__pycache__/ 4 | -------------------------------------------------------------------------------- /screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaryHal/dmenu-suite/HEAD/screenshot01.png -------------------------------------------------------------------------------- /screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaryHal/dmenu-suite/HEAD/screenshot02.png -------------------------------------------------------------------------------- /screenshot03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaryHal/dmenu-suite/HEAD/screenshot03.png -------------------------------------------------------------------------------- /menu_launcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | 5 | flock -n /tmp/menusuite.lock -c "${MENU_DIR}/scripts/$*" 6 | -------------------------------------------------------------------------------- /scripts/infomenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | info=$( 7 | date +"%a %B %d, %I:%M %p" 8 | acpi -ba 9 | ) 10 | 11 | menu "" "$info" 12 | 13 | -------------------------------------------------------------------------------- /scripts/brightnessmenu: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | brightness="100 7 | 80 8 | 60 9 | 40 10 | 20" 11 | 12 | value=$(menu "Option: " "${brightness}") 13 | 14 | [[ -z "${value}" ]] && exit 15 | 16 | # notify-send "Brightness:" "Setting brightness to $value" 17 | xbacklight = "${value}" 18 | -------------------------------------------------------------------------------- /scripts/wmmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | windowname=$(menu "WindowSearch: " "") 7 | size=$(menu "Size: " "") 8 | 9 | # windowname=$(echo "" | $MenuCmd --prompt "Window Search: ") 10 | # size=$(echo "" | $MenuCmd --prompt "Window Size: ") 11 | 12 | wmctrl -r "$windowname" -e 0,0,0,$size 13 | -------------------------------------------------------------------------------- /perl/brightness.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | my @brightnessSteps = qw (100 80 60 40 20); 15 | my $brightness = MenuSuite::selectMenu('Brightness', \@brightnessSteps) || exit 0; 16 | 17 | exec "xbacklight = $brightness"; 18 | -------------------------------------------------------------------------------- /testmenu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/scripts/lib/menu_helpers.sh 5 | 6 | items=$'asdf\nzxcv\nqwer' 7 | 8 | value=$(menu "Hello World" "$items") 9 | # value=$($MenuProg $promptOption "Hello World" <<< "$items") 10 | confirm "$value z" "b" 11 | 12 | items=$'A♠\nK♥\nQ♦\nJ♣' 13 | selection=$(menu "Pick a card: " "$items") 14 | echo "$selection" 15 | -------------------------------------------------------------------------------- /scripts/killmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | processList=$(ps -ef | sed 1d) 7 | selection=$(menu "Kill: " "$processList") 8 | [[ -z "$selection" ]] && exit 9 | 10 | pid=$(awk '{print $2}' <<< "$selection" | tr '\n' ' ') 11 | [[ -z "$pid" ]] && exit 12 | 13 | notify-send "Killing Process:" "$selection" 14 | kill "$pid" 15 | 16 | -------------------------------------------------------------------------------- /scripts/dvdmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | mainMenu="[Chapter] 7 | [DvdNav] 8 | [Nav+MouseMovement]" 9 | 10 | action=$(menu "Option: " "$mainMenu") 11 | case "$action" in 12 | *'Chapter'*) 13 | chapter=$(menu "Chapter #: ") 14 | $(mpv dvd://$chapter) 15 | ;; 16 | *'DvdNav'*) 17 | $(mpv dvdnav://) 18 | ;; 19 | *'Nav+MouseMovement'*) 20 | $(mpv -mouse-movements dvdnav://) 21 | ;; 22 | esac 23 | 24 | -------------------------------------------------------------------------------- /scripts/menumenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | passthrough_args="$@" 7 | 8 | menuchoices=$(find "${MENU_DIR}" -maxdepth 1 -type f -exec basename {} \;) 9 | menuchoices="${menuchoices} 10 | [Common] Screenshot 11 | " 12 | choice=$(menu "Option: " "${menuchoices}") 13 | 14 | case "${choice}" in 15 | '[Common] Screenshot') 16 | maim -s 17 | # Automatic upload? 18 | ;; 19 | *) 20 | exec "${MENU_DIR}/${choice}" "${passthrough_args}" 21 | ;; 22 | esac 23 | -------------------------------------------------------------------------------- /scripts/lib/dmenurc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Base16-ashes theme. 4 | base00="#1C2023" 5 | base01="#393F45" 6 | base02="#565E65" 7 | base03="#747C84" 8 | base04="#ADB3BA" 9 | base05="#C7CCD1" 10 | base06="#DFE2E5" 11 | base07="#F3F4F5" 12 | base08="#C7AE95" 13 | base09="#C7C795" 14 | base0A="#AEC795" 15 | base0B="#95C7AE" 16 | base0C="#95AEC7" 17 | base0D="#AE95C7" 18 | base0E="#C795AE" 19 | base0F="#C79595" 20 | 21 | background="$base00" 22 | foreground="$base05" 23 | 24 | font="DroidSansFallback:bold:size=8" 25 | options="-i -fn $font -nf $foreground -nb $background -sf $background -sb $base0C" 26 | DMENU="dmenu $options" 27 | -------------------------------------------------------------------------------- /scripts/runmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | cachedir=${XDG_CACHE_HOME:-"$HOME/.cache"} 7 | if [[ -d "$cachedir" ]]; then 8 | cache=$cachedir/dmenu_run 9 | else 10 | cache=$HOME/.dmenu_cache # if no xdg dir, fall back to dotfile in ~ 11 | fi 12 | 13 | IFS=: 14 | if stest -dqr -n "$cache" $PATH; then 15 | cache=$(stest -flx $PATH | sort -u | tee "$cache") 16 | IFS=$' \t\n' 17 | else 18 | IFS=$' \t\n' 19 | cache=$(<$cache) 20 | fi 21 | 22 | app=$(menu "Run: " "$cache" ) 23 | 24 | [[ -n $app ]] && setsid "$app" 25 | -------------------------------------------------------------------------------- /scripts/cpumenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | current=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor) 7 | # read -r available < /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors 8 | available=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors | sed 's/ /\n/g') 9 | # gov=$(printf "$available" | $MenuCmd --prompt "[$current] Governors: ") 10 | 11 | gov=$(menu "[$current]: " "$available") 12 | 13 | [[ -z $gov ]] && exit 14 | 15 | # No longer use sudo -A. Use visudo to allow user to run cpupower. 16 | sudo cpupower frequency-set -g $gov 17 | -------------------------------------------------------------------------------- /scripts/netmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script assumes you can run "netctl" as a user through sudo without 3 | # entering a password. This can be done by editing /etc/sudoers via visudo. 4 | 5 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 6 | source $MENU_DIR/lib/menu_helpers.sh 7 | 8 | netchoices="$(netctl list)" 9 | other=" 10 | [Wifi-Menu]" 11 | 12 | choice=$(menu "Wifi: " "$(echo "$netchoices $other" | sed 's/^ *//')") 13 | [[ -z "$choice" ]] && exit 14 | 15 | if [[ "$choice" == "[Wifi-Menu]" ]]; then 16 | $TERMINAL -e sh -c "sudo wifi-menu" 17 | else 18 | # $choice = $(echo "$choice" | cut -c 3-) 19 | notify-send "Netctl:" "Switching to profile \"$choice\"" 20 | sudo netctl switch-to "$choice" 21 | fi 22 | -------------------------------------------------------------------------------- /perl/menu.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use File::Basename; 7 | use File::Glob ':bsd_glob'; 8 | 9 | use FindBin; 10 | use lib "$FindBin::Bin/"; 11 | 12 | use MenuSuite; 13 | 14 | use Data::Dumper; 15 | $Data::Dumper::Sortkeys = 1; 16 | 17 | my $this_dir = "$FindBin::Bin"; 18 | 19 | my @filenames = map { basename($_) } bsd_glob("$this_dir/*.pl"); 20 | my %menuOptions = map 21 | { 22 | my $filename = $_; 23 | $_ => sub { 24 | exec "perl $this_dir/$filename @ARGV" 25 | } 26 | } @filenames; 27 | 28 | $menuOptions{'# Arandr #'} = sub { exec 'setsid arandr'; }; 29 | $menuOptions{'# Screenshot #'} = sub { exec 'maim -s'; }; 30 | 31 | MenuSuite::runMenu('Run', \%menuOptions) || exit 0; 32 | -------------------------------------------------------------------------------- /perl/kill.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use Scalar::Util qw(looks_like_number); 10 | 11 | use MenuSuite; 12 | 13 | use Data::Dumper; 14 | $Data::Dumper::Sortkeys = 1; 15 | 16 | my $data = `ps aux`; # Already returns newline-delimited output 17 | my $selection = MenuSuite::promptMenu("Process List", $data) || exit 0; 18 | 19 | my @tokens = split /\s+/s, $selection; 20 | 21 | my $user = $tokens[0]; 22 | my $pid = $tokens[1]; 23 | 24 | if (looks_like_number($pid)) 25 | { 26 | # if ($user eq 'root') 27 | 28 | my $check = MenuSuite::promptMenu("Are you sure? (yes/no) ", $selection) || exit 0; 29 | exit 0 if uc($check) ne 'YES'; 30 | 31 | exec "kill $pid"; 32 | } 33 | -------------------------------------------------------------------------------- /perl/net.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # This script assumes you can run "netctl" as a user through sudo without 4 | # entering a password. This can be done by editing /etc/sudoers via visudo. 5 | 6 | use warnings; 7 | use strict; 8 | 9 | use FindBin; 10 | use lib "$FindBin::Bin/"; 11 | 12 | use MenuSuite; 13 | 14 | use Data::Dumper; 15 | $Data::Dumper::Sortkeys = 1; 16 | 17 | sub loadNetProfile 18 | { 19 | my ($profileName) = @_; 20 | 21 | exec 'sudo', 'netctl', 'switch-to', "$profileName"; 22 | } 23 | 24 | my @rawList = `netctl list`; 25 | 26 | my %profileOptions; 27 | foreach my $profile (@rawList) 28 | { 29 | chomp $profile; 30 | 31 | $profileOptions{$profile} = sub 32 | { 33 | loadNetProfile($profile); 34 | }; 35 | } 36 | 37 | $profileOptions{'# Wifi-Menu #'} = sub { 38 | exec 'termite', '-e', 'sudo wifi-menu'; 39 | }; 40 | 41 | MenuSuite::runMenu('Profile', \%profileOptions) || exit 0; 42 | -------------------------------------------------------------------------------- /perl/cpugov.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | my $scalingGovFile = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'; 15 | my $availableGovFile = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors'; 16 | 17 | my $currentGov; 18 | open(my $fh, '<', $scalingGovFile) or die "cannot open file $scalingGovFile"; 19 | { 20 | local $/; 21 | $currentGov = <$fh>; 22 | } 23 | chomp $currentGov; 24 | close $fh; 25 | 26 | my @availableGov; 27 | open($fh, '<', $availableGovFile) or die "cannot open file $availableGovFile"; 28 | { 29 | my $govOptionString = <$fh>; 30 | @availableGov = split /\s/s, $govOptionString; 31 | } 32 | close $fh; 33 | 34 | my $gov = MenuSuite::selectMenu("[${currentGov}]", \@availableGov) || exit 0; 35 | 36 | exec 'sudo', 'cpupower', 'frequency-set', '-g', "$gov > /dev/null"; 37 | -------------------------------------------------------------------------------- /scripts/monitormenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | # Menu Items 7 | options="[Laptop] 8 | [External] 9 | [MainLeft] 10 | [MainRight] 11 | [ARandR]" 12 | # [On] 13 | # [Off]" 14 | 15 | action=$(menu "Option: " "${options}") 16 | case "${action}" in 17 | '[Laptop]') 18 | xrandr --output LVDS1 --mode 1366x768 19 | xrandr --output VGA1 --off 20 | ;; 21 | '[External]') 22 | xrandr --output LVDS1 --off 23 | xrandr --output VGA1 --mode 1920x1080 24 | ;; 25 | '[MainLeft]') 26 | xrandr --output LVDS1 --mode 1366x768 --left-of VGA1 --primary 27 | xrandr --output VGA1 --mode 1920x1080 28 | ;; 29 | '[MainRight]') 30 | xrandr --output LVDS1 --mode 1366x768 --right-of VGA1 --primary 31 | xrandr --output VGA1 --mode 1920x1080 32 | ;; 33 | '[ARandR]') 34 | arandr & disown 35 | ;; 36 | # *'On'*) 37 | # xset dpms force on 38 | # ;; 39 | # *'Off'*) 40 | # xset dpms force off 41 | # ;; 42 | esac 43 | -------------------------------------------------------------------------------- /perl/shutdown.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | my @timings = qw(now +60 +45 +30 +15 +10 +5 +3 +2 +1); 15 | 16 | my %shutdownOptions = ( 17 | Shutdown => sub { 18 | my $delay = MenuSuite::selectMenu('When', \@timings) || exit 0; 19 | exec 'sudo', 'shutdown', '-P', "$delay"; 20 | }, 21 | Reboot => sub { 22 | my $delay = MenuSuite::selectMenu('When', \@timings) || exit 0; 23 | exec 'sudo', 'shutdown', '-r', "$delay"; 24 | }, 25 | Sleep => sub { 26 | exec 'sudo', 'systemctl', 'suspend'; 27 | }, 28 | Lock => sub { 29 | my $lockscreenWallpaper = $ENV{'HOME'} . '/docs/wallpapers/old/SoftAndClean.png'; 30 | exec 'i3lock', 31 | '--show-failed-attempts', 32 | '--color=EEEEEE', 33 | "--image=$lockscreenWallpaper", 34 | '--tiling'; 35 | }, 36 | Cancel => sub { 37 | exec 'sudo', 'shutdown', '-c'; 38 | }, 39 | ); 40 | 41 | MenuSuite::runMenu('Shutdown', \%shutdownOptions); 42 | -------------------------------------------------------------------------------- /scripts/wallpapermenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | wallpaper_dir="$HOME/docs/wallpapers/current" 7 | 8 | wallset_prog="setroot" 9 | img_options="-s" 10 | 11 | wallpaper_files=$(find "$wallpaper_dir" -maxdepth 1 -type f -exec basename {} \;) 12 | other_options=" 13 | [Random]" 14 | 15 | wp_options="$wallpaper_files $other_options" 16 | 17 | # num_monitors=$(menu "# of monitors: " "") 18 | num_monitors=$(xrandr -d :0 -q | grep ' connected' | wc -l) 19 | 20 | # choice=$(menu "File: " "$wallpaper_files $other_options") 21 | # [[ -z "$choice" ]] && exit 22 | 23 | declare -a wp 24 | 25 | for ((i=1; i <= $num_monitors; i++)) 26 | { 27 | selection=$(menu "$i/$num_monitors: " "$wp_options") 28 | [ -z "$selection" ] && exit 29 | 30 | case "$selection" in 31 | '[Random]') 32 | wp[$i]=$(echo "$wallpaper_files" | shuf -n1) 33 | ;; 34 | *) 35 | wp[$i]="$selection" 36 | ;; 37 | esac 38 | } 39 | 40 | command="$wallset_prog --store " 41 | for i in "${wp[@]}" 42 | do 43 | command="$command $img_options $wallpaper_dir/$i" 44 | done 45 | $command 46 | -------------------------------------------------------------------------------- /perl/service.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | sub getServiceList 15 | { 16 | my $serviceListString = `systemctl list-unit-files --type=service --no-legend`; 17 | my @lines = split("\n", $serviceListString); 18 | 19 | my %services; 20 | 21 | foreach my $line (@lines) 22 | { 23 | my @components = split(/\s+/, $line); 24 | my $serviceName = shift(@components); 25 | my $serviceStatus = shift(@components); 26 | 27 | my $serviceKey = "$serviceName ($serviceStatus)"; 28 | 29 | $services{$serviceKey} = sub 30 | { 31 | my @serviceActions = ('Enable', 'Disable', 'Start', 'Stop', 'Restart', 'Reload'); 32 | 33 | my $action = MenuSuite::selectMenu($serviceKey, \@serviceActions); 34 | 35 | if (grep /^$action$/, @serviceActions) 36 | { 37 | exec 'sudo', 'systemctl', lc($action), $serviceName; 38 | } 39 | }; 40 | } 41 | 42 | return %services; 43 | } 44 | 45 | my %services = getServiceList(); 46 | 47 | MenuSuite::runMenu('Service', \%services); 48 | -------------------------------------------------------------------------------- /perl/pwsafe.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | my $entriesFile = "/tmp/pwentries"; 15 | 16 | sub DumpPasswordEntries 17 | { 18 | my $ret = system("termite --class \"fzf-menu\" --geometry 560x80 -e \"pwsafe --list -o ${entriesFile}\""); 19 | 20 | if ($ret != 0) 21 | { 22 | system("rm ${entriesFile}"); 23 | die "pwsafe command busted or cancelled"; 24 | } 25 | } 26 | 27 | sub ReadPasswordEntries 28 | { 29 | open(my $fh, '<', $entriesFile); 30 | chomp(my @entries = <$fh>); 31 | close($fh); 32 | 33 | return @entries; 34 | } 35 | 36 | sub GetUsernamePassword 37 | { 38 | my ($entry) = @_; 39 | exec "termite --class \"fzf-menu\" --geometry 560x80 -e \"pwsafe -up '${entry}'\""; 40 | } 41 | 42 | sub GetAddEntry 43 | { 44 | exec "termite --class \"fzf-menu\" --geometry 560x80 -e \"pwsafe -add\""; 45 | } 46 | 47 | ## Build our menu 48 | 49 | my %entries = ( 50 | '# Reload #' => \&DumpPasswordEntries, 51 | '# Add #' => \&GetAddEntry, 52 | '# Edit #' => sub { 53 | my $entry = MenuSuite::promptMenu("Which") || exit 0; 54 | exec "termite --class \"fzf-menu\" --geometry 560x80 -e \"pwsafe -edit '${entry}'\""; 55 | }); 56 | 57 | foreach my $entry (ReadPasswordEntries()) 58 | { 59 | $entries{$entry} = sub { 60 | GetUsernamePassword($entry); 61 | }; 62 | } 63 | 64 | MenuSuite::runMenu("Entry", \%entries) || exit 0; 65 | -------------------------------------------------------------------------------- /scripts/shutdownmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Dmenu shutdown menu. 4 | # This script assumes you can run "shutdown" as a user through sudo without 5 | # entering a password. This can be done by editing /etc/sudoers via visudo. 6 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 7 | source $MENU_DIR/lib/menu_helpers.sh 8 | 9 | LOCKSCREEN_WALLPAPER="$HOME/docs/wallpapers/old/SoftAndClean.png" 10 | 11 | mainMenu="[Shutdown] 12 | [Reboot] 13 | [Sleep] 14 | [Lock] 15 | [Cancel] 16 | " 17 | #[Hibernate] #defunct 18 | 19 | pauseMenu="now 20 | +60 21 | +45 22 | +30 23 | +15 24 | +10 25 | +5 26 | +3 27 | +2 28 | +1" 29 | 30 | action=$(menu "Option: " "$mainMenu") 31 | [ -z "$action" ] && exit 32 | 33 | case "$action" in 34 | '[Shutdown]') 35 | pauseTime=$(menu "Pause: " "$pauseMenu") 36 | [ -z "$pauseTime" ] && exit 37 | 38 | notify-send "System:" "Shutdown scheduled - $pauseTime" 39 | sudo shutdown -P "$pauseTime" 40 | ;; 41 | '[Reboot]') 42 | pauseTime=$(menu "Pause: " "$pauseMenu") 43 | [ -z "$pauseTime" ] && exit 44 | 45 | notify-send "System:" "Reboot scheduled - $pauseTime" 46 | sudo shutdown -r "$pauseTime" 47 | ;; 48 | '[Sleep]') 49 | notify-send "System:" "Suspending now" 50 | sudo systemctl suspend 51 | ;; 52 | '[Lock]') 53 | i3lock --show-failed-attempts --color=EEEEEE --image="$LOCKSCREEN_WALLPAPER" --tiling & 54 | ;; 55 | '[Cancel]') 56 | notify-send "System:" "Shutdown command cancelled" 57 | sudo shutdown -c 58 | ;; 59 | esac 60 | -------------------------------------------------------------------------------- /perl/run.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # If using rofi, try using 'rofi -modi "run,drun" -show run'. 4 | 5 | use warnings; 6 | use strict; 7 | 8 | use FindBin; 9 | use lib "$FindBin::Bin/"; 10 | 11 | use File::Path qw(make_path); 12 | 13 | use MenuSuite; 14 | 15 | use Data::Dumper; 16 | $Data::Dumper::Sortkeys = 1; 17 | 18 | sub getDirectoriesStringFromPath 19 | { 20 | my $dirs = $ENV{'PATH'}; 21 | $dirs =~ s/:/ /g; 22 | 23 | return $dirs; 24 | } 25 | 26 | sub shouldUpdateCache 27 | { 28 | my ($cacheFile, $directories) = @_; 29 | return -z $cacheFile || !system("stest -dqr -n '$cacheFile' $directories"); 30 | } 31 | 32 | sub createDirectory 33 | { 34 | my ($directory) = @_; 35 | 36 | if ( ! -d $directory) { 37 | make_path $directory || die "Failed to create path: $directory"; 38 | } 39 | 40 | return; 41 | } 42 | 43 | sub createFile 44 | { 45 | my ($file) = @_; 46 | 47 | unless(-e $file) 48 | { 49 | open my $fc, ">", $file; 50 | close $fc; 51 | } 52 | 53 | return; 54 | } 55 | 56 | my $cacheDirectory = "$ENV{'HOME'}/.cache/dmenu/"; 57 | my $cacheFile = 'run_cache'; 58 | 59 | my $cachePath = "${cacheDirectory}/${cacheFile}"; 60 | 61 | createDirectory($cacheDirectory); 62 | createFile($cachePath); 63 | 64 | my @progs; 65 | my $searchdirs = getDirectoriesStringFromPath(); 66 | 67 | if (shouldUpdateCache($cachePath, $searchdirs)) 68 | { 69 | @progs = `stest -flx $searchdirs | sort -u | tee "$cachePath"`; 70 | } 71 | else 72 | { 73 | open(my $fh, '<', $cachePath) or die "cannot open file $cachePath"; 74 | { 75 | @progs = <$fh>; 76 | } 77 | close($fh); 78 | } 79 | 80 | my $cmd = MenuSuite::selectMenu("Run", \@progs) || exit 0; 81 | exec 'setsid', "$cmd"; 82 | -------------------------------------------------------------------------------- /scripts/lib/menu_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set to 1 is you want to use dmenurc instead of reading from the Xresources 4 | # database database (patched dmenu only (like dmenu2)) 5 | USE_DMENURC=0 6 | USE_VERTICAL=1 7 | 8 | MenuProg="" 9 | promptOption="" 10 | BACKEND=${1:-dmenu} 11 | 12 | case "${BACKEND}" in 13 | 'fzf') 14 | MenuProg="fzf --print-query" 15 | promptOption="--prompt" 16 | ;; 17 | 'fzy') 18 | MenuProg="fzy" 19 | promptOption="--prompt" 20 | ;; 21 | 'dmenu') 22 | if [[ $USE_DMENURC == 1 ]]; then 23 | # $MENU_DIR should be set in parent script... 24 | source $MENU_DIR/lib/dmenurc 25 | else 26 | if [[ $USE_VERTICAL == 1 ]]; then 27 | DMENU="dmenu -i -l 12 -x 403 -y 200 -w 560" 28 | else 29 | DMENU="dmenu -i" 30 | fi 31 | fi 32 | 33 | # Dmenu2 implements the '-s' option which allows us to choose which 34 | # monitor to open our menu on. 35 | MenuProg="$DMENU -s 0 " 36 | 37 | # MenuProg="$DMENU" 38 | 39 | promptOption="-p" 40 | ;; 41 | 'rofi') 42 | MenuProg="rofi -dmenu" 43 | promptOption="-p" 44 | ;; 45 | esac 46 | 47 | ################### 48 | ## Functions 49 | ################### 50 | 51 | function join 52 | { 53 | local IFS="$1" 54 | shift 55 | echo "$*" 56 | } 57 | 58 | function menu 59 | { 60 | # Grab the prompt message. 61 | local prompt="$1" 62 | shift 63 | 64 | # Combine the rest of our arguments. 65 | local items=$(join $'\n' "$@") 66 | 67 | $MenuProg $promptOption "${prompt}" <<< "${items}" | tail -1 68 | } 69 | 70 | # We can use menu() function for yes/no prompts. 71 | function confirm 72 | { 73 | menu "$*" 'No' 'Yes' 74 | } 75 | 76 | # And we can even use it for a simple notice. 77 | function alert 78 | { 79 | menu "$*" 'OK' 80 | } 81 | -------------------------------------------------------------------------------- /scripts/i3windowSelect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # dmenu script to jump to windows in i3. 3 | # 4 | # using ziberna's i3-py library: https://github.com/ziberna/i3-py 5 | # depends: dmenu (vertical patch), i3. 6 | # released by joepd under WTFPLv2-license: 7 | # http://sam.zoy.org/wtfpl/COPYING 8 | # 9 | # edited by Jure Ziberna for i3-py's examples section 10 | 11 | from lib import i3 12 | 13 | import subprocess 14 | import sys 15 | 16 | def i3clients(): 17 | """ 18 | Returns a dictionary of key-value pairs of a window text and window id. 19 | Each window text is of format "[workspace] window title (instance number)" 20 | """ 21 | clients = {} 22 | for ws_num in range(1,11): 23 | workspace = i3.filter(num=ws_num) 24 | if not workspace: 25 | continue 26 | workspace = workspace[0] 27 | windows = i3.filter(workspace, nodes=[]) 28 | instances = {} 29 | # Adds windows and their ids to the clients dictionary 30 | for window in windows: 31 | win_str = '[%s] %s' % (workspace['name'], window['name']) 32 | # Appends an instance number if other instances are present 33 | if win_str in instances: 34 | instances[win_str] += 1 35 | win_str = '%s (%d)' % (win_str, instances[win_str]) 36 | else: 37 | instances[win_str] = 1 38 | clients[win_str] = window['id'] 39 | return clients 40 | 41 | def win_menu(tool, clients, l=10, ): 42 | """ 43 | Displays a window menu using dmenu. Returns window id. 44 | """ 45 | # , '-x', '443', '-y', '200', '-w', '480' 46 | if tool == "dmenu": 47 | process = subprocess.Popen(['/usr/bin/dmenu', '-s', '0', '-i','-l', str(l)], 48 | stdin=subprocess.PIPE, 49 | stdout=subprocess.PIPE) 50 | elif tool == "fzf": 51 | process = subprocess.Popen(['fzf'], 52 | stdin=subprocess.PIPE, 53 | stdout=subprocess.PIPE) 54 | 55 | menu_str = '\n'.join(sorted(clients.keys())) 56 | 57 | # Popen.communicate returns a tuple stdout, stderr 58 | win_str = process.communicate(menu_str.encode('utf-8'))[0].decode('utf-8').rstrip() 59 | return clients.get(win_str, None) 60 | 61 | if __name__ == '__main__': 62 | narrowingTool = "dmenu" if len(sys.argv) < 2 else "fzf" 63 | clients = i3clients() 64 | win_id = win_menu(narrowingTool, clients) 65 | if win_id: 66 | i3.focus(con_id=win_id) 67 | -------------------------------------------------------------------------------- /perl/MenuSuite.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | package MenuSuite; 4 | 5 | use warnings; 6 | use strict; 7 | 8 | use Data::Dumper; 9 | 10 | use POSIX ':sys_wait_h'; 11 | use IPC::Open2; 12 | 13 | my ($menuProg) = @ARGV; 14 | $menuProg //= 'dmenu'; 15 | 16 | sub setMenuHandler 17 | { 18 | my ($prompt) = @_; 19 | 20 | if ($menuProg eq 'dmenu') 21 | { 22 | return "dmenu -i -l 12 -x 403 -y 200 -w 560 -s 0 -p '$prompt'"; 23 | } 24 | elsif ($menuProg eq 'fzf') 25 | { 26 | return "fzf $ENV{'FZF_DEFAULT_OPTS'} --print-query --prompt '$prompt: '"; 27 | } 28 | elsif ($menuProg eq 'fzf-tmux') 29 | { 30 | return "fzf-tmux $ENV{'FZF_DEFAULT_OPTS'} --print-query --prompt '$prompt'"; 31 | } 32 | elsif ($menuProg eq 'fzy') 33 | { 34 | return "fzy --prompt '$prompt'"; 35 | } 36 | elsif ($menuProg eq 'rofi') 37 | { 38 | return "rofi -dmenu -i -p '$prompt'"; 39 | } 40 | else 41 | { 42 | die "Invalid MenuProg $menuProg $!"; 43 | } 44 | } 45 | 46 | sub buildInputStringFromArray 47 | { 48 | my ($options) = @_; 49 | 50 | # Chomp every line in options, then join. Don't wanna double up on newlines! 51 | return join("\n", map { s/\s+\z//srx } @{$options}); 52 | } 53 | 54 | sub launchMenu 55 | { 56 | my ($prompt, $input) = @_; 57 | $input //= ''; 58 | 59 | my $menuCommand = setMenuHandler($prompt); 60 | my $pid = open2(\*CHILD_OUT, \*CHILD_IN, ${menuCommand}) || die "open2() failed $!"; 61 | 62 | binmode CHILD_OUT, ':encoding(UTF-8)'; 63 | binmode CHILD_IN, ':encoding(UTF-8)'; 64 | 65 | print CHILD_IN $input; 66 | close CHILD_IN; 67 | 68 | waitpid($pid, 0); 69 | 70 | # Get the last line of output, sadly this doesn't support multiple 71 | # selection. 72 | my $line = ''; 73 | while () 74 | { 75 | chomp; 76 | if (/\S/s) 77 | { 78 | $line = $_; 79 | } 80 | } 81 | 82 | close CHILD_OUT; 83 | 84 | if ($line eq '~kill') 85 | { 86 | die 'Kill switch activated'; 87 | } 88 | 89 | return $line; 90 | } 91 | 92 | sub promptMenu 93 | { 94 | my ($prompt, $info) = @_; 95 | return MenuSuite::launchMenu($prompt, $info // ''); 96 | } 97 | 98 | sub selectMenu 99 | { 100 | my ($prompt, $options) = @_; 101 | return launchMenu($prompt, buildInputStringFromArray($options // ())); 102 | } 103 | 104 | sub runMenu 105 | { 106 | my ($prompt, $dispatchTable) = @_; 107 | $dispatchTable //= (); 108 | 109 | my @menuOptions = sort keys %{$dispatchTable}; 110 | my $selection = launchMenu($prompt, buildInputStringFromArray(\@menuOptions)); 111 | 112 | my $defaultAction = sub {}; 113 | ((length $selection && $dispatchTable->{$selection}) || $defaultAction)->(); 114 | 115 | return $selection; 116 | } 117 | 118 | 1; 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Menu Suite 2 | 3 | This is a collection of shell scripts that interface with either [dmenu](http://tools.suckless.org/dmenu/) or [fzf](https://github.com/junegunn/fzf). There's also an experimental perl rewrite in the `perl` directory. 4 | 5 | Since everyone's *nix setups are different, these scripts fit my personal use case and may not fit yours. 6 | 7 | ## Included Scripts 8 | 9 | | Script | Description 10 | | -------------- | ----------- 11 | | brightnessmenu | Set laptop monitor brightness. 12 | | cpumenu | Query and set system's enabled cpu profiles. 13 | | dvdmenu | Navigate dvd with mpv. 14 | | i3windowSelect.py | Jump-to-window/workspace in [i3](http://i3wm.org/) (dmenu only) 15 | | infomenu | Display some system information with acpi. 16 | | killmenu | Kill processes. 17 | | menumenu | Menu to select menus. 18 | | monitormenu | Setup (preconfigured, hardcoded) monitor layouts with xrandr. 19 | | mpcmenu | Interface for [mpd](http://www.musicpd.org/) using [mpc](http://linux.die.net/man/1/mpc). 20 | | netmenu | Wifi profile select using netctl. 21 | | runmenu | List and run programs in user's $PATH. 22 | | shutdownmenu | Shutdown, reboot, and sleep. 23 | | wallpapermenu | Set a wallpaper (using setroot) from a hardcoded directory. Need to implement selecting specific wallpapers for specific monitors. 24 | | wmmenu | Unfinished script to interact with wmctrl. 25 | 26 | ## Additional Perl Scripts 27 | 28 | | Script | Description 29 | | -------------- | ----------- 30 | | MenuSuite.pm | Perl module to manage menu making by using dispatch tables or simple list selection. 31 | | brightness.pl | Set laptop monitor brightness. 32 | | cpugov.pl | Query and set system's enabled cpu profiles. 33 | | kill.pl | Kill processes. 34 | | menu.pl | Menu to select menus (in this directory). 35 | | mpc.pl | Interface for [mpd](http://www.musicpd.org/) using [Net::MPD](https://metacpan.org/pod/Net::MPD). 36 | | net.pl | Wifi profile select using netctl. 37 | | pwsafe.pl | Interact with pwsafe (mostly by spawning a terminal with that process running). 38 | | run.pl | List and run programs in user's $PATH. Should have the same functionality as dmenu_run. 39 | | shutdown.pl | Shutdown, reboot, and sleep. 40 | 41 | ## Screenshot(s) 42 | 43 | ![Dmenu2 screenshot](screenshot01.png) 44 | ![FZF screenshot](screenshot02.png) 45 | ![Rofi screenshot](screenshot03.png) 46 | 47 | ## Possible Usage 48 | 49 | All scripts take a single optional argument to decide which backend to use. If this argument is excluded, dmenu is used by default. Backends supported: fzf, dmenu, and rofi. 50 | 51 | Run mpcmenu (interface for mpd client) with dmenu: 52 | 53 | ~/bin/menu/scripts/mpcmenu dmenu 54 | 55 | Run mpcmenu in a new terminal emulator ([urxvt](https://en.wikipedia.org/wiki/Rxvt-unicode) or [termite](https://github.com/thestinger/termite)) window with fzf: 56 | 57 | urxvt -name "fzf-menu" -geometry 80x24 -e $HOME/bin/menu/scripts/mpcmenu fzf 58 | termite --class "fzf-menu" --geometry 640x480 -e "$HOME/bin/menu/scripts/mpcmenu fzf" 59 | 60 | We set an interface name for our urxvt window so we can allow a window manager to specifically manage these menus. For example, [bspwm](https://github.com/baskerville/bspwm) allows us to set rules for window interfaces: 61 | 62 | bspc rule -a fzf-menu floating=on,center=on,monitor=LVDS1,follow=on 63 | 64 | With this, my fzf-enabled menus will be floating and centered on my laptop monitor. It will also focus itself if I run it from any other monitor. 65 | 66 | A similar rule for i3 would be: 67 | 68 | for_window [class="^fzf-menu$"] floating enable, move output LVDS1 69 | 70 | #### Note: 71 | 72 | When spawning a new terminal to run the `runmenu` script (with fzf), sometimes the new process will not properly detach itself from its parent shell. So when the spawned terminal exits, the process started by `runmenu` will also exit. 73 | 74 | ## Xresources 75 | 76 | If you choose to use dmenu, `lib/menu_helpers` has a `USE_DMENURC` flag if you choose to not use the Xresources database (or don't have a version of dmenu that supports it). You should set that flag to `1` and modify `lib/dmenurc` in that case. 77 | 78 | If you choose to go the Xresources route, you can specify options in your `.Xresources` file. For example, dmenu2 reads these: 79 | 80 | dmenu.font: DroidSansFallback:bold:size=8 81 | dmenu.foreground: #C7CCD1 82 | dmenu.background: #1C2023 83 | dmenu.selbackground: #95AEC7 84 | dmenu.selforeground: #1C2023 85 | 86 | Remember to run `xrdb -merge ~/.Xresources` to load these new values! 87 | 88 | ## Lockfiles 89 | 90 | A useful concept relevant to these scripts are [semaphores](https://en.wikipedia.org/wiki/Semaphore_(programming)) (in particular, lockfiles). What lockfiles will allow use to do is to ensure that there is only one running menu at a time. There are many tools to do this: [lockfile](http://linux.die.net/man/1/lockfile), [flock](http://linux.die.net/man/1/flock), and some [homegrown](http://stackoverflow.com/questions/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at) (albeit flawed) solutions. `lockwrap` is an extremely simple script that will pass your command to flock under a predefined lock name: 91 | 92 | Inside lockwrap: 93 | 94 | #!/bin/sh 95 | flock -n /tmp/menusuite.lock -c $@ 96 | 97 | Usage: 98 | 99 | $ ./lockwrap ./mpcmenu 100 | $ ./lockwrap alsamixer 101 | 102 | If these commands are immediately run one after another, alsamixer will not be opened as mpcmenu would be using the lockfile. 103 | 104 | ## TODO 105 | 106 | ### Shell Scripts 107 | 108 | - Sourcing `menu_helpers.sh` introduces redundancy into every menu script. Maybe a way to solve this is to have another wrapper script to run each menu. 109 | 110 | - Merge `menu_helpers.sh` into `menuwrap.sh`. The problem I have with this though is that each menu script can no longer be run without delegation from `menuwrap.sh`. 111 | -------------------------------------------------------------------------------- /scripts/mpcmenu: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MENU_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | source $MENU_DIR/lib/menu_helpers.sh 5 | 6 | MUSIC_DIR="${HOME}/docs/music" 7 | CACHE_FILE="/tmp/mpcmenu_cache" 8 | 9 | tempOut=$(mpc --format "") 10 | 11 | # playState=$(echo "${tempOut}" | sed -n 2p | awk '{ print $1; }') 12 | 13 | stateLine=$(echo "${tempOut}" | sed 's/volume://') 14 | randomState=$(awk '/repeat:/{ print $5; }' <<< "${stateLine}") 15 | repeatState=$(awk '/repeat:/{ print $3; }' <<< "${stateLine}") 16 | singleState=$(awk '/repeat:/{ print $7; }' <<< "${stateLine}") 17 | consumeState=$(awk '/repeat:/{ print $9; }' <<< "${stateLine}") 18 | 19 | # Menu Items 20 | mainMenu="[Select] 21 | [PushDir] 22 | [PushLoop] 23 | [Current] 24 | [Play] 25 | [Stop] 26 | [Pause] 27 | [Load] 28 | [Playlist] 29 | [Clear] 30 | [Previous] 31 | [Next] 32 | [Seek] 33 | [Replay] 34 | [Random: ${randomState}] 35 | [Repeat: ${repeatState}] 36 | [Single: ${singleState}] 37 | [Consume: ${consumeState}] 38 | [Lyrics] 39 | [Rescan]" 40 | 41 | playlistOptions="[AddAll] 42 | [Remove] 43 | [Clear] 44 | [Save] 45 | [Load] 46 | [Delete]" 47 | 48 | seekOptions="0% 49 | 10% 50 | 20% 51 | 30% 52 | 40% 53 | 50% 54 | 60% 55 | 70% 56 | 80% 57 | 90%" 58 | 59 | filterOptions="any 60 | artist 61 | album 62 | title 63 | track 64 | name 65 | genre 66 | date 67 | composer 68 | performer 69 | comment 70 | disc 71 | filename" 72 | 73 | action=$(menu "Option: " "${mainMenu}") 74 | case "${action}" in 75 | '[Select]') 76 | candidates=$(mpc playlist --format '%position% [%album% - ][%artist% - ]%title%') 77 | selection=$(menu "Song: " "${candidates}") 78 | 79 | # Only select song if not in consume ("playback queue") mode. 80 | if [[ "${consumeState}" != "on" ]]; then 81 | if [[ -n "${selection}" ]]; then 82 | songNum=$(awk '{print $1}' <<< "${selection}") 83 | mpc -q play "${songNum}" 84 | fi 85 | fi 86 | ;; 87 | '[PushDir]') 88 | pushd ${MUSIC_DIR} > /dev/null 89 | musicDirList=$(find . -type d | sed -e 's!^\./!!') 90 | popd > /dev/null 91 | query=$(menu "Add: " "${musicDirList}") 92 | 93 | if [[ -n "${query}" ]]; then 94 | mpc ls "${query}" | mpc -q add 95 | fi 96 | ;; 97 | '[PushLoop]') 98 | # if [[ ! -a "${CACHE_FILE}" || "${CACHE_FILE}" -ot "${MUSIC_DIR}" ]]; then 99 | # notify-send "MPD:" "Building Cache" 100 | # musicList=$(mpc listall | tee "${CACHE_FILE}") 101 | # else 102 | # notify-send 'MPD:' 'Using Cache' 103 | # musicList=$(<${CACHE_FILE}) 104 | # fi 105 | 106 | filterType=$(menu "Filter Type: " "${filterOptions}") 107 | [[ -z "${filterType}" ]] && exit 108 | 109 | filterQuery=$(menu "Query (${filterType}): " "") 110 | 111 | # We should permit empty query strings so we can sift through all 112 | # elements of a filter type. 113 | 114 | # [[ -z "${filterQuery}" ]] && exit 115 | 116 | filterList=$(mpc search "${filterType}" "${filterQuery}") 117 | 118 | count=0 119 | selection="a" 120 | while [[ -n "${selection}" ]]; do 121 | selection=$(menu "Song ($count): " "${filterList}") 122 | echo " ${selection}" 123 | if [[ -n "${selection}" ]]; then 124 | mpc -q add "${selection}" 125 | ((count++)) 126 | fi 127 | done 128 | ;; 129 | '[Current]') 130 | songInfo=$(mpc -f 'Title: [%title%]\nArtist: [%artist%]\nAlbum: [%album%]\n ') 131 | notify-send "MPD:" "${songInfo}" 132 | ;; 133 | '[Play]') 134 | mpc -q toggle 135 | ;; 136 | '[Stop]') 137 | mpc -q stop 138 | ;; 139 | '[Pause]') 140 | mpc -q toggle 141 | ;; 142 | '[Load]') 143 | choice=$(menu "Load: " "$(mpc lsplaylists)") 144 | if [[ -n "${choice}" ]]; then 145 | mpc -q stop 146 | mpc -q clear 147 | mpc -q load "${choice}" 148 | notify-send "MPD:" "Playlist \"${choice}\" Loaded" 149 | fi 150 | ;; 151 | '[Playlist]') 152 | action=$(menu "Option: " "${playlistOptions}") 153 | case "${action}" in 154 | '[AddAll]') 155 | mpc -q update 156 | mpc ls | mpc -q add 157 | ;; 158 | '[Remove]') 159 | selection="a" 160 | while [[ -n "${selection}" ]]; do 161 | candidates=$(mpc playlist --format '%position% [%album% - ][%artist% - ]%title%') 162 | selection=$(menu "Song: " "${candidates}") 163 | 164 | if [[ -n "${selection}" ]]; then 165 | songNum=$(awk '{print $1}' <<< "${selection}") 166 | mpc -q del "${songNum}" 167 | fi 168 | done 169 | ;; 170 | '[Clear]') 171 | mpc -q clear 172 | notify-send "MPD:" "Playlist Cleared" 173 | ;; 174 | '[Load]') 175 | choice=$(menu "Load: " "$(mpc lsplaylists)") 176 | if [[ -n "${choice}" ]]; then 177 | mpc -q stop 178 | mpc -q clear 179 | mpc -q load "${choice}" 180 | notify-send "MPD:" "Playlist \"${choice}\" Loaded" 181 | fi 182 | ;; 183 | '[Save]') 184 | name=$(menu "Load: " "$(mpc lsplaylists)") 185 | if [[ -n "${name}" ]]; then 186 | mpc save "${name}" 187 | notify-send "MPD:" "Playlist \"${name}\" Saved" 188 | fi 189 | ;; 190 | '[Delete]') 191 | name=$(menu "Load: " "$(mpc lsplaylists)") 192 | if [[ -n "${name}" ]]; then 193 | mpc rm "${name}" 194 | notify-send "MPD:" "Playlist \"${name}\" Deleted" 195 | fi 196 | ;; 197 | esac 198 | ;; 199 | '[Clear]') 200 | mpc -q clear 201 | notify-send "MPD:" "Playlist Cleared" 202 | ;; 203 | '[Previous]') 204 | mpc -q prev 205 | ;; 206 | '[Next]') 207 | mpc -q next 208 | ;; 209 | '[Seek]') 210 | seek=$(menu "Seek: " "${seekOptions}") 211 | if [[ -n "${seek}" ]]; then 212 | mpc -q seek "${seek}" 213 | fi 214 | ;; 215 | '[Replay]') 216 | mpc -q stop 217 | mpc -q play 218 | ;; 219 | '[Random'*) 220 | mpc -q random 221 | ;; 222 | '[Repeat'*) 223 | mpc -q repeat 224 | ;; 225 | '[Single'*) 226 | mpc -q single 227 | ;; 228 | '[Consume'*) 229 | mpc -q consume 230 | ;; 231 | '[Lyrics]') 232 | song=$(mpc current) 233 | lyricsFile="$HOME/.lyrics/${song}.txt" 234 | 235 | if [[ -e "${lyricsFile}" ]]; then 236 | if [[ "${BACKEND}" == "fzf" ]]; then 237 | ${EDITOR:-gvim} "${lyricsFile}" 238 | else 239 | xdg-open "${lyricsFile}" 240 | fi 241 | # emacsclient --no-wait "${lyricsFile}" 2> /dev/null 242 | else 243 | notify-send "MPD:" "Lyrics for \"${song}\" not found" 244 | createFlag=$(confirm "Create Lyrics File?") 245 | if [[ "${createFlag,,}" == "yes" ]]; then 246 | touch "${lyricsFile}" 247 | notify-send "MPD:" "Created ${lyricsFile}" 248 | fi 249 | fi 250 | ;; 251 | '[Rescan]') 252 | notify-send "MPD Database" "Updating" 253 | mpc -q update 254 | ;; 255 | esac 256 | -------------------------------------------------------------------------------- /perl/mpc.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use FindBin; 7 | use lib "$FindBin::Bin/"; 8 | 9 | use MenuSuite; 10 | 11 | use Data::Dumper; 12 | $Data::Dumper::Sortkeys = 1; 13 | 14 | use Net::MPD; 15 | my $mpd_; 16 | 17 | sub mpc 18 | { 19 | if (!defined $mpd_) 20 | { 21 | $mpd_ = Net::MPD->connect(); 22 | } 23 | 24 | return $mpd_; 25 | } 26 | 27 | sub isPlaying 28 | { 29 | return mpc()->state ne 'stop'; 30 | } 31 | 32 | sub playOrPause 33 | { 34 | if (mpc()->state eq 'stop') 35 | { 36 | mpc()->play(); 37 | } 38 | elsif (mpc()->state eq 'pause') 39 | { 40 | mpc()->pause(0); 41 | } 42 | elsif (mpc()->state eq 'play') 43 | { 44 | mpc()->pause(1); 45 | } 46 | 47 | return; 48 | } 49 | 50 | sub getCurrentPlaylist 51 | { 52 | my @playlist = grep { scalar keys %{$_}; } mpc()->playlist_info(); 53 | if (!scalar @playlist) 54 | { 55 | MenuSuite::promptMenu('Info', 'Playlist is Empty'); 56 | return; 57 | } 58 | 59 | return \@{playlist}; 60 | } 61 | 62 | sub getCurrentSong 63 | { 64 | if (!mpc()->playlist_length) 65 | { 66 | MenuSuite::promptMenu('Info', 'Playlist is Empty'); 67 | return; 68 | } 69 | 70 | if (isPlaying()) 71 | { 72 | return \%{mpc()->current_song()}; 73 | } 74 | 75 | return \%{mpc()->playlist_info(0)}; 76 | } 77 | 78 | sub secondsToString 79 | { 80 | my ($seconds) = @_; 81 | return 82 | sprintf '%02d:%02d', 83 | ($seconds / 60) % 60, 84 | $seconds % 60; 85 | } 86 | 87 | sub listPlaylist 88 | { 89 | my ($songList) = @_; 90 | 91 | if (!scalar @{$songList}) 92 | { 93 | MenuSuite::promptMenu('Info', 'Playlist is Empty'); 94 | return; 95 | } 96 | 97 | my $i = 1; 98 | my %optionHash; 99 | foreach my $song (@{$songList}) 100 | { 101 | my $key = sprintf '%3i %s', $i, briefSongInfo($song); 102 | $optionHash{$key} = sub 103 | { 104 | my @data = detailedSongInfo($song); 105 | MenuSuite::selectMenu('Info', \@data); 106 | }; 107 | 108 | $i++; 109 | } 110 | 111 | MenuSuite::runMenu('List', \%optionHash); 112 | 113 | return; 114 | } 115 | 116 | sub briefSongInfo 117 | { 118 | my ($song) = @_; 119 | 120 | if (scalar keys %{$song}) 121 | { 122 | return sprintf '%s - %s - %s', 123 | $song->{'Title'}, 124 | $song->{'Artist'}, 125 | $song->{'Album'}; 126 | } 127 | } 128 | 129 | sub detailedSongInfo 130 | { 131 | my ($songInfo) = @_; 132 | 133 | my @data = ( 134 | $songInfo->{'Track'} . ': ' . $songInfo->{'Title'}, 135 | $songInfo->{'Artist'}, 136 | $songInfo->{'Album'}, 137 | # secondsToString(mpc()->elapsed), 138 | secondsToString($songInfo->{'Time'}), 139 | ); 140 | 141 | return @data; 142 | } 143 | 144 | sub showDetailedSongInfo 145 | { 146 | my $songInfo = getCurrentSong() || return; 147 | 148 | my @data = detailedSongInfo($songInfo); 149 | MenuSuite::selectMenu('Info', \@data); 150 | 151 | return; 152 | } 153 | 154 | sub showToggleMenu 155 | { 156 | my $boolToString = sub 157 | { 158 | return shift ? 'true' : 'false'; 159 | }; 160 | my $randomState = $boolToString->(mpc()->random); 161 | my $repeatState = $boolToString->(mpc()->repeat); 162 | my $consumeState = $boolToString->(mpc()->consume); 163 | my $singleState = $boolToString->(mpc()->single); 164 | 165 | # Proof of concept 166 | my %toggleOptions = ( 167 | "Random: $randomState" => sub { mpc()->random(mpc()->random ? 0 : 1); showToggleMenu(); }, 168 | "Repeat: $repeatState" => sub { mpc()->repeat(mpc()->repeat ? 0 : 1); showToggleMenu(); }, 169 | "Consume: $consumeState" => sub { mpc()->consume(mpc()->consume ? 0 : 1); showToggleMenu(); }, 170 | "Single: $singleState" => sub { mpc()->single(mpc()->single ? 0 : 1); showToggleMenu(); }, 171 | ); 172 | 173 | MenuSuite::runMenu('Toggle', \%toggleOptions); 174 | 175 | return; 176 | } 177 | 178 | sub songSeek 179 | { 180 | if (!isPlaying()) 181 | { 182 | MenuSuite::promptMenu('No song is playing'); 183 | exit 0; 184 | } 185 | 186 | my $seekValue = MenuSuite::promptMenu('Seek') || exit 0; 187 | 188 | if ($seekValue =~ /(\d+)%/s) 189 | { 190 | $seekValue = $1; 191 | 192 | my $songInfo = mpc()->current_song(); 193 | mpc()->seek_cur($songInfo->{Time} * $seekValue / 100.0); 194 | } 195 | elsif ($seekValue =~ /(?:(\d+):)?(\d+)/s) 196 | { 197 | my $minutes = $1 || 0; 198 | my $seconds = $2; 199 | 200 | mpc()->seek_cur($minutes * 60.0 + $seconds); 201 | } 202 | 203 | return; 204 | } 205 | 206 | sub songPushLoop 207 | { 208 | my ($songList) = @_; 209 | 210 | my @uriList = map { $_->{'uri'} } @{$songList}; 211 | my $songListStr = join("\n", @uriList); 212 | 213 | while (1) 214 | { 215 | my $uri = MenuSuite::promptMenu('Push', $songListStr) || last; 216 | mpc()->add($uri); 217 | } 218 | 219 | return; 220 | } 221 | 222 | my %mainOptions = ( 223 | Push => sub { 224 | my @songList = mpc()->list_all(); 225 | songPushLoop(\@songList); 226 | }, 227 | PushFilter => sub { 228 | my @filterTypes = ('any', 229 | 'artist', 230 | 'album', 231 | 'title', 232 | 'track', 233 | 'name', 234 | 'genre', 235 | 'date', 236 | 'composer', 237 | 'performer', 238 | 'comment', 239 | 'disc', 240 | 'filename', 241 | ); 242 | my $filterType = MenuSuite::selectMenu('Filter Type', \@filterTypes) || exit 0; 243 | 244 | my @filteredTags = mpc()->list($filterType); 245 | my $filter = MenuSuite::selectMenu("Filter Query ($filterType)", \@filteredTags) || exit 0; 246 | 247 | my @songList = mpc()->search($filterType, $filter); 248 | songPushLoop(\@songList); 249 | }, 250 | Remove => sub { 251 | while (1) 252 | { 253 | my $playlist = getCurrentPlaylist() || exit 0; 254 | my @options = map { $_->{Id} . ' ' . briefSongInfo($_); } @{$playlist}; 255 | 256 | my $song = MenuSuite::selectMenu('Remove', \@options) || exit 0; 257 | 258 | if ($song =~ /^(\d+)/s) 259 | { 260 | mpc()->delete_id($1); 261 | } 262 | } 263 | }, 264 | Current => sub { 265 | my $playlist = getCurrentPlaylist() || exit 0; 266 | listPlaylist($playlist); 267 | }, 268 | Play => \&playOrPause, 269 | PlayById => sub { 270 | my $playlist = getCurrentPlaylist() || exit 0; 271 | my @options = map { $_->{Id} . ' ' . briefSongInfo($_); } @$playlist; 272 | 273 | my $song = MenuSuite::selectMenu('Play', \@options) || exit 0; 274 | 275 | if ($song =~ /^(\d+)/s) 276 | { 277 | mpc()->play_id($1); 278 | } 279 | }, 280 | Next => sub { mpc()->next(); }, 281 | Prev => sub { mpc()->previous(); }, 282 | Pause => sub { mpc()->pause(); }, 283 | Stop => sub { mpc()->stop(); }, 284 | # Current => \&showDetailedSongInfo, 285 | Seek => \&songSeek, 286 | Playlist => sub 287 | { 288 | my @playlistList = map { $_->{playlist} } mpc()->list_playlists(); 289 | 290 | my %playlistMenuOptions = ( 291 | Save => sub 292 | { 293 | my $name = MenuSuite::promptMenu('Save') || exit 0; 294 | mpc()->save($name); 295 | }, 296 | List => sub 297 | { 298 | my $name = MenuSuite::selectMenu('List', \@playlistList) || exit 0; 299 | 300 | my @playlist = grep { scalar keys %$_; } mpc()->list_playlist_info($name); 301 | listPlaylist(\@playlist); 302 | }, 303 | Load => sub 304 | { 305 | my $name = MenuSuite::selectMenu('Load', \@playlistList) || exit 0; 306 | mpc()->load($name); 307 | }, 308 | Rename => sub 309 | { 310 | my $oldname = MenuSuite::selectMenu('Old Name', \@playlistList) || exit 0; 311 | my $newname = MenuSuite::promptMenu('New Name') || exit 0; 312 | mpc()->rename($oldname, $newname); 313 | }, 314 | Delete => sub 315 | { 316 | my $name = MenuSuite::selectMenu('Delete', \@playlistList) || exit 0; 317 | mpc()->rm($name); 318 | }, 319 | Clear => sub { mpc()->clear(); }, 320 | ); 321 | MenuSuite::runMenu('Playlist', \%playlistMenuOptions); 322 | }, 323 | Toggle => \&showToggleMenu, 324 | Rescan => sub { mpc()->update(); }, 325 | Stats => sub { 326 | my $stats = mpc()->stats(); 327 | my @data; 328 | foreach my $key (sort keys %{$stats}) 329 | { 330 | push @data, "$key: $stats->{$key}"; 331 | } 332 | 333 | MenuSuite::selectMenu('Stats', \@data); 334 | }, 335 | Lyrics => sub { 336 | my $songInfo = getCurrentSong() || exit 0; 337 | 338 | my $lyricsFile = sprintf 339 | '%s/.lyrics/%s - %s.txt', 340 | $ENV{'HOME'}, 341 | $songInfo->{'Artist'}, 342 | $songInfo->{'Title'}; 343 | 344 | binmode STDOUT, ':encoding(UTF-8)'; 345 | 346 | if (! -f "$lyricsFile") 347 | { 348 | my $selection = MenuSuite::promptMenu('Lyrics file not found, create? (yes/no)', "$lyricsFile") || exit 0; 349 | 350 | if (uc($selection) eq 'YES') 351 | { 352 | my $now = time; 353 | local (*TMP); 354 | 355 | utime($now, $now, $lyricsFile) 356 | || open(\*TMP, '>>', "$lyricsFile") 357 | || warn "Couldn't touch file: $!\n"; 358 | close TMP; 359 | } 360 | } 361 | else 362 | { 363 | system('xdg-open', "$lyricsFile") == 0 or die "Call to xdg-open failed: $!"; 364 | } 365 | }, 366 | ); 367 | 368 | MenuSuite::runMenu('Mpd', \%mainOptions); 369 | -------------------------------------------------------------------------------- /scripts/lib/i3.py: -------------------------------------------------------------------------------- 1 | #====================================================================== 2 | # i3 (Python module for communicating with i3 window manager) 3 | # Copyright (C) 2012 Jure Ziberna 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | #====================================================================== 18 | 19 | 20 | import sys 21 | import subprocess 22 | import json 23 | import socket 24 | import struct 25 | import threading 26 | import time 27 | 28 | ModuleType = type(sys) 29 | 30 | 31 | __author__ = 'Jure Ziberna' 32 | __version__ = '0.6.5' 33 | __date__ = '2012-06-20' 34 | __license__ = 'GNU GPL 3' 35 | 36 | 37 | MSG_TYPES = [ 38 | 'command', 39 | 'get_workspaces', 40 | 'subscribe', 41 | 'get_outputs', 42 | 'get_tree', 43 | 'get_marks', 44 | 'get_bar_config', 45 | ] 46 | 47 | EVENT_TYPES = [ 48 | 'workspace', 49 | 'output', 50 | ] 51 | 52 | 53 | class i3Exception(Exception): 54 | pass 55 | 56 | class MessageTypeError(i3Exception): 57 | """ 58 | Raised when message type isn't available. See i3.MSG_TYPES. 59 | """ 60 | def __init__(self, type): 61 | msg = "Message type '%s' isn't available" % type 62 | super(MessageTypeError, self).__init__(msg) 63 | 64 | class EventTypeError(i3Exception): 65 | """ 66 | Raised when even type isn't available. See i3.EVENT_TYPES. 67 | """ 68 | def __init__(self, type): 69 | msg = "Event type '%s' isn't available" % type 70 | super(EventTypeError, self).__init__(msg) 71 | 72 | class MessageError(i3Exception): 73 | """ 74 | Raised when a message to i3 is unsuccessful. 75 | That is, when it contains 'success': false in its JSON formatted response. 76 | """ 77 | pass 78 | 79 | class ConnectionError(i3Exception): 80 | """ 81 | Raised when a socket couldn't connect to the window manager. 82 | """ 83 | def __init__(self, socket_path): 84 | msg = "Could not connect to socket at '%s'" % socket_path 85 | super(ConnectionError, self).__init__(msg) 86 | 87 | 88 | def parse_msg_type(msg_type): 89 | """ 90 | Returns an i3-ipc code of the message type. Raises an exception if 91 | the given message type isn't available. 92 | """ 93 | try: 94 | index = int(msg_type) 95 | except ValueError: 96 | index = -1 97 | if index >= 0 and index < len(MSG_TYPES): 98 | return index 99 | msg_type = str(msg_type).lower() 100 | if msg_type in MSG_TYPES: 101 | return MSG_TYPES.index(msg_type) 102 | else: 103 | raise MessageTypeError(msg_type) 104 | 105 | def parse_event_type(event_type): 106 | """ 107 | Returns an i3-ipc string of the event_type. Raises an exception if 108 | the given event type isn't available. 109 | """ 110 | try: 111 | index = int(event_type) 112 | except ValueError: 113 | index = -1 114 | if index >= 0 and index < len(EVENT_TYPES): 115 | return EVENT_TYPES[index] 116 | event_type = str(event_type).lower() 117 | if event_type in EVENT_TYPES: 118 | return event_type 119 | else: 120 | raise EventTypeError(event_type) 121 | 122 | 123 | class Socket(object): 124 | """ 125 | Socket for communicating with the i3 window manager. 126 | Optional arguments: 127 | - path of the i3 socket. Path is retrieved from i3-wm itself via 128 | "i3.get_socket_path()" if not provided. 129 | - timeout in seconds 130 | - chunk_size in bytes 131 | - magic_string as a safety string for i3-ipc. Set to 'i3-ipc' by default. 132 | """ 133 | magic_string = 'i3-ipc' # safety string for i3-ipc 134 | chunk_size = 1024 # in bytes 135 | timeout = 0.5 # in seconds 136 | buffer = b'' # byte string 137 | 138 | def __init__(self, path=None, timeout=None, chunk_size=None, 139 | magic_string=None): 140 | if not path: 141 | path = get_socket_path() 142 | self.path = path 143 | if timeout: 144 | self.timeout = timeout 145 | if chunk_size: 146 | self.chunk_size = chunk_size 147 | if magic_string: 148 | self.magic_string = magic_string 149 | # Socket initialization and connection 150 | self.initialize() 151 | self.connect() 152 | # Struct format initialization, length of magic string is in bytes 153 | self.struct_header = '<%dsII' % len(self.magic_string.encode('utf-8')) 154 | self.struct_header_size = struct.calcsize(self.struct_header) 155 | 156 | def initialize(self): 157 | """ 158 | Initializes the socket. 159 | """ 160 | self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 161 | self.socket.settimeout(self.timeout) 162 | 163 | def connect(self, path=None): 164 | """ 165 | Connects the socket to socket path if not already connected. 166 | """ 167 | if not self.connected: 168 | self.initialize() 169 | if not path: 170 | path = self.path 171 | try: 172 | self.socket.connect(path) 173 | except socket.error: 174 | raise ConnectionError(path) 175 | 176 | def get(self, msg_type, payload=''): 177 | """ 178 | Convenience method, calls "socket.send(msg_type, payload)" and 179 | returns data from "socket.receive()". 180 | """ 181 | self.send(msg_type, payload) 182 | return self.receive() 183 | 184 | def subscribe(self, event_type, event=None): 185 | """ 186 | Subscribes to an event. Returns data on first occurrence. 187 | """ 188 | event_type = parse_event_type(event_type) 189 | # Create JSON payload from given event type and event 190 | payload = [event_type] 191 | if event: 192 | payload.append(event) 193 | payload = json.dumps(payload) 194 | return self.get('subscribe', payload) 195 | 196 | def send(self, msg_type, payload=''): 197 | """ 198 | Sends the given message type with given message by packing them 199 | and continuously sending bytes from the packed message. 200 | """ 201 | message = self.pack(msg_type, payload) 202 | # Continuously send the bytes from the message 203 | self.socket.sendall(message) 204 | 205 | def receive(self): 206 | """ 207 | Tries to receive a data. Unpacks the received byte string if 208 | successful. Returns None on failure. 209 | """ 210 | try: 211 | data = self.socket.recv(self.chunk_size) 212 | msg_magic, msg_length, msg_type = self.unpack_header(data) 213 | msg_size = self.struct_header_size + msg_length 214 | # Keep receiving data until the whole message gets through 215 | while len(data) < msg_size: 216 | data += self.socket.recv(msg_length) 217 | data = self.buffer + data 218 | return self.unpack(data) 219 | except socket.timeout: 220 | return None 221 | 222 | def pack(self, msg_type, payload): 223 | """ 224 | Packs the given message type and payload. Turns the resulting 225 | message into a byte string. 226 | """ 227 | msg_magic = self.magic_string 228 | # Get the byte count instead of number of characters 229 | msg_length = len(payload.encode('utf-8')) 230 | msg_type = parse_msg_type(msg_type) 231 | # "struct.pack" returns byte string, decoding it for concatenation 232 | msg_length = struct.pack('I', msg_length).decode('utf-8') 233 | msg_type = struct.pack('I', msg_type).decode('utf-8') 234 | message = '%s%s%s%s' % (msg_magic, msg_length, msg_type, payload) 235 | # Encoding the message back to byte string 236 | return message.encode('utf-8') 237 | 238 | def unpack(self, data): 239 | """ 240 | Unpacks the given byte string and parses the result from JSON. 241 | Returns None on failure and saves data into "self.buffer". 242 | """ 243 | data_size = len(data) 244 | msg_magic, msg_length, msg_type = self.unpack_header(data) 245 | msg_size = self.struct_header_size + msg_length 246 | # Message shouldn't be any longer than the data 247 | if data_size >= msg_size: 248 | payload = data[self.struct_header_size:msg_size].decode('utf-8') 249 | payload = json.loads(payload) 250 | self.buffer = data[msg_size:] 251 | return payload 252 | else: 253 | self.buffer = data 254 | return None 255 | 256 | def unpack_header(self, data): 257 | """ 258 | Unpacks the header of given byte string. 259 | """ 260 | return struct.unpack(self.struct_header, data[:self.struct_header_size]) 261 | 262 | @property 263 | def connected(self): 264 | """ 265 | Returns True if connected and False if not. 266 | """ 267 | try: 268 | self.get('command') 269 | return True 270 | except socket.error: 271 | return False 272 | 273 | def close(self): 274 | """ 275 | Closes the socket connection. 276 | """ 277 | self.socket.close() 278 | 279 | 280 | class Subscription(threading.Thread): 281 | """ 282 | Creates a new subscription and runs a listener loop. Calls the 283 | callback on event. 284 | Example parameters: 285 | callback = lambda event, data, subscription: print(data) 286 | event_type = 'workspace' 287 | event = 'focus' 288 | event_socket = 289 | data_socket = 290 | """ 291 | subscribed = False 292 | type_translation = { 293 | 'workspace': 'get_workspaces', 294 | 'output': 'get_outputs' 295 | } 296 | 297 | def __init__(self, callback, event_type, event=None, event_socket=None, 298 | data_socket=None): 299 | # Variable initialization 300 | if not callable(callback): 301 | raise TypeError('Callback must be callable') 302 | event_type = parse_event_type(event_type) 303 | self.callback = callback 304 | self.event_type = event_type 305 | self.event = event 306 | # Socket initialization 307 | if not event_socket: 308 | event_socket = Socket() 309 | self.event_socket = event_socket 310 | self.event_socket.subscribe(event_type, event) 311 | if not data_socket: 312 | data_socket = Socket() 313 | self.data_socket = data_socket 314 | # Thread initialization 315 | threading.Thread.__init__(self) 316 | self.start() 317 | 318 | def run(self): 319 | """ 320 | Wrapper method for the listen method -- handles exceptions. 321 | The method is run by the underlying "threading.Thread" object. 322 | """ 323 | try: 324 | self.listen() 325 | except socket.error: 326 | self.close() 327 | 328 | def listen(self): 329 | """ 330 | Runs a listener loop until self.subscribed is set to False. 331 | Calls the given callback method with data and the object itself. 332 | If event matches the given one, then matching data is retrieved. 333 | Otherwise, the event itself is sent to the callback. 334 | In that case 'change' key contains the thing that was changed. 335 | """ 336 | self.subscribed = True 337 | while self.subscribed: 338 | event = self.event_socket.receive() 339 | if not event: # skip an iteration if event is None 340 | continue 341 | if not self.event or ('change' in event and event['change'] == self.event): 342 | msg_type = self.type_translation[self.event_type] 343 | data = self.data_socket.get(msg_type) 344 | else: 345 | data = None 346 | self.callback(event, data, self) 347 | self.close() 348 | 349 | def close(self): 350 | """ 351 | Ends subscription loop by setting self.subscribed to False and 352 | closing both sockets. 353 | """ 354 | self.subscribed = False 355 | self.event_socket.close() 356 | if self.data_socket is not default_socket(): 357 | self.data_socket.close() 358 | 359 | 360 | def __call_cmd__(cmd): 361 | """ 362 | Returns output (stdout or stderr) of the given command args. 363 | """ 364 | try: 365 | output = subprocess.check_output(cmd) 366 | except subprocess.CalledProcessError as error: 367 | output = error.output 368 | output = output.decode('utf-8') # byte string decoding 369 | return output.strip() 370 | 371 | 372 | __socket__ = None 373 | def default_socket(socket=None): 374 | """ 375 | Returns i3.Socket object, which was initiliazed once with default values 376 | if no argument is given. 377 | Otherwise sets the default socket to the given socket. 378 | """ 379 | global __socket__ 380 | if socket and isinstance(socket, Socket): 381 | __socket__ = socket 382 | elif not __socket__: 383 | __socket__ = Socket() 384 | return __socket__ 385 | 386 | 387 | def msg(type, message=''): 388 | """ 389 | Takes a message type and a message itself. 390 | Talks to the i3 via socket and returns the response from the socket. 391 | """ 392 | response = default_socket().get(type, message) 393 | return response 394 | 395 | 396 | def __function__(type, message='', *args, **crit): 397 | """ 398 | Accepts a message type, a message. Takes optional args and keyword 399 | args which are present in all future calls of the resulting function. 400 | Returns a function, which takes arguments and container criteria. 401 | If message type was 'command', the function returns success value. 402 | """ 403 | def function(*args2, **crit2): 404 | msg_full = ' '.join([message] + list(args) + list(args2)) 405 | criteria = dict(crit) 406 | criteria.update(crit2) 407 | if criteria: 408 | msg_full = '%s %s' % (container(**criteria), msg_full) 409 | response = msg(type, msg_full) 410 | response = success(response) 411 | if isinstance(response, i3Exception): 412 | raise response 413 | return response 414 | function.__name__ = type 415 | function.__doc__ = 'Message sender (type: %s, message: %s)' % (type, message) 416 | return function 417 | 418 | 419 | def subscribe(event_type, event=None, callback=None): 420 | """ 421 | Accepts an event_type and event itself. 422 | Creates a new subscription, prints data on every event until 423 | KeyboardInterrupt is raised. 424 | """ 425 | if not callback: 426 | def callback(event, data, subscription): 427 | print('changed:', event['change']) 428 | if data: 429 | print('data:\n', data) 430 | 431 | socket = default_socket() 432 | subscription = Subscription(callback, event_type, event, data_socket=socket) 433 | try: 434 | while True: 435 | time.sleep(1) 436 | except KeyboardInterrupt: 437 | print('') # force newline 438 | finally: 439 | subscription.close() 440 | 441 | 442 | def get_socket_path(): 443 | """ 444 | Gets the socket path via i3 command. 445 | """ 446 | cmd = ['i3', '--get-socketpath'] 447 | output = __call_cmd__(cmd) 448 | return output 449 | 450 | 451 | def success(response): 452 | """ 453 | Convenience method for filtering success values of a response. 454 | Each success dictionary is replaces with boolean value. 455 | i3.MessageError is returned if error key is found in any of the 456 | success dictionaries. 457 | """ 458 | if isinstance(response, dict) and 'success' in response: 459 | if 'error' in response: 460 | return MessageError(response['error']) 461 | return response['success'] 462 | elif isinstance(response, list): 463 | for index, item in enumerate(response): 464 | item = success(item) 465 | if isinstance(item, i3Exception): 466 | return item 467 | response[index] = item 468 | return response 469 | 470 | 471 | def container(**criteria): 472 | """ 473 | Turns keyword arguments into a formatted container criteria. 474 | """ 475 | criteria = ['%s="%s"' % (key, val) for key, val in criteria.items()] 476 | return '[%s]' % ' '.join(criteria) 477 | 478 | 479 | def parent(con_id, tree=None): 480 | """ 481 | Searches for a parent of a node/container, given the container id. 482 | Returns None if no container with given id exists (or if the 483 | container is already a root node). 484 | """ 485 | def has_child(node): 486 | for child in node['nodes']: 487 | if child['id'] == con_id: 488 | return True 489 | return False 490 | parents = filter(tree, has_child) 491 | if not parents or len(parents) > 1: 492 | return None 493 | return parents[0] 494 | 495 | 496 | def filter(tree=None, function=None, **conditions): 497 | """ 498 | Filters a tree based on given conditions. For example, to get a list of 499 | unfocused windows (leaf nodes) in the current tree: 500 | i3.filter(nodes=[], focused=False) 501 | The return value is always a list of matched items, even if there's 502 | only one item that matches. 503 | The user function should take a single node. The function doesn't have 504 | to do any dict key or index checking (this is handled by i3.filter 505 | internally). 506 | """ 507 | if tree is None: 508 | tree = msg('get_tree') 509 | elif isinstance(tree, list): 510 | tree = {'list': tree} 511 | if function: 512 | try: 513 | if function(tree): 514 | return [tree] 515 | except (KeyError, IndexError): 516 | pass 517 | else: 518 | for key, value in conditions.items(): 519 | if key not in tree or tree[key] != value: 520 | break 521 | else: 522 | return [tree] 523 | matches = [] 524 | for nodes in ['nodes', 'floating_nodes', 'list']: 525 | if nodes in tree: 526 | for node in tree[nodes]: 527 | matches += filter(node, function, **conditions) 528 | return matches 529 | 530 | 531 | class i3(ModuleType): 532 | """ 533 | i3.py is a Python module for communicating with the i3 window manager. 534 | """ 535 | def __init__(self, module): 536 | self.__module__ = module 537 | self.__name__ = module.__name__ 538 | 539 | def __getattr__(self, name): 540 | """ 541 | Turns a nonexistent attribute into a function. 542 | Returns the resulting function. 543 | """ 544 | try: 545 | return getattr(self.__module__, name) 546 | except AttributeError: 547 | pass 548 | if name.lower() in self.__module__.MSG_TYPES: 549 | return self.__module__.__function__(type=name) 550 | else: 551 | return self.__module__.__function__(type='command', message=name) 552 | 553 | 554 | # Turn the module into an i3 object 555 | sys.modules[__name__] = i3(sys.modules[__name__]) 556 | --------------------------------------------------------------------------------