├── LICENSE ├── README.md ├── wf-msg ├── _bash_args_parser.bash ├── _help.bash ├── _internal.bash ├── _lib.bash ├── actions.bash ├── common.bash ├── queries.bash ├── subscriptions.bash └── wf-msg └── wf-utils ├── _internal.bash ├── common.bash ├── move_titled_window_to_workspace.bash ├── peek_titled_window.bash └── wf-utils /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Damian Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `wf-msg` and `wf-utils` for [Wayfire](https://github.com/WayfireWM/wayfire) 2 | This allows you to have very fine-grained control of Wayfire from the CLI. At its most basic it can manipulate windows eg; move, minimize, resize and close them. However it can also query current properties, or subscribe to changes, about; windows, workspaces and outputs. Together these tools can be used to create some powerful functionality. 3 | 4 | ### `wf-msg` 5 | Inspired by SwayWM's [swaymsg](https://github.com/swaywm/sway/blob/master/swaymsg/swaymsg.1.scd). Talks to the D-Bus plugin. 6 | 7 | ``` 8 | Usage: wf-msg [SUBCOMMAND] [--help] 9 | 10 | Control Wayfire from the CLI 11 | 12 | Subcommands: 13 | focus_window Focus the given window 14 | get_all_window_ids Get the IDs of all the current windows 15 | get_all_windows Get the full details of all current windows 16 | get_current_output Get the ID of the current output (usually a physical monitor) 17 | get_current_workspace Get the X,Y coords of the current workspace on the current output 18 | get_window_app Get the application that launched the given window 19 | get_window_title Get the title of the given window 20 | get_window_workspace Get the X,Y coords of the given window 21 | is_window_active Is the given window ID active? (I think that means focussed) 22 | maximize_window Maximize the given window 23 | minimize_window Minimize the given window 24 | move_window_to_workspace Move the given window to the given workspace 25 | unfocus_window Return window to focus state before previous focus 26 | unmaximize_window Reize window to size before previous maximization 27 | unminimize_window Resize window to size before previous minimization 28 | wf-call Make a call to Wayfire 29 | wf-dbus-introspect Returns XML of all the available Wayfire D-Bus methods and signals 30 | 31 | Options: 32 | --help Show this help 33 | ``` 34 | 35 | ### `wf-utils` 36 | ``` 37 | Usage: wf-utils [SUBCOMMAND] [--help] 38 | 39 | Collection of general utilities for controling Wayfire 40 | 41 | Subcommands: 42 | find_titled_window Find a window by searching for its name and title. Returns window ID 43 | minimize_window_on_unfocus Minimise a window once it becomes unfoccused 44 | move_titled_window_to_workspace Find a window by its app and title then move it to a workspace 45 | move_window_to_current_workspace Move window to current workspace 46 | peek_titled_window Find a window and bring it to the current workspace 47 | wait_for_window_creation Wait for a window with the given title to come into existence 48 | wait_for_window_title_change Wait until a window changes its title to that provided 49 | 50 | Options: 51 | --help Show this help 52 | ``` 53 | 54 | I'd hope that `wf-msg` and `wf-utils` could find a home in either the [D-Bus plugin](https://github.com/damianatorrpm/wayfire-plugin_dbus_interface) repo or even Wayfire's own [wayfire-plugins-extra](https://github.com/WayfireWM/wayfire-plugins-extra) repo 55 | 56 | ### Examples 57 | 58 | Hotkey for specfic window: 59 | ```ini 60 | # Unconditionally focus a Firefox window with a translation tab. 61 | # It doesn't create the Firefox window, just focusses a pre-existing one. 62 | binding_toggle_translator = KEY_T 63 | command_toggle_translator = wf-utils peek_titled_window firefox 'DeepL Translate' 64 | ``` 65 | 66 | Autostart windows to specific layout: 67 | ```sh 68 | # When a browser window contains multiple tabs you need an extension to insert a consistent, 69 | # searchable string token in the title in order for `wf-utils` to find it. For Firefox there is: 70 | # https://addons.mozilla.org/en-US/firefox/addon/window-titler 71 | # The `--wait-for-title` flag is needed because it can take a few moments for Firefox to update 72 | # its titles. 73 | wf-utils move_titled_window_to_workspace firefox 'Projects' 0 0 --wait-for-title --timeout 60 & 74 | wf-utils move_titled_window_to_workspace firefox 'Email etc' 0 1 --wait-for-title --timeout 60 & 75 | wf-utils move_titled_window_to_workspace firefox 'System' 0 2 --wait-for-title --timeout 60 & 76 | wf-utils move_titled_window_to_workspace firefox 'Entertainment' 2 1 --wait-for-title --timeout 60 & 77 | wf-utils move_titled_window_to_workspace firefox 'Remote' 2 2 --wait-for-title --timeout 60 & 78 | firefox & 79 | disown 80 | firefox --private-window https://www.deepl.com/translator & 81 | disown 82 | ``` 83 | 84 | 85 | 86 | 87 | ## Requirements 88 | * @soppelmann's fork (it fixes damianatorrpm#46) of the [Wayfire D-Bus plugin](https://github.com/soppelmann/wayfire-plugin_dbus_interface) 89 | * [wayfire-plugins-extra](https://github.com/WayfireWM/wayfire-plugins-extra) 90 | * `wf-utils` requires [`jq`](https://stedolan.github.io/jq/download) 91 | 92 | 93 | ## Install 94 | From project root: 95 | * `ln -s $(pwd)/wf-msg/wf-msg /usr/local/bin/wf-msg` 96 | * `ln -s $(pwd)/wf-utils/wf-utils /usr/local/bin/wf-utils` 97 | 98 | -------------------------------------------------------------------------------- /wf-msg/_bash_args_parser.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # Bash Arguments Parser by tombh (BAPt) 4 | # 5 | # Usage: 6 | # ``` 7 | # declare -A args=( 8 | # [summary]="A short description of what this command or function does" 9 | # [0:foo]="A foo argument, it's positional and required" 10 | # [--bar:flag]="A boolean option that doesn't take a value" 11 | # [--foobar]="An option that requires a value" 12 | # ) 13 | # BAPt_parse_arguments args "$@" 14 | # 15 | # echo "foo is ${args[foo]}. bar is ${args[foo]}. foobar is ${args[foobar]}" 16 | # ``` 17 | 18 | __BAPt_ERROR_PREFIX='Error parsing arguments' 19 | __BAPt_SCRIPT_NAME=$(basename "$0") 20 | 21 | function BAPt_parse_arguments { 22 | local -n arg_defs="$1" 23 | shift 24 | local parent_args=("$@") 25 | local usage option_definitions parsed parts positionals options 26 | arg_defs+=([--help:flag]="Show help") 27 | 28 | # Why build $usage now rather than as and when it's needed? 29 | # $arg_defs, which is needded to build the usage string, is an associative array 30 | # which are hard to copy. So it's easier to maintain a copy of the prebuilt usage 31 | # rather than a copy of $arg_defs. 32 | usage=$(__BAPt_build_usage arg_defs) 33 | 34 | if [[ ${parent_args[*]} = "--help" ]]; then 35 | __BAPt_show_usage 0 36 | fi 37 | 38 | if [[ -n ${arg_defs[any]} ]]; then 39 | if [[ -z ${parent_args[*]} ]]; then 40 | echo "$__BAPt_ERROR_PREFIX: arguments expected" 1>&2 41 | echo 1>&2 42 | __BAPt_show_usage 1 43 | fi 44 | return 0 45 | fi 46 | 47 | __BAPt_parse 48 | } 49 | 50 | function __BAPt_parse { 51 | option_definitions=$(__BAPt_get_option_definitions) 52 | if ! parsed=$( 53 | getopt \ 54 | -n "$__BAPt_ERROR_PREFIX" \ 55 | --longoptions "$option_definitions" \ 56 | -- _ "${parent_args[@]}" 57 | ); then 58 | echo 59 | __BAPt_show_usage 1 60 | fi 61 | 62 | parts="$(echo "$parsed" | sed 's/ -- /\n/' | sed 's/ --$/\n/')" 63 | parts1=$(echo "$parts" | sed -n 1p) 64 | parts2=$(echo "$parts" | sed -n 2p) 65 | [[ $parts2 = "''" ]] && parts2="" 66 | 67 | if ! __BAPt_parse_positional_args "$parts2"; then 68 | __BAPt_show_usage 1 69 | fi 70 | 71 | if ! __BAPt_parse_options "$parts1"; then 72 | __BAPt_show_usage 1 73 | fi 74 | } 75 | 76 | function __BAPt_show_usage { 77 | local exit_code=$1 78 | if [[ $exit_code -gt 0 ]]; then 79 | echo "$usage" >&2 80 | exit "$exit_code" 81 | else 82 | echo "$usage" 83 | exit 0 84 | fi 85 | } 86 | 87 | function __BAPt_build_usage { 88 | local calling_function=${FUNCNAME[2]} 89 | local widest usage command_name positionals=() options=() description line arg_list=() 90 | 91 | widest=$(__BAPt_find_widest) 92 | 93 | __BAPt_extract_positionals_and_options 94 | 95 | if [[ -n $calling_function ]]; then 96 | command_name=$(basename "$calling_function") 97 | else 98 | command_name=$(basename "$__BAPt_SCRIPT_NAME") 99 | fi 100 | 101 | echo "Usage: $command_name ${arg_list[*]} [OPTIONS]" 102 | 103 | if [[ -n ${arg_defs[summary]} ]]; then 104 | echo 105 | echo "${arg_defs[summary]}" 106 | fi 107 | 108 | if [[ ${#positionals} -gt 0 ]]; then 109 | echo 110 | echo "Arguments:" 111 | for line in "${positionals[@]}"; do 112 | echo "$line" 113 | done 114 | fi 115 | 116 | if [[ ${#options} -gt 0 ]]; then 117 | echo 118 | echo "Options:" 119 | for line in "${options[@]}"; do 120 | echo "$line" 121 | done 122 | fi 123 | 124 | if [[ -n ${arg_defs[details]} ]]; then 125 | echo 126 | echo "${arg_defs[details]}" 127 | fi 128 | } 129 | 130 | function __BAPt_extract_positionals_and_options { 131 | for key in "${!arg_defs[@]}"; do 132 | description="${arg_defs[$key]}" 133 | if [[ $key =~ ^[0-9]: ]]; then 134 | name=${key/*:/} 135 | index=${key//:*/} 136 | else 137 | name=${key//:flag/} 138 | fi 139 | line=$(printf "%-${widest}s %s\n" " $name" "$description") 140 | if [[ $key =~ ^[0-9]: ]]; then 141 | arg_list[$index]="$name" 142 | positionals[$index]=$line 143 | fi 144 | if [[ $key = any ]]; then 145 | arg_list[0]="[ARGUMENTS]" 146 | positionals[0]=$line 147 | fi 148 | if [[ $key =~ ^-- ]]; then 149 | options+=("$line") 150 | fi 151 | done 152 | } 153 | 154 | function __BAPt_find_widest { 155 | local widest=0 156 | for key in "${!arg_defs[@]}"; do 157 | key=${key//:flag/} 158 | width=${#key} 159 | if [[ $width -gt $widest ]]; then 160 | widest=$width 161 | fi 162 | done 163 | echo "$((widest + 2))" 164 | } 165 | 166 | # Convert an associative array into a `getopt`-compatible options definition 167 | # Eg, from: 168 | # ([--foo]="bar" [--boolme]="") 169 | # to: 170 | # "foo:,boolme" 171 | function __BAPt_get_option_definitions { 172 | local value option_defs_string option_defs_array=() 173 | 174 | for key in "${!arg_defs[@]}"; do 175 | if [[ ! $key =~ ^-- ]]; then 176 | continue 177 | fi 178 | value="${arg_defs[$key]}" 179 | key="${key//--/}" 180 | if [[ ! $key =~ ":flag" ]]; then 181 | key="$key:" 182 | else 183 | key="${key//:flag/}" 184 | fi 185 | option_defs_array+=("$key") 186 | done 187 | option_defs_string=$(__BAPt_join_by ',' "${option_defs_array[@]}") 188 | 189 | echo "$option_defs_string" 190 | } 191 | 192 | function __BAPt_parse_positional_args { 193 | local parsed=$1 194 | declare -a "positionals=($parsed)" 195 | 196 | local index name arity=0 197 | for key in "${!arg_defs[@]}"; do 198 | if [[ $key =~ ^[0-9]: ]]; then 199 | index=${key//:*/} 200 | name=${key/*:/} 201 | arg_defs["$name"]="${positionals[index]}" 202 | unset 'arg_defs['"$key"']' 203 | arity=$((arity + 1)) 204 | fi 205 | done 206 | 207 | if [[ ${#positionals[@]} -ne $arity ]]; then 208 | echo "$__BAPt_ERROR_PREFIX: Expected $arity got ${#positionals[@]}" >&2 209 | echo >&2 210 | return 1 211 | fi 212 | } 213 | 214 | function __BAPt_parse_options { 215 | local parsed=$1 216 | local value name 217 | declare -a "options=($parsed)" 218 | 219 | local index=0 220 | for item in "${options[@]}"; do 221 | if [[ $item =~ ^-- ]]; then 222 | name=${item/*--/} 223 | if [[ -n ${arg_defs[$item]} ]]; then 224 | value=${options[(($index + 1))]} 225 | else 226 | value=true 227 | fi 228 | arg_defs["$name"]="$value" 229 | unset 'arg_defs['"$item"']' 230 | fi 231 | index=$((index + 1)) 232 | done 233 | } 234 | 235 | function __BAPt_join_by { 236 | local delimeter=${1-} field=${2-} 237 | if shift 2; then 238 | printf %s "$field" "${@/#/$delimeter}" 239 | fi 240 | } 241 | -------------------------------------------------------------------------------- /wf-msg/_help.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | function _help { 4 | local _ 5 | declare -A _=( 6 | [summary]="Show this help text" 7 | ) 8 | BAPt_parse_arguments _ "$@" 9 | 10 | local sub_commands widest 11 | readarray -t sub_commands <<<"$(_get_all_function_names | _exclude_private_functions)" 12 | widest=$(_get_widest "${sub_commands[@]}") 13 | 14 | _echo_usage_title 15 | _echo_subcommands 16 | _echo_options 17 | } 18 | 19 | function _echo_usage_title { 20 | # shellcheck disable=2154 21 | echo "Usage: $__BAPt_SCRIPT_NAME [SUBCOMMAND] [--help]" 22 | echo 23 | echo "$_PROGRAM_DESCRIPTION" 24 | echo 25 | } 26 | 27 | function _echo_subcommands { 28 | echo "Subcommands:" 29 | for cmd in "${sub_commands[@]}"; do 30 | body=$(_get_contents_of_function "$cmd") 31 | summary=$(_summary "$body") 32 | line=$(printf "%-${widest}s %s\n" " $cmd" "$summary") 33 | echo "$line" 34 | done 35 | } 36 | 37 | function _echo_options { 38 | echo 39 | echo "Options:" 40 | printf "%-${widest}s %s\n" " --help" "Show this help" 41 | } 42 | 43 | function _get_all_function_names { 44 | compgen -A function 45 | } 46 | 47 | function _exclude_private_functions { 48 | grep -v -e '^_' -e '^BAPt' 49 | } 50 | 51 | function _get_contents_of_function { 52 | local fn=$1 53 | declare -f "$fn" 54 | } 55 | 56 | function _summary { 57 | local body=$1 58 | local summary 59 | local regex="s/.*\[summary\]=[\"']\([^\"']*[^\"']\)[\"'].*/\1/p" 60 | summary=$( 61 | echo "$body" | grep summary | sed -n "$regex" 62 | ) 63 | echo "$summary" 64 | } 65 | 66 | function _get_widest { 67 | local items=("$@") 68 | local widest=0 69 | for item in "${items[@]}"; do 70 | width=${#item} 71 | if [[ $width -gt $widest ]]; then 72 | widest=$width 73 | fi 74 | done 75 | echo "$((widest + 2))" 76 | } 77 | -------------------------------------------------------------------------------- /wf-msg/_internal.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | _LOG_IDENTIFIER="WF-MSG " 4 | _PROGRAM_DESCRIPTION="Control Wayfire from the CLI" 5 | __WF_MSG_WINDOW_ID_DESCRIPTION="Window ID. See something like 'get_all_window_ids'" 6 | 7 | function _extract_numbers { 8 | sed \ 9 | -e 's/uint32//g' \ 10 | -e 's/[^0-9]/ /g' 11 | } 12 | 13 | function _clean { 14 | sed \ 15 | -e "s/^()$//g" \ 16 | -e "s/^(['\"]//g" \ 17 | -e "s/['\"],)$//g" \ 18 | -e "s/^(//g" \ 19 | -e "s/,)$//g" \ 20 | -e "s/)$//g" \ 21 | -e 's/uint32 //g' 22 | } 23 | 24 | function _escape_double_quotes { 25 | local value=$1 26 | echo -n "${value//\"/\\\"}" 27 | } 28 | 29 | function _escape_forward_slashes { 30 | local value=$1 31 | echo -n "$value" | sed 's/\//\\\//g' 32 | } 33 | 34 | function _o_numbers { 35 | _extract_numbers | _trim 36 | } 37 | 38 | function _o_string { 39 | _clean | _trim 40 | } 41 | 42 | function _o_boolean { 43 | _clean | _trim 44 | } 45 | 46 | function _o_none { 47 | _clean | _trim 48 | } 49 | 50 | function _json_rm_trailing_comma { 51 | sed 's/,$//' 52 | } 53 | 54 | function _json_kv { 55 | local key="$1" 56 | local value="$2" 57 | 58 | value=$(_escape_double_quotes "$value") 59 | echo -n '"'"$key"'":"'"$value"'",' 60 | } 61 | 62 | function _json_object { 63 | local kvs="$1" 64 | echo -n "$kvs" | _json_rm_trailing_comma | sed 's/.*/{&},/' 65 | } 66 | 67 | function _json_array { 68 | local objects="$1" 69 | echo -n "$objects" | _json_rm_trailing_comma | sed 's/.*/[&],/' 70 | } 71 | 72 | function _cache_path { 73 | local name=$1 74 | [[ -z $name ]] && _error "No cache name provided" 75 | echo "$(_ensure_cache_setup)/$name" 76 | } 77 | 78 | function _get_cache { 79 | local name=$1 80 | local path 81 | path="$(_cache_path "$name")" 82 | if [[ -f "$path" ]]; then 83 | _debug "Returning cache for $name" 84 | cat "$path" 85 | else 86 | return 1 87 | fi 88 | } 89 | 90 | function _create_cache { 91 | local name=$1 92 | local contents=$2 93 | local path 94 | 95 | _debug "Creating cache for $name" 96 | path="$(_cache_path "$name")" 97 | echo "$contents" >"$path" 98 | } 99 | 100 | function _ensure_cache_setup { 101 | local path="/tmp/wf-msg" 102 | mkdir -p "$path" 103 | echo "$path" 104 | } 105 | -------------------------------------------------------------------------------- /wf-msg/_lib.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | function _includes_path { 4 | dirname "$(readlink -f "$0")" 5 | } 6 | 7 | function _load_includes { 8 | for file in "$(_includes_path)"/*.bash; do 9 | # shellcheck disable=1090 10 | source "$file" 11 | done 12 | } 13 | 14 | function _is_function { 15 | local suspect=$1 cleaned 16 | # shellcheck disable=2001 17 | cleaned=$(echo "$suspect" | sed 's/^-*//') 18 | if [[ $(type -t "$cleaned") != function ]]; then 19 | return 1 20 | fi 21 | } 22 | 23 | function _init { 24 | local subcommand=$1 start_time elapsed 25 | shift 26 | 27 | local args=("$@") 28 | 29 | _load_includes 30 | 31 | if [[ "$subcommand" = "--help" ]]; then 32 | _help 33 | exit 0 34 | fi 35 | 36 | if ! _is_function "$subcommand"; then 37 | # shellcheck disable=2154 38 | echo "$__BAPt_ERROR_PREFIX: Unknown subcommand" 2>&1 39 | echo 2>&1 40 | _help 41 | exit 1 42 | fi 43 | 44 | start_time=$(date +%s%N) 45 | "$subcommand" "${args[@]}" 46 | elapsed=$((($(date +%s%N) - start_time) / 1000000)) 47 | _debug "\`$subcommand ${args[*]}\` took ${elapsed}ms" 48 | } 49 | 50 | function _timestamp { 51 | date +"%H:%M:%S.%3N" 52 | } 53 | 54 | function _error { 55 | local message="$1" 56 | local caller=${FUNCNAME[1]} 57 | local prefix="" 58 | 59 | if [[ $DEBUG = 1 ]]; then 60 | prefix="$(_timestamp) $_LOG_IDENTIFIER |ERROR: $caller() " 61 | fi 62 | # shellcheck disable=2001 63 | echo "$message" | sed "s/.*/$prefix&/" 1>&2 64 | kill -s TERM "$_TOP_PID" 65 | exit 1 66 | } 67 | 68 | function _debug { 69 | [[ "$DEBUG" != "1" ]] && return 0 70 | local message="$1" 71 | local caller=${FUNCNAME[1]} 72 | local prefix 73 | prefix="$(_timestamp) $_LOG_IDENTIFIER |DEBUG: $caller()" 74 | # shellcheck disable=2001 75 | echo "$message" | sed "s/.*/$prefix &/" 1>&2 76 | } 77 | 78 | function _join_by { 79 | local delimeter=${1-} field=${2-} 80 | if shift 2; then 81 | printf %s "$field" "${@/#/$delimeter}" 82 | fi 83 | } 84 | 85 | function _trim { 86 | sed -e 's/\n//g' | awk '{$1=$1};1' 87 | } 88 | 89 | function _is_empty_whitespace { 90 | [[ -z "${1// /}" ]] 91 | } 92 | -------------------------------------------------------------------------------- /wf-msg/actions.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=2120 2 | 3 | function maximize_window { 4 | local args 5 | declare -A args=( 6 | [summary]="Maximize the given window" 7 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 8 | ) 9 | BAPt_parse_arguments args "$@" 10 | dbus-method maximize_view "${args[window_id]}" 1 | _o_none 11 | } 12 | 13 | function unmaximize_window { 14 | local args 15 | declare -A args=( 16 | [summary]="Reize window to size before previous maximization" 17 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 18 | ) 19 | BAPt_parse_arguments args "$@" 20 | dbus-method maximize_view "${args[window_id]}" 0 | _o_none 21 | } 22 | 23 | function minimize_window { 24 | local args 25 | declare -A args=( 26 | [summary]="Minimize the given window" 27 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 28 | ) 29 | BAPt_parse_arguments args "$@" 30 | dbus-method minimize_view "${args[window_id]}" 1 | _o_none 31 | } 32 | 33 | function unminimize_window { 34 | local args 35 | declare -A args=( 36 | [summary]="Resize window to size before previous minimization" 37 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 38 | ) 39 | BAPt_parse_arguments args "$@" 40 | dbus-method minimize_view "${args[window_id]}" 0 | _o_none 41 | } 42 | 43 | function focus_window { 44 | local args 45 | declare -A args=( 46 | [summary]="Focus the given window" 47 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 48 | ) 49 | BAPt_parse_arguments args "$@" 50 | dbus-method focus_view "${args[window_id]}" 1 | _o_none 51 | } 52 | 53 | function unfocus_window { 54 | local args 55 | declare -A args=( 56 | [summary]="Return window to focus state before previous focus" 57 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 58 | ) 59 | BAPt_parse_arguments args "$@" 60 | dbus-method focus_view "${args[window_id]}" 0 | _o_none 61 | } 62 | 63 | function move_window_to_workspace { 64 | local args 65 | declare -A args=( 66 | [summary]="Move the given window to the given workspace" 67 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 68 | [1:x]="x-cord of destination workspace" 69 | [2:y]="y-cord of destination workspace" 70 | ) 71 | BAPt_parse_arguments args "$@" 72 | dbus-method change_workspace_view "${args[window_id]}" "${args[x]}" "${args[y]}" | _o_none 73 | } 74 | -------------------------------------------------------------------------------- /wf-msg/common.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=2120 2 | 3 | function dbus-method { 4 | local _ 5 | declare -A _=( 6 | [summary]="Make a call to Wayfire's D-Bus interface" 7 | [details]="$( 8 | cat <<-EOM 9 | To see all the available commands and arguments, use: 10 | 11 | \`wf-msg dbus-introspect\` 12 | EOM 13 | )" 14 | [any]="Arguments to Wayfire D-Bus interface" 15 | ) 16 | BAPt_parse_arguments _ "$@" 17 | 18 | local method=$1 19 | shift 20 | local _args=("$@") args_with_commas result 21 | 22 | args_with_commas=$(_join_by , "${_args[@]}") 23 | _debug "Calling DBUS $method($args_with_commas)" 24 | 25 | dbus_args=( 26 | call 27 | --session 28 | --dest org.wayland.compositor 29 | --object-path /org/wayland/compositor 30 | --method org.wayland.compositor."$method" 31 | ) 32 | 33 | [ ${#_args[@]} -gt 0 ] && dbus_args+=("${_args[@]}") 34 | 35 | if ! result=$(gdbus "${dbus_args[@]}" 2>&1); then 36 | _error "D-Bus error ($method): $result" 37 | else 38 | echo "$result" 39 | fi 40 | } 41 | 42 | function dbus-signal { 43 | local args 44 | declare -A args=( 45 | [summary]="Monitor Wayfire's D-Bus interface for specific activity" 46 | [details]="$( 47 | cat <<-EOM 48 | To see all the available signals, use: 49 | 50 | \`wf-msg dbus-introspect\` 51 | EOM 52 | )" 53 | [0:signal]="Name of signal to monitor" 54 | [--timeout]="How long to wait until quitting, default 15 seconds" 55 | ) 56 | BAPt_parse_arguments args "$@" 57 | 58 | local prefix timeout="${args[timeout]:-15}" 59 | prefix=$( 60 | _escape_forward_slashes \ 61 | "/org/wayland/compositor: org.wayland.compositor.${args[signal]} " 62 | ) 63 | 64 | dbus_cmd=( 65 | gdbus 66 | monitor 67 | --session 68 | --object-path /org/wayland/compositor 69 | --dest org.wayland.compositor 70 | ) 71 | 72 | _debug "Monitoring D-Bus for ${args[signal]}" 73 | while read -r line; do 74 | echo "$line" | 75 | grep "${args[signal]}" | 76 | sed "s/$prefix//" | 77 | _clean 78 | done < <(timeout "$timeout" "${dbus_cmd[@]}") 79 | } 80 | 81 | function dbus-introspect { 82 | local _ 83 | declare -A _=( 84 | [summary]="Returns XML of all the available Wayfire D-Bus methods and signals" 85 | ) 86 | BAPt_parse_arguments _ "$@" 87 | 88 | dbus-send \ 89 | --session \ 90 | --type=method_call \ 91 | --print-reply \ 92 | --dest=org.wayland.compositor \ 93 | /org/wayland/compositor \ 94 | org.freedesktop.DBus.Introspectable.Introspect 95 | } 96 | -------------------------------------------------------------------------------- /wf-msg/queries.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=2120 2 | 3 | function get_all_window_ids { 4 | local _ 5 | declare -A _=( 6 | [summary]="Get the IDs of all the current windows" 7 | ) 8 | BAPt_parse_arguments _ "$@" 9 | dbus-method query_view_vector_taskman_ids | _o_numbers 10 | } 11 | 12 | function get_window_title { 13 | local args 14 | declare -A args=( 15 | [summary]="Get the title of the given window" 16 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 17 | ) 18 | BAPt_parse_arguments args "$@" 19 | dbus-method query_view_title "${args[window_id]}" | _o_string 20 | } 21 | 22 | function get_window_app { 23 | local args 24 | declare -A args=( 25 | [summary]="Get the application that launched the given window" 26 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 27 | ) 28 | BAPt_parse_arguments args "$@" 29 | dbus-method query_view_app_id "${args[window_id]}" | _o_string 30 | } 31 | 32 | function get_current_output { 33 | local _ 34 | declare -A _=( 35 | [summary]="Get the ID of the current output (usually a physical monitor)" 36 | ) 37 | BAPt_parse_arguments _ "$@" 38 | dbus-method query_active_output | _o_numbers 39 | } 40 | 41 | function get_current_workspace { 42 | local _ 43 | declare -A _=( 44 | [summary]="Get the X,Y coords of the current workspace on the current output" 45 | ) 46 | BAPt_parse_arguments _ "$@" 47 | dbus-method query_output_workspace "$(get_current_output)" | _o_numbers 48 | } 49 | 50 | function get_window_workspace { 51 | local args 52 | declare -A args=( 53 | [summary]="Get the X,Y coords of the given window" 54 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 55 | ) 56 | BAPt_parse_arguments args "$@" 57 | dbus-method query_view_workspaces "${args[window_id]}" | _o_numbers 58 | } 59 | 60 | function is_window_active { 61 | local args 62 | declare -A args=( 63 | [summary]="Is the given window ID active? (I think that means focussed)" 64 | [0:window_id]="$__WF_MSG_WINDOW_ID_DESCRIPTION" 65 | ) 66 | BAPt_parse_arguments args "$@" 67 | dbus-method query_view_active "${args[window_id]}" | _o_boolean 68 | } 69 | 70 | function get_all_windows { 71 | local args json 72 | declare -A args=( 73 | [summary]="Get the full details of all current windows" 74 | [--use-cache:flag]="Use cached results, or create cache if it doesn't already exist" 75 | ) 76 | BAPt_parse_arguments args "$@" 77 | 78 | local cache_name="get_all_windows" 79 | if [[ -n ${args[use-cache]} ]]; then 80 | if json=$(_get_cache "$cache_name"); then 81 | echo "$json" 82 | return 0 83 | fi 84 | fi 85 | json=$(_json_all_windows) 86 | _create_cache "$cache_name" "$json" 87 | echo "$json" 88 | } 89 | 90 | function _json_all_windows { 91 | _json_array "$( 92 | read -ra wids <<<"$(get_all_window_ids)" 93 | for wid in "${wids[@]}"; do 94 | _json_object "$( 95 | _json_kv 'id' "$wid" 96 | _json_kv 'app' "$(get_window_app "$wid")" 97 | _json_kv 'title' "$(get_window_title "$wid")" 98 | )" 99 | done 100 | )" | _json_rm_trailing_comma 101 | } 102 | -------------------------------------------------------------------------------- /wf-msg/subscriptions.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=2120 2 | -------------------------------------------------------------------------------- /wf-msg/wf-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | trap "exit 1" TERM 4 | trap "kill 0" SIGINT 5 | export _TOP_PID=$$ 6 | _INCLUDES_PATH=$(dirname "$(readlink -f "$0")") 7 | source "$_INCLUDES_PATH/_lib.bash" 8 | 9 | _init "$@" 10 | -------------------------------------------------------------------------------- /wf-utils/_internal.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | export _LOG_IDENTIFIER="WF-UTILS" 4 | export _PROGRAM_DESCRIPTION="Collection of general utilities for controling Wayfire" 5 | export DEFAULT_TIMEOUT=15 6 | 7 | function _wf-msg { 8 | local args=("$@") 9 | 10 | while read -r line; do 11 | if ! _is_empty_whitespace "$line"; then 12 | echo "$line" 13 | fi 14 | done < <(wf-msg "${args[@]}") 15 | test $? -eq 0 || _error "Error calling \`wf-msg\` ${args[*]}" 16 | } 17 | 18 | function _jq { 19 | local query="$1" 20 | shift 21 | 22 | if ! command -v jq &>/dev/null; then 23 | _error "The command \`jq\` is needed: https://stedolan.github.io/jq/download" 24 | fi 25 | jq "$query" --raw-output "$@" 26 | } 27 | 28 | function _get_x { 29 | local coords="$1" 30 | echo "$coords" | cut -d ' ' -f1 31 | } 32 | 33 | function _get_y { 34 | local coords="$1" 35 | echo "$coords" | cut -d ' ' -f2 36 | } 37 | -------------------------------------------------------------------------------- /wf-utils/common.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | function minimize_window_on_unfocus { 4 | local args 5 | declare -A args=( 6 | [summary]="Minimise a window once it becomes unfoccused" 7 | [0:window_id]="Window ID. See something like \`find_titled_window\`" 8 | ) 9 | BAPt_parse_arguments args "$@" 10 | 11 | # TODO: use D-Bus subscribes 12 | while [[ "$(_wf-msg is_window_active "${args[window_id]}")" == "false" ]]; do 13 | sleep 0.5 14 | done 15 | while [[ "$(_wf-msg is_window_active "${args[window_id]}")" == "true" ]]; do 16 | sleep 0.5 17 | done 18 | _wf-msg minimize_window "${args[window_id]}" 19 | } 20 | 21 | function find_titled_window { 22 | local args 23 | declare -A args=( 24 | [summary]='Find a window by searching for its name and title. Returns window ID' 25 | [0:app]="Application ID, eg; 'firefox'" 26 | [1:titlish]='Regex to match title of window' 27 | [--allow-absence:flag]="Don't error if window not found" 28 | ) 29 | BAPt_parse_arguments args "$@" 30 | 31 | local query=".[] | 32 | select( 33 | (.app==\"${args[app]}\") and (.title|test(\"${args[titlish]}\")) 34 | ) | .id 35 | " 36 | _debug "jq query: $(echo "$query" | _trim)" 37 | 38 | match=$(_wf-msg get_all_windows --use-cache | _jq "$query" | head -n1) 39 | if ! _double_check_window_title "$match" "${args[titlish]}"; then 40 | match="" 41 | fi 42 | 43 | if [[ -z $match ]]; then 44 | match=$(_wf-msg get_all_windows | _jq "$query" | head -n1) 45 | fi 46 | 47 | if [[ -z $match && -z ${args[allow-absence]} ]]; then 48 | _error "Couldn't find a window matching '${args[app]}', '${args[titlish]}'" 49 | fi 50 | 51 | _debug "matched '${args[app]}', '${args[titlish]}': window $match" 52 | echo "$match" 53 | } 54 | 55 | function _double_check_window_title { 56 | local window_id=$1 titlish=$2 actual_title 57 | if [[ -z $window_id ]]; then 58 | return 1 59 | fi 60 | 61 | actual_title=$(wf-msg get_window_title "$window_id") 62 | _debug "$titlish =~ $actual_title" 63 | if [[ $actual_title =~ $titlish ]]; then 64 | return 0 65 | else 66 | return 1 67 | fi 68 | } 69 | 70 | # TODO: This is slow, ~150ms, because as well as the 2 calls in this function it also needs 71 | # to get the current output 72 | function move_window_to_current_workspace { 73 | local args 74 | declare -A args=( 75 | [summary]="Move window to current workspace" 76 | [0:window_id]="Window ID. See something like \`find_titled_window\`" 77 | ) 78 | BAPt_parse_arguments args "$@" 79 | 80 | local current_workspace x y 81 | current_workspace="$(_wf-msg get_current_workspace)" 82 | x=$(_get_x "$current_workspace") 83 | y=$(_get_y "$current_workspace") 84 | _wf-msg move_window_to_workspace "${args[window_id]}" "$x" "$y" 85 | } 86 | 87 | function wait_for_window_title_change { 88 | local args 89 | declare -A args=( 90 | [summary]='Wait until a window changes its title to that provided' 91 | [0:app]="Application ID, eg; 'firefox'" 92 | [1:titlish]='Regex to match title of window' 93 | [--timeout]="How long to wait until quitting, default 15 seconds" 94 | ) 95 | BAPt_parse_arguments args "$@" 96 | 97 | local app title window_id timeout="${args[timeout]:-$DEFAULT_TIMEOUT}" 98 | 99 | _debug "Waiting for window title (${args[app]}, ${args[titlish]})..." 100 | while read -r line ; do 101 | window_id=$(echo "$line" | sed -n 's/^\([0-9]*\),.*/\1/p') 102 | app=$(_wf-msg get_window_app "$window_id") 103 | [[ $app != "${args[app]}" ]] && continue 104 | title=$(_wf-msg get_window_title "$window_id") 105 | if [[ $title =~ ${args[titlish]} ]]; then 106 | echo "$window_id" 107 | _debug "Matched window ($window_id) title change: (${args[app]}, ${args[titlish]})" 108 | break 109 | fi 110 | done < <(_wf-msg dbus-signal view_title_changed --timeout "$timeout") 111 | } 112 | 113 | function wait_for_window_creation { 114 | local args 115 | declare -A args=( 116 | [summary]='Wait for a window with the given title to come into existence' 117 | [0:app]="Application ID, eg; 'firefox'" 118 | [1:titlish]='Regex to match title of window' 119 | [--timeout]="How long to wait until quitting, default 15 seconds" 120 | ) 121 | BAPt_parse_arguments args "$@" 122 | 123 | local app title window_id timeout="${args[timeout]:-$DEFAULT_TIMEOUT}" 124 | 125 | _debug "Waiting for window (${args[app]}, ${args[titlish]}) to be created..." 126 | while read -r window_id; do 127 | app=$(_wf-msg get_window_app "$window_id") 128 | [[ $app != "${args[app]}" ]] && continue 129 | title=$(_wf-msg get_window_title "$window_id") 130 | if [[ $title =~ ${args[titlish]} ]]; then 131 | echo "$window_id" 132 | _debug "Matched window creation: (${args[app]}, ${args[titlish]})" 133 | break 134 | fi 135 | done < <(_wf-msg dbus-signal view_added --timeout "$timeout") 136 | if [[ -n $window_id ]]; then 137 | return 0 138 | else 139 | return 1 140 | fi 141 | } 142 | -------------------------------------------------------------------------------- /wf-utils/move_titled_window_to_workspace.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | function move_titled_window_to_workspace { 4 | local args 5 | declare -A args=( 6 | [summary]="Find a window by its app and title then move it to a workspace" 7 | [0:app]="Application ID, eg; 'firefox'" 8 | [1:titlish]="Regex to match title of window" 9 | [2:x]="x-coord of destination workspace" 10 | [3:y]="y-coord of destination workspace" 11 | [--timeout]="How many seconds to wait if using a waiting flag, default: $default_timeout" 12 | [--on-creation:flag]="Whether to first wait for the window to be created" 13 | [--wait-for-title:flag]="Whether to wait for the title to change" 14 | ) 15 | BAPt_parse_arguments args "$@" 16 | 17 | local window timeout="${args[timeout]:-$DEFAULT_TIMEOUT}" 18 | window=$(find_titled_window "${args[app]}" "${args[titlish]}" --allow-absence) 19 | 20 | if [[ -z $window && -n ${args[wait-for-title]} ]]; then 21 | window=$( 22 | wait_for_window_title_change \ 23 | "${args[app]}" \ 24 | "${args[titlish]}" \ 25 | --timeout "$timeout" 26 | ) 27 | fi 28 | 29 | if [[ -z $window && -n ${args[on-creation]} ]]; then 30 | window="$( 31 | wait_for_window_creation \ 32 | "${args[app]}" \ 33 | "${args[titlish]}" \ 34 | --timeout "$timeout" 35 | )" 36 | fi 37 | 38 | if [[ -n $window ]]; then 39 | _wf-msg move_window_to_workspace "$window" "${args[x]}" "${args[y]}" 40 | else 41 | _error "Window not found" 42 | fi 43 | 44 | } 45 | -------------------------------------------------------------------------------- /wf-utils/peek_titled_window.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | function peek_titled_window { 4 | local args 5 | declare -A args=( 6 | [summary]="Find a window and bring it to the current workspace" 7 | [details]="$(cat <<-EOM 8 | Note that the window will be automatically minimised once 9 | it's unfocussed. So this functionality is for when you want 10 | something like a "popup" window, say a terminal to enter a 11 | quick command. 12 | 13 | Also, calling this function when the window is already 14 | focussed will minimise it. 15 | EOM 16 | )" 17 | [0:app]="Application ID, eg; 'firefox'" 18 | [1:titlish]="Regex to match title of window" 19 | ) 20 | BAPt_parse_arguments args "$@" 21 | 22 | local window is_active 23 | window=$(find_titled_window "${args[app]}" "${args[titlish]}") 24 | move_window_to_current_workspace "$window" & 25 | is_active=$(_wf-msg is_window_active "$window") 26 | 27 | if [[ "$is_active" == "false" ]]; then 28 | _debug "window $window is not active. focussing ..." 29 | _wf-msg unminimize_window "$window" 30 | minimize_window_on_unfocus "$window" 31 | else 32 | _debug "window $window is active. hiding ..." 33 | _wf-msg minimize_window "$window" 34 | fi 35 | } 36 | -------------------------------------------------------------------------------- /wf-utils/wf-utils: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function _load_wf_msg_deps { 4 | _wf_msg_path=$(dirname "$(readlink -f "$(which wf-msg)")") 5 | # shellcheck disable=1091 6 | source "$_wf_msg_path/_bash_args_parser.bash" 7 | # shellcheck disable=1091 8 | source "$_wf_msg_path/_lib.bash" 9 | # shellcheck disable=1091 10 | source "$_wf_msg_path/_help.bash" 11 | } 12 | 13 | trap "exit 1" TERM 14 | export _TOP_PID=$$ 15 | _load_wf_msg_deps 16 | 17 | _init "$@" 18 | --------------------------------------------------------------------------------