#!/bin/bash mkdir -p /tmp/tdrop print_help() { echo " Used for hiding/unhiding programs to acheive quake-dropdown-like-functionality. Can create a dropdown window if one does not already exist and provides options to control the intial size and position, e.g. to leave panels visible. When used with a terminal, provides the option to specify the name of a tmuxinator or tmux session to automatically start or attach to. Also supports the ability to auto-hide and auto-show. For example, this can be used to automatically hide a window when opening something, e.g. image viewer, video player, etc. from it, and then re-show the window whenever the image view, video player, etc. is closed. Takes a window as an argument or one of auto_show, auto_hide, or toggle_auto_hide. 'toggle_auto_hide' toggles whether calling 'auto_hide' or 'auto_show' will have any effect. 'current' will turn the current window into a dropdown. usage: tdrop [options] or one of options: -h height specify a height for a newly created term (default: 100%) -w width specify a width for a newly created term (default: 45%) -x pos specify x offset for a newly created term (default: 0) -y pos specify y offset for a newly created term (default: 1, see BUGS in man) -s name name for tmux or tmuxinator session (requires the program to be a supported terminal) -n num num or extra text; only needed if want multiple dropdowns of same program (default: "") -p cmd provide a pre-map command to float the window if necessary -P cmd provide a post-map command to float the window if necessary -M cmd provide a post-unmap command; can be used for example with a window manager that doesn't support floating to turn fullscreen on when mapping a terminal then off when unmapping it -O cmd provide a one time command only for when a dropdown is created/initiated (useful for 'tdrop current') -d XxY give decoration/border size to accurately save position; only applicable with auto_show; on applicable with a few window managers (e.g. blackbox) -f specify flags/options to be used when creating the term or window (e.g. -f '--title mytitle', default: none) -a automatically detect window manager and if settings exist for it, automatically set -p, -P, and -d values as necessary; this can have affect when used with a terminal or with auto_show or auto_hide (default: false) -m for use with multiple monitors and only with dropdowns (not for auto_show or auto_hide); convert percentages used for width or height to values relative to the size of the current monitor and force reszing of the dropdown when the monitor changes (default: false) --clear clear saved window id; useful accidentally make a window a dropdown (e.g. '$ tdrop --clear current') --no-cancel don't cancel auto-showing (default is to prevent this when manually toggling a term after it is auto-hidden) --help print help See man page for more details. " if [[ $1 == illegal_opt ]]; then exit 1 else exit 0 fi } # # Default Options and Option Parsing # # xdotool can take percentages; cannot take decimal percentages though width="100%" height="45%" xoff=0 yoff=2 session_name="" num="" map_pre="" map_post="" unmap_post="" oneshot_post="" dec_fix="" program_flags="" clearwid=false cancel_auto_show=true auto_detect_wm=false monitor_aware=false while getopts :h:w:x:y:s:n:p:P:M:O:d:f:-:am opt do case $opt in h) height=$OPTARG;; w) width=$OPTARG;; x) xoff=$OPTARG;; y) yoff=$OPTARG;; s) session_name=$OPTARG;; n) num=$OPTARG;; p) map_pre=$OPTARG;; P) map_post=$OPTARG;; M) unmap_post=$OPTARG;; O) oneshot_post=$OPTARG;; d) dec_fix=$OPTARG;; f) program_flags=$OPTARG;; a) auto_detect_wm=true;; m) monitor_aware=true;; -) if [[ $OPTARG =~ ^(auto-detect-wm|monitor-aware|clear|no-cancel|help)$ ]] || \ [[ $OPTARG == *=* ]]; then OPTION=${OPTARG%%=*} OPTARG=${OPTARG#*=} else echo "Long options with args must be written as --opt=val." | \ tee -a /tmp/tdrop/log exit 1 fi case $OPTION in height) height=$OPTARG;; width) width=$OPTARG;; x-offset) xoff=$OPTARG;; y-offset) yoff=$OPTARG;; session) session_name=$OPTARG;; number) num=$OPTARG;; pre-command) map_pre=$OPTARG;; post-command) map_post=$OPTARG;; post-unmap) unmap_post=$OPTARG;; oneshot-post) oneshot_post=$OPTARG;; decoration-fix) dec_fix=$OPTARG;; program-flags) program_flags=$OPTARG;; auto-detect-wm) auto_detect_wm=true;; monitor-aware) monitor_aware=true;; clear) clearwid=true;; no-cancel) cancel_auto_show=false;; help) print_help;; *) print_help illegal_opt;; esac;; *) print_help illegal_opt;; esac done program=${*:0-1} # ensure that positional argument exists if [[ -z $program ]] || [[ $program =~ ^- ]]; then echo "1 positional argument is required." \ "For help use -h or --help or see the manpage." | \ tee -a /tmp/tdrop/log exit 1 fi # require -m for negative width or height if ! $monitor_aware && [[ $height$width == *-* ]]; then echo "-m is required to use negative width or height values." | \ tee -a /tmp/tdrop/log exit 1 fi # validate options that require number values if [[ ! $height$width$xoff$yoff =~ ^[0-9%-]*$ ]]; then echo "The -h, -w, -x, and -y values must be numbers (or percentages)." | \ tee -a /tmp/tdrop/log exit 1 fi if [[ -n $dec_fix ]] && [[ ! $dec_fix =~ ^-?[0-9]+x-?[0-9]+$ ]]; then echo "The decoration fix value must have form 'num'x'num'." \ "The numbers can be negative or zero." | tee -a /tmp/tdrop/log exit 1 fi # non-user-settable global vars # necessary to account for differences in geometry/positioning output from xwininfo # for different WMs (matters when a window is in a corner or on the side) subtract_when_same=true # will be set to true to eliminate or reduce flicker for WMs that alter window # position when remapping; when true, windowmap and windowmove are combined, so that # the window is correctly positioned earlier combine_map_post=false # set to true if the -m option is being used and the monitor has changed # in order to force resize the dropdown to the given width/height percentages # for the current monitor monitor_changed=false # used for -m option; at first tdrop assumes that there is a focused window # on the current desktop; if there isn't (and the WM doesn't have some way to # query the current monitor), this will be set to false, and tdrop will have to # find out the current monitor info after opening the dropdown # (currently, using xwininfo to find the position of a window is the only # WM-independent way I know to find out what the current monitor is) focused_window_exists=true # # Multiple Monitor Automatic Re-Sizing # update_geometry_settings_for_monitor() { # 1. Correctly interpret width/height percentages when there exist multiple # monitors so an initially created dropdown is the correct size (xdotool # would create a dropdown the width of all screens for 100% width) # 2. Force resize the dropdown to the correct percentage of the current # monitor IF the monitor has changed since the last time the dropdown # was used # it is conceivable that a user may want to use -m but not -a, so # get the wm from with # in this function local wm current_monitor wm=$(get_window_manager) if [[ $wm == bspwm ]]; then current_monitor=$(bspc query -m -M) elif [[ $wm == i3 ]]; then # I'd rather not make jq a dependency current_monitor=$(i3-msg -t get_workspaces | sed 's/{"num"/\n/g' | \ awk -F ',' '/focused":true/ {sub(".*output",""); gsub("[:\"]",""); print $1}') fi local monitor_geo x_begin y_begin x_width y_height if [[ -n $current_monitor ]]; then monitor_geo=$(xrandr --query | \ awk "/$current_monitor/ {gsub(\"primary \",\"\"); print \$3}") x_begin=$(echo "$monitor_geo" | awk -F '+' '{print $2}') y_begin=$(echo "$monitor_geo" | awk -F '+' '{print $3}') x_width=$(echo "$monitor_geo" | awk -F 'x' '{print $1}') y_height=$(echo "$monitor_geo" | awk -F 'x|+' '{print $2}') else local wid wininfo window_x window_y monitors_info x_end y_end # determine current monitor in generic way wid=$(xdotool getactivewindow) if [[ -z $wid ]]; then # will try again after remapping or creating the dropdown return 1 fi wininfo=$(xwininfo -id "$wid") window_x=$(echo "$wininfo" | awk '/Absolute.*X/ {print $4}') window_y=$(echo "$wininfo" | awk '/Absolute.*Y/ {print $4}') monitors_info=$(xrandr --query | awk '/ connected/ {gsub("primary ",""); print}') while read -r monitor; do monitor_geo=$(echo "$monitor" | awk '{print $3}') x_begin=$(echo "$monitor_geo" | awk -F '+' '{print $2}') y_begin=$(echo "$monitor_geo" | awk -F '+' '{print $3}') x_width=$(echo "$monitor_geo" | awk -F 'x' '{print $1}') y_height=$(echo "$monitor_geo" | awk -F 'x|+' '{print $2}') x_end=$((x_begin+x_width)) y_end=$((y_begin+y_height)) if [[ $window_x -ge $x_begin ]] && [[ $window_x -lt $x_end ]] && \ [[ $window_y -ge $y_begin ]] && [[ $window_y -lt $y_end ]]; then current_monitor=$(echo "$monitor" | awk '{print $1}') break fi done <<< "$monitors_info" fi # update x and y offsets, so that will appear on correct screen # (required for some WMs apparently, but not for others) ((xoff+=x_begin)) ((yoff+=y_begin)) # convert w/h percentages to pixels based on size of current screen if [[ $height =~ %$ ]]; then height=$(awk "BEGIN {printf(\"%.0f\", 0.01*${height%\%}*$y_height)}") elif [[ $height =~ ^- ]]; then minus_height=${height#-} height=$((y_height-minus_height)) fi if [[ $width =~ %$ ]]; then width=$(awk "BEGIN {printf(\"%.0f\", 0.01*${width%\%}*$x_width)}") elif [[ $width =~ ^- ]]; then minus_width=${width#-} width=$((x_width-minus_width)) fi # determine if monitor has changed and save current monitor local last_monitor last_monitor=$(< /tmp/tdrop/last_monitor) echo "$current_monitor" > /tmp/tdrop/last_monitor if [[ $current_monitor != "$last_monitor" ]]; then monitor_changed=true fi } set_geometry_for_monitor() { xdotool windowmove "$1" "$xoff" "$yoff" windowsize "$1" "$width" "$height" } # # WM Detection and Settings # get_window_manager() { # xfwm4 and fvwm at least will give two names (hence piping into head) xprop -notype -id "$(xprop -root -notype | \ awk '$1=="_NET_SUPPORTING_WM_CHECK:"{print $5}')" \ -f _NET_WM_NAME 8u | awk -F "\"" '/WM_NAME/ {print $2}' | head -n 1 } # settings that apply for both tdrop and tdrop auto_(hide|show) # e.g. tilers w/ floating support: settings for floating a window when mapping it # can set up a rule for always floating a window or term type/class # alternatively, can use these settings to float just the dropdown wm_autoset_for_all() { # bspwm will use previous size when floating already if [[ $wm == bspwm ]]; then map_pre() { bspc rule -a "$1" -o state=floating # maintain compatability with old syntax bspc rule -a "$1" -o floating=on } elif [[ $wm == awesome ]]; then # awesome remembers size, but need to float and then set size first map_post_oneshot() { echo 'local awful = require("awful") ; awful.client.floating.set(c, true)' | \ awesome-client && \ xdotool windowmove "$1" "$xoff" "$yoff" \ windowsize "$1" "$width" "$height" } map_post() { echo 'local awful = require("awful") ; awful.client.floating.set(c, true)' | \ awesome-client } # tilers that won't remember sizing elif [[ $wm == i3 ]]; then map_post_oneshot() { i3-msg "[id=$1] floating enable" > /dev/null && \ xdotool windowmove "$1" "$xoff" "$yoff" \ windowsize "$1" "$width" "$height" } map_post() { i3-msg "[id=$1] floating enable" > /dev/null && \ xdotool windowmove "$1" "$xoff" "$yoff" } fi } # settings for maintining previous size/placement of windows when unhiding them wm_autoset_for_hide_show() { # tilers: is_floating function so that hidden tiling windows don't become floating if [[ $wm == bspwm ]]; then subtract_when_same=false is_floating() { # checking if the window id (converted from decimal to hex) is floating; empty if not floating bspc query -T | grep -i "$(printf 0x%x "$1").*f-" || \ bspc query -T -w | grep '"state": "floating"' } elif [[ $wm == i3 ]]; then subtract_when_same=false is_floating() { # do you even sed? i3-msg -t get_tree | awk 'gsub(/{"id"/, "\n{\"id\"")' | \ awk '/focused":true.*floating":"user_on/ {print $1}' } # settings for stacking/floating wms where can't get right position easily from xwininfo # take borders into account elif [[ $wm == Blackbox ]]; then dec_fix_auto="1x22" elif [[ $wm =~ ^(Mutter|GNOME Shell)$ ]]; then dec_fix_auto="-10x-8" subtract_when_same=false elif [[ $wm =~ ^(Mutter \(Muffin\))$ ]]; then dec_fix_auto="-9x-8" # NOTE: # pekwm, xfwm4, sawfish, openbox need subtract_when_same to be true (default) # for fluxbox, blackbox, fvwm, and metacity, the value does not matter elif [[ $wm == GoomwW ]]; then subtract_when_same=false fi if [[ $wm =~ ^(pekwm|Fluxbox|Blackbox|xfwm4|Metacity|FVWM|Sawfish|GoomwW|Mutter|GNOME Shell|Mutter \(Muffin\)|KWin|Metacity \(Marco\))$ ]]; then combine_map_post=true map_post() { xdotool windowmap "$1" windowmove "$1" "$2" "$3" } fi } wm_autoset_for_dropdown() { # tilers without floating support: can toggle fullscreen instead for the dropdown if [[ $wm == herbstluftwm ]]; then map_post() { herbstclient fullscreen on } unmap_post() { herbstclient fullscreen off } # floating window managers that may both move and resize a window after unmapping then mapping it elif [[ $wm == Openbox ]]; then # openbox will resize window to be slightly less than the width of the screen when mapping # this is necessary when want width to be 100% combine_map_post=true map_post() { xdotool windowmap "$1" windowmove "$1" "$xoff" "$yoff" \ windowsize "$1" "$width" "$height" } # floating window managers that may move a window after unmapping then mapping it elif [[ $wm =~ ^(pekwm|Fluxbox|Blackbox|xfwm4|Metacity|FVWM|Sawfish|GoomwW|Mutter|GNOME Shell|Mutter \(Muffin\)|KWin|Metacity \(Marco\))$ ]]; then # most will center # mutter/gnome shell will move to top centerish (just below panel on gnome shell) # xfwm4 will normally move to top left; metacity will move close to top left # sawfish just moves all over the place combine_map_post=true map_post() { xdotool windowmap "$1" windowmove "$1" "$xoff" "$yoff" } fi } # # Helper Functions for Specific Dropdowns and Auto Hide/Show # get_class_name() { xprop -id "$1" WM_CLASS | awk '{gsub(/"/, ""); print $4}' } get_visibility() { xwininfo -id "$1" 2> /dev/null | awk '/Map State/ {print $3}' } map_pre_command() { # a user set option has higher priority if [[ -n $map_pre ]]; then eval "$map_pre" # use automatically set function if exists elif [[ -n $(type map_pre 2> /dev/null) ]]; then # needed when creating oneshot rules for programs where cmd differs from actual class name if [[ $1 == gnome-terminal ]]; then map_pre "Gnome-terminal" elif [[ $1 == urxvtc ]]; then map_pre "urxvt" else map_pre "$1" fi fi } map_post_command() { if [[ -n $oneshot_post ]] && [[ $1 == oneshot ]]; then eval "$oneshot_post" elif [[ -n $(type map_post_oneshot 2> /dev/null) ]] && \ [[ $1 == oneshot ]]; then map_post_oneshot "$2" elif [[ -n $map_post ]]; then eval "$map_post" elif [[ -n $(type map_post 2> /dev/null) ]]; then map_post "$@" fi } unmap_post_command() { if [[ -n $unmap_post ]]; then eval "$unmap_post" elif [[ -n $(type unmap_post 2> /dev/null) ]]; then unmap_post fi } maybe_cancel_auto_show() { if $cancel_auto_show && \ [[ $1 == $(cat /tmp/tdrop/auto_hidden/wid 2> /dev/null) ]]; then > /tmp/tdrop/auto_hidden/wid fi } # # Dropdown Initialization # create_win_return_wid() { local blacklist program_command pid visible_wid wids program_wid # blacklist all existing wids of program # (for programs where one pid shares all wids) blacklist=$(xdotool search --classname "$program") # need to redirect stdout or function won't return program_command="$1 > /dev/null &" # for programs where $! won't give the correct pid if [[ $program == terminix ]] && pgrep terminix; then pid=$(pgrep terminix) eval "$program_command" elif [[ $program == urxvtc ]]; then blacklist=$(xdotool search --classname urxvtd) pid=$(pgrep urxvtd) eval "$program_command" else eval "$program_command" pid=$! fi visible_wid=false while : ; do wids=$(xdotool search --pid "$pid") if [[ -n "$wids" ]]; then while read -r wid; do if [[ ! $blacklist =~ (^|$'\n')"$wid"($|$'\n') ]] && \ [[ $(get_visibility "$wid") == IsViewable ]]; then visible_wid=true program_wid=$wid fi done <<< "$wids" fi if $visible_wid; then break fi sleep 0.01 done echo -n "$program_wid" } program_start() { local program_command tmux_command wid program_command="$program $program_flags" if [[ -n "$session_name" ]]; then tmux_command="'tmux attach-session -dt $session_name || \ tmuxinator start $session_name || \ tmux new-session -s $session_name'" if [[ $program == urxvt ]]; then program_command="$program_command -e bash -c $tmux_command" elif [[ $program == terminix ]]; then program_command="$program_command -x \"bash -c $tmux_command\"" else program_command="$program_command -e \"bash -c $tmux_command\"" fi fi wid=$(create_win_return_wid "$program_command") echo "$wid" > /tmp/tdrop/"$program$num" # only will work if a pre-command has been run (e.g. bspwm) xdotool windowmove "$wid" "$xoff" "$yoff" \ windowsize "$wid" "$width" "$height" echo -n "$wid" } current_create() { # turns active window into a dropdown local wid wid=$(xdotool getactivewindow) echo "$wid" > /tmp/tdrop/current"$num" get_class_name "$wid" > /tmp/tdrop/current"$num"_type echo -n "$wid" } wid_toggle() { # get saved window id if already created local wid exists visibility wid=$(< /tmp/tdrop/"$program$num") exists=true if [[ -n $wid ]]; then visibility=$(get_visibility "$wid") # sometimes xwininfo will still report a window as existing hence xprop check if [[ -z $visibility ]] || [[ -z $(xprop -id "$wid") ]]; then # window no longer exists exists=false > /tmp/tdrop/"$program$num" fi else exists=false fi if $exists; then if [[ $visibility == IsUnMapped ]]; then if [[ $program == current ]]; then map_pre_command "$(< /tmp/tdrop/current"$num"_type)" else map_pre_command "$program" fi # update here if possible so this doesn't cause a delay # between the window being remapped and resized if $monitor_aware; then update_geometry_settings_for_monitor || \ focused_window_exists=false fi if ! $combine_map_post; then xdotool windowmap "$wid" map_post_command "$wid" else # combine mapping with resizing/moving when possible to reduce flicker map_post_command "$wid" fi maybe_cancel_auto_show "$wid" if ! $focused_window_exists; then # need to use dropdown as active window to get monitor info update_geometry_settings_for_monitor fi # would be much too messy to integrate a resize into existing map_post commands # or add for those WMs that don't even need an xdotool command # cleaner to do after any map_post command with the downside of potential # for minimal flicker or redundancy if $monitor_changed; then set_geometry_for_monitor "$wid" fi else xdotool windowunmap "$wid" fi else # necessary to deal with negative width or height # if creating on an empty desktop and can't determine the monitor, # must set temporary values for negative width and/or height local original_width original_height if $monitor_aware && ! update_geometry_settings_for_monitor; then focused_window_exists=false if [[ $width =~ ^- ]]; then original_width=$width width=100% fi if [[ $height =~ ^- ]]; then original_height=$height height=100% fi fi # make it map_pre_command "$program" if [[ $program == current ]]; then wid=$(current_create) xdotool windowunmap "$wid" else wid=$(program_start) map_post_command oneshot "$wid" # update window dimensions if necessary if ! $focused_window_exists; then width=${original_width:-$width} height=${original_height:-$height} update_geometry_settings_for_monitor set_geometry_for_monitor "$wid" fi fi fi } # # Helper Functions for Auto Hiding/Showing # get_geometry() { # so that won't float a tiled window later when showing if [[ -n $(type is_floating 2> /dev/null) ]] && \ [[ -z $(is_floating "$1") ]]; then # window is not floating; don't bother saving geometry echo -n "false" else local wininfo x y rel_x rel_y wininfo=$(xwininfo -id "$1") x=$(echo "$wininfo" | awk '/Absolute.*X/ {print $4}') y=$(echo "$wininfo" | awk '/Absolute.*Y/ {print $4}') rel_x=$(echo "$wininfo" | awk '/Relative.*X/ {print $4}') rel_y=$(echo "$wininfo" | awk '/Relative.*Y/ {print $4}') if $subtract_when_same; then # behaviour works for most WMs (at least floating ones) x=$((x-rel_x)) y=$((y-rel_y)) else # don't subtract when abs and rel values are the same # necessary for WMs like bspwm and i3 if [[ $x -ne $rel_x ]]; then x=$((x-rel_x)) fi if [[ $y -ne $rel_y ]]; then y=$((y-rel_y)) fi fi echo -n -e "X=$x\nY=$y" fi } give_geometry() { eval "$(< /tmp/tdrop/auto_hidden/geometry)" local x_fix y_fix X Y if [[ -n $dec_fix ]]; then x_fix=$(echo "$dec_fix" | awk -F "x" '{print $1}') y_fix=$(echo "$dec_fix" | awk -F "x" '{print $2}') X=$((X-x_fix)) Y=$((Y-y_fix)) elif [[ -n $dec_fix_auto ]]; then x_fix=$(echo "$dec_fix_auto" | awk -F "x" '{print $1}') y_fix=$(echo "$dec_fix_auto" | awk -F "x" '{print $2}') X=$((X-x_fix)) Y=$((Y-y_fix)) fi echo -n "$X $Y" } set_geometry() { xdotool windowmove "$1" "$(give_geometry)" } toggle_auto_hide() { local no_hide no_hide=$(< /tmp/tdrop/auto_hidden/no_hide) mkdir -p /tmp/tdrop/auto_hidden if [[ -z $no_hide ]]; then echo "true" > /tmp/tdrop/auto_hidden/no_hide else > /tmp/tdrop/auto_hidden/no_hide fi } # # Auto Hiding/Showing # auto_hide() { local no_hide no_hide=$(< /tmp/tdrop/auto_hidden/no_hide) if [[ -z $no_hide ]]; then local wid wid=$(xdotool getactivewindow) mkdir -p /tmp/tdrop/auto_hidden echo "$wid" > /tmp/tdrop/auto_hidden/wid get_class_name "$wid" > /tmp/tdrop/auto_hidden/class get_geometry "$wid" > /tmp/tdrop/auto_hidden/geometry xdotool windowunmap "$wid" fi } auto_show() { local no_hide no_hide=$(< /tmp/tdrop/auto_hidden/no_hide) if [[ -z $no_hide ]]; then local wid class was_floating wid=$(< /tmp/tdrop/auto_hidden/wid) class=$(< /tmp/tdrop/auto_hidden/class) was_floating=$(< /tmp/tdrop/auto_hidden/geometry) if [[ $was_floating != false ]]; then map_pre_command "$class" fi if ! $combine_map_post; then xdotool windowmap "$wid" fi if [[ $was_floating != false ]]; then if $combine_map_post; then local XY X Y XY=$(give_geometry "$wid") X=$(echo "$XY" | awk '{print $1}') Y=$(echo "$XY" | awk '{print $2}') map_post_command "$wid" "$X" "$Y" else map_post_command "$wid" set_geometry "$wid" fi else xdotool windowmap "$wid" fi fi } # # Main # if [[ -n $1 ]]; then if $auto_detect_wm; then wm=$(get_window_manager) wm_autoset_for_all if [[ $program == auto_show ]] || [[ $program == auto_hide ]]; then wm_autoset_for_hide_show else wm_autoset_for_dropdown fi fi if $clearwid; then > /tmp/tdrop/"$program$num" elif [[ $program == toggle_auto_hide ]]; then toggle_auto_hide elif [[ $program == auto_hide ]]; then auto_hide elif [[ $program == auto_show ]]; then auto_show else wid_toggle fi else print_help illegal_opt fi # vim is dumb # vim: set ft=sh: