├── debian ├── compat ├── source │ └── format ├── rules ├── control └── changelog ├── .gitignore ├── .travis.yml ├── Vagrantfile ├── jumpappify-desktop-entry ├── LICENSE.txt ├── jumpapp.spec ├── Makefile ├── README.md ├── jumpapp └── t └── test_jumpapp /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | debian/copyright 3 | jumpapp*.changes 4 | jumpapp*.deb 5 | jumpapp*.rpm 6 | jumpapp*.tar.bz2 7 | jumpapp.1 8 | README.man.md 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | addons: 4 | apt: 5 | packages: 6 | - devscripts 7 | - shunit2 8 | 9 | script: 10 | - make test 11 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # Uncomment this to turn on verbose mode. 4 | #export DH_VERBOSE=1 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_auto_install: 10 | dh_auto_install -- PREFIX=/usr 11 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | # Generated with: 2 | # dh_make --single --indep --copyright mit --file ../jumpapp_0.1.tar.bz2 3 | 4 | Source: jumpapp 5 | Section: x11 6 | Priority: optional 7 | Maintainer: Michael Kropat 8 | Build-Depends: debhelper (>= 8.0.0), pandoc, shunit2 9 | Standards-Version: 4.1.4 10 | Homepage: https://github.com/mkropat/jumpapp 11 | Vcs-Git: https://github.com/mkropat/jumpapp.git 12 | Vcs-Browser: https://github.com/mkropat/jumpapp 13 | 14 | Package: jumpapp 15 | Architecture: all 16 | Depends: wmctrl, ${misc:Depends} 17 | Recommends: xdotool 18 | Description: jump to another application, unconditionally 19 | Jumpapp focuses the window of the application you're interested in — assuming 20 | it's already running — otherwise jumpapp launches the application for you. 21 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | provision_ubuntu = < ~/.local/share/applications/chromium-browser.desktop 13 | 14 | Or convert multiple in one go: 15 | 16 | for entry in /usr/share/applications/{firefox,gnome-terminal}.desktop; do 17 | target=~/\".local/share/applications/\$(basename \"\$entry\")\" 18 | jumpappify-desktop-entry \"\$entry\" >\"\$target\" 19 | done" 20 | exit 0 21 | fi 22 | 23 | # Desktop Entry reference: http://standards.freedesktop.org/desktop-entry-spec/latest/index.html 24 | 25 | exec perl -pe 'do { 26 | s/^(Name=.*)/\1 (Jump)/; 27 | s/^Exec=(.*)/Exec=jumpapp \1/; 28 | s/^Exec=jumpapp (?=.*%\w)(.*)/Exec=jumpapp -p \1/; 29 | } if /^\[Desktop Entry\]/ ... /^\[.*\]/' "$@" 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Michael Kropat and contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /jumpapp.spec: -------------------------------------------------------------------------------- 1 | # jumpapp.spec: specification for building the .rpm file 2 | 3 | # Created with: rpmdev-newspec jumpapp 4 | 5 | Name: jumpapp 6 | Version: VERSION 7 | Release: 1%{?dist} 8 | Summary: jump to another application, unconditionally 9 | 10 | License: MIT 11 | URL: https://github.com/mkropat/jumpapp 12 | Source0: %{name}_%{version}.tar.bz2 13 | 14 | BuildArch: noarch 15 | BuildRequires: pandoc 16 | Requires: wmctrl 17 | 18 | %description 19 | Jumpapp focuses the window of the application you're interested in — assuming 20 | it's already running — otherwise jumpapp launches the application for you. 21 | 22 | %prep 23 | %setup -q 24 | 25 | %build 26 | make 27 | 28 | %install 29 | rm -rf $RPM_BUILD_ROOT 30 | make DESTDIR=%{buildroot} PREFIX=/usr install 31 | mkdir -p %{buildroot}%{_unitdir} 32 | 33 | %files 34 | %{_bindir}/* 35 | %{_mandir}/man1/* 36 | 37 | %changelog 38 | * Wed Mar 18 2022 Michael Kropat - 1.2-1 39 | - Add fallback for non-stacking window managers 40 | 41 | * Wed Jun 20 2019 Michael Kropat - 1.1-1 42 | - Add -m option to toggle window visibility 43 | - Change window-type filter logic to work better with Slack 44 | 45 | * Mon Jul 2 2018 Michael Kropat - 1.0-1 46 | - Add -C option to center mouse 47 | - Fix stacking order bug with >10 windows 48 | 49 | * Sat Mar 4 2017 Michael Kropat - 0.9-1 50 | - Make `-t` support regex matching 51 | 52 | * Tue Apr 12 2016 Michael Kropat - 0.8-1 53 | - Jump to last-focused window when switching applications 54 | - Add `-t` title-matching option 55 | - Add `-w` workspace-matching option 56 | 57 | * Fri Mar 28 2014 Michael Kropat - 0.2-1 58 | - Window Cycling feature 59 | 60 | * Thu Mar 27 2014 Michael Kropat - 0.1-1 61 | - Initial version 62 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | jumpapp (1.2-1) focal; urgency=low 2 | 3 | * Add fallback for non-stacking window managers 4 | 5 | -- Michael Kropat Fri, 18 Mar 2022 21:44:27 -0400 6 | 7 | jumpapp (1.1-1) bionic; urgency=medium 8 | 9 | * Add -m option to toggle window visibility 10 | * Change window-type filter logic to work better with Slack 11 | 12 | -- Michael Kropat Thu, 20 Jun 2019 22:22:57 -0400 13 | 14 | jumpapp (1.0-1) xenial; urgency=low 15 | 16 | * Add -C option to center mouse 17 | * Fix stacking order bug with >10 windows 18 | 19 | -- Michael Kropat Mon, 02 Jul 2018 17:47:18 -0400 20 | 21 | jumpapp (0.9-1) xenial; urgency=low 22 | 23 | * Make `-t` support regex matching 24 | 25 | -- Michael Kropat Sat, 04 Mar 2017 23:14:00 +0000 26 | 27 | jumpapp (0.8-1) trusty; urgency=low 28 | 29 | * Jump to last-focused window when switching applications 30 | * Add `-t` title-matching option 31 | * Add `-w` workspace-matching option 32 | 33 | -- Michael Kropat Tue, 12 Apr 2016 01:08:55 +0000 34 | 35 | jumpapp (0.7-1) trusty; urgency=low 36 | 37 | * Handle multiple _NET_WM_WINDOW_TYPE values 38 | 39 | -- Michael Kropat Tue, 10 Mar 2015 21:53:13 -0400 40 | 41 | jumpapp (0.6-1) trusty; urgency=low 42 | 43 | * Add `-L` option 44 | 45 | -- Michael Kropat Thu, 26 Feb 2015 01:14:12 -0500 46 | 47 | jumpapp (0.5-1) trusty; urgency=low 48 | 49 | * Do not check hostname for WM_CLASS match 50 | 51 | -- Michael Kropat Sat, 17 Jan 2015 16:10:07 -0500 52 | 53 | jumpapp (0.4-1) trusty; urgency=low 54 | 55 | * Fix logic for matching hostname 56 | 57 | -- Michael Kropat Mon, 17 Nov 2014 23:22:13 -0500 58 | 59 | jumpapp (0.3-1) trusty; urgency=low 60 | 61 | * Add `-r` option 62 | * Only match on windows with normal/dialog type 63 | * Add -p arg passthrough option 64 | 65 | -- Michael Kropat Mon, 01 Sep 2014 15:49:53 -0400 66 | 67 | jumpapp (0.2-1) saucy; urgency=low 68 | 69 | * Window Cycling feature 70 | 71 | -- Michael Kropat Fri, 28 Mar 2014 23:25:21 -0400 72 | 73 | jumpapp (0.1-1ubuntu1) saucy; urgency=medium 74 | 75 | * Fix issue with PPA build 76 | 77 | -- Michael Kropat Fri, 28 Mar 2014 13:04:57 -0400 78 | 79 | jumpapp (0.1-1) saucy; urgency=low 80 | 81 | * Initial release 82 | 83 | -- Michael Kropat Thu, 27 Mar 2014 21:05:40 -0400 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = /usr/local 2 | 3 | BIN = $(DESTDIR)/$(PREFIX)/bin 4 | MAN = $(DESTDIR)/$(PREFIX)/share/man 5 | 6 | VERSION = 1.2 7 | PACKAGE_DIR = jumpapp-$(VERSION) 8 | PACKAGE_FILE = jumpapp_$(VERSION).tar.bz2 9 | PACKAGE_ORIG_FILE = jumpapp_$(VERSION).orig.tar.bz2 10 | 11 | AUTHOR = Michael Kropat 12 | DATE = Mar 18, 2022 13 | FILES = t README.md LICENSE.txt Makefile jumpapp jumpappify-desktop-entry 14 | 15 | .PHONY: all 16 | all: jumpapp.1 17 | 18 | jumpapp.1: README.man.md 19 | @hash pandoc 2>/dev/null || { echo "ERROR: can't find pandoc. Have you installed it?" >&2; exit 1; } 20 | pandoc --from=markdown --standalone --output="$@" "$<" 21 | 22 | README.man.md: README.md 23 | echo '% JUMPAPP(1) jumpapp | $(VERSION)' >"$@" 24 | echo '% $(AUTHOR)' >>"$@" 25 | echo '% $(DATE)' >>"$@" 26 | perl -ne 's/^##/#/; print if ! (/^# Installation/ ... /^# /) || /^# (?!Installation)/' "$<" >>"$@" 27 | 28 | .PHONY: check test 29 | check: test 30 | test: 31 | -shellcheck --exclude=SC2016,SC2034 jumpappify-desktop-entry 32 | -checkbashisms jumpappify-desktop-entry 33 | t/test_jumpapp 34 | 35 | .PHONY: install 36 | install: 37 | mkdir -p "$(BIN)" 38 | cp jumpapp jumpappify-desktop-entry "$(BIN)" 39 | 40 | mkdir -p "$(MAN)/man1" 41 | cp jumpapp.1 "$(MAN)/man1/" 42 | 43 | .PHONY: uninstall 44 | uninstall: 45 | -rm -f "$(BIN)/jumpapp" "$(BIN)/jumpappify-desktop-entry" 46 | -rm -f "$(MAN)/man1/jumpapp.1" 47 | 48 | .PHONY: clean 49 | clean: 50 | -rm -f README.man.md 51 | -rm -f jumpapp*.tar.bz2 jumpapp*.deb jumpapp*.rpm 52 | -rm -f jumpapp.1 53 | 54 | 55 | ##### make dist ##### 56 | 57 | .PHONY: dist 58 | dist: $(PACKAGE_FILE) 59 | 60 | $(PACKAGE_FILE): $(FILES) 61 | tar --transform 's,^,$(PACKAGE_DIR)/,S' -cjf "$@" $^ 62 | 63 | 64 | ### make deb deb-src deb-clean ### 65 | 66 | .PHONY: deb 67 | deb: jumpapp_$(VERSION)-1_all.deb 68 | 69 | jumpapp_$(VERSION)-1_all.deb: $(PACKAGE_FILE) debian/copyright 70 | @hash dpkg-buildpackage 2>/dev/null || { \ 71 | echo "ERROR: can't find dpkg-buildpackage. Did you run \`sudo apt-get install debhelper devscripts\`?" >&2; exit 1; \ 72 | } 73 | dpkg-buildpackage -b -tc -uc -us 74 | mv "../$@" . 75 | mv ../jumpapp_$(VERSION)-1_*.changes . 76 | 77 | .PHONY: deb-src 78 | deb-src: jumpapp_$(VERSION)-1_source.changes 79 | 80 | jumpapp_$(VERSION)-1_source.changes: $(PACKAGE_FILE) $(PACKAGE_ORIG_FILE) debian/copyright 81 | @hash dpkg-buildpackage 2>/dev/null || { echo "ERROR: can't find debuild. Did you run \`sudo apt-get install debhelper devscripts\`?" >&2; exit 1; } 82 | tar xf "$<" 83 | cp -r debian "$(PACKAGE_DIR)" 84 | (cd "$(PACKAGE_DIR)"; debuild -S) 85 | 86 | $(PACKAGE_ORIG_FILE): $(PACKAGE_FILE) 87 | cp "$<" "$@" 88 | 89 | debian/copyright: LICENSE.txt 90 | cp "$<" "$@" 91 | 92 | .PHONY: deb-clean 93 | deb-clean: 94 | -debian/rules clean 95 | -rm -f *.build *.changes *.dsc *.debian.tar.gz *.orig.tar.bz2 96 | -rm -rf $(PACKAGE_DIR) 97 | -rm -f debian/copyright 98 | 99 | .PHONY: deb-deploy 100 | deb-deploy: jumpapp_$(VERSION)-1_source.changes 101 | dput ppa:mkropat/ppa "$<" 102 | 103 | ubuntu-%: 104 | vagrant up ubuntu 105 | vagrant ssh -c 'cd /vagrant; make $*' ubuntu 106 | 107 | 108 | ### make rpm ### 109 | 110 | .PHONY: rpm 111 | rpm: $(PACKAGE_FILE) 112 | @hash rpmbuild 2>/dev/null || { echo "ERROR: can't find rpmbuild. Did you run \`yum install @development-tools\`?" >&2; exit 1; } 113 | @test -d "$$HOME/rpmbuild" || { echo "ERROR: ~/rpmbuild does not exist. Did you run \`rpmdev-setuptree\`?" >&2; exit 1; } 114 | cp "$<" ~/rpmbuild/SOURCES/ 115 | sed s/VERSION/$(VERSION)/ jumpapp.spec >~/rpmbuild/SPECS/jumpapp.spec 116 | rpmbuild -ba ~/rpmbuild/SPECS/jumpapp.spec 117 | mv ~/rpmbuild/RPMS/noarch/jumpapp-$(VERSION)-1.*.noarch.rpm . 118 | mv ~/rpmbuild/SRPMS/jumpapp-$(VERSION)-1.*.src.rpm . 119 | 120 | fedora-%: 121 | vagrant up fedora 122 | vagrant ssh -c 'cd /vagrant; make $*' fedora 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jumpapp 2 | 3 | *A run-or-raise application switcher for any X11 desktop* 4 | 5 | [![Build Status](https://travis-ci.org/mkropat/jumpapp.svg?branch=master)](https://travis-ci.org/mkropat/jumpapp) 6 | 7 | The idea is simple — bind a key for any given application that will: 8 | 9 | - launch the application, if it's not already running, or 10 | - focus the application's most recently opened window, if it is running. 11 | 12 | Pressing the key again will cycle to the application's next window, if there's more than one. 13 | 14 | In short, **jumpapp** is probably the fastest way for a keyboard-junkie to switch between applications in a modern desktop environment. Once [installed](#installation), all you have to do is configure the key bindings you want to use: 15 | 16 | ![Settings Example](http://i.imgur.com/dAj8NDZ.png "On Ubuntu it's under All Settings → Keyboard → Shortcuts") 17 | 18 | ## Synopsis 19 | 20 | Usage: jumpapp [OPTION]... COMMAND [ARG]... 21 | 22 | Jump to (focus) the existing window for an application, if it's running. 23 | Otherwise, launch COMMAND (with optional ARGs) to start the application. 24 | 25 | Options: 26 | -r -- cycle through windows in reverse order 27 | -f -- force COMMAND to launch if process found but no windows found 28 | -m -- if a single window is already open and in focus - minimize it 29 | -n -- do not fork into background when launching COMMAND 30 | -N -- don't launch if no window is found 31 | -p -- always launch COMMAND when ARGs passed 32 | (see Argument Passthrough in man page) 33 | -L -- list matching windows for COMMAND and quit 34 | -t NAME -- process window has to have NAME as the window title 35 | -c NAME -- find window using NAME as WM_CLASS (instead of COMMAND) 36 | -i NAME -- find process using NAME as the command name (instead of COMMAND) 37 | -w -- only find the applications in the current workspace 38 | -R -- bring the application to the current workspace when raising 39 | (the default behaviour is to switch to the workspace that the 40 | application is currently on) 41 | -C -- center cursor when raising application 42 | 43 | ## Installation 44 | 45 | ### Ubuntu, Debian and Friends 46 | 47 | sudo apt-get install build-essential debhelper git pandoc shunit2 48 | git clone https://github.com/mkropat/jumpapp.git 49 | cd jumpapp 50 | make deb 51 | sudo dpkg -i jumpapp*all.deb 52 | # if there were missing dependencies 53 | sudo apt-get install -f 54 | 55 | ### Fedora and Friends 56 | 57 | git clone https://github.com/mkropat/jumpapp.git 58 | cd jumpapp 59 | make rpm 60 | sudo yum localinstall jumpapp*.noarch.rpm 61 | 62 | ### Arch linux and Friends 63 | 64 | `yay` is used here but any [AUR 65 | helper](https://wiki.archlinux.org/title/AUR_helpers) is fine. 66 | 67 | yay -S jumpapp 68 | 69 | ### From Source 70 | 71 | git clone https://github.com/mkropat/jumpapp.git 72 | cd jumpapp 73 | make && sudo make install 74 | 75 | ## Argument Passthrough (`-p` option) 76 | 77 | Many applications keep track of what windows they have open so that if you run 78 | the command again, it will interact with the existing application window 79 | instead of launching a new instance of the application. 80 | 81 | Take Firefox, for example. If you already have a Firefox window open and you 82 | run `firefox https://github.com/`, Firefox won't start a new instance. What it 83 | does is open a new tab in the existing window and browse to the URL you passed. 84 | 85 | [Especially in the case of Desktop Entry files](#jumpappify-desktop-entry1), we 86 | want to preserve this behavior. With `jumpapp -p COMMAND [ARGs]...`, when you 87 | include one or more ARGs, COMMAND is always executed in order to pass the ARGs 88 | to the running application. But if no ARGs are included, **jumpapp** will 89 | behave normally. 90 | 91 | ## A Wrapper Around wmctrl(1) 92 | 93 | All the heavy lifting is done by Tomáš Stýblo's powerful 94 | [**wmctrl**](http://tripie.sweb.cz/utils/wmctrl/). You must have it installed to 95 | use **jumpapp**. 96 | 97 | **jumpapp** was built for the GNOME desktop environment. There's a good chance 98 | though that it'll work on [any window manager supported by 99 | **wmctrl**](http://tripie.sweb.cz/utils/wmctrl/#about). 100 | 101 | ## XBindKeys 102 | 103 | If your desktop environment doesn't offer a way to bind keys to commands — or if it's too limited — take a look at [XBindKeys](http://www.nongnu.org/xbindkeys/xbindkeys.html). 104 | 105 | Example `.xbindkeysrc`: 106 | 107 | "jumpapp chromium" 108 | control + alt + c 109 | 110 | "jumpapp -r chromium" 111 | shift + control + alt + c 112 | 113 | "jumpapp firefox" 114 | control + alt + f 115 | 116 | "jumpapp -r firefox" 117 | shift + control + alt + f 118 | 119 | "jumpapp gnome-terminal" 120 | control + alt + t 121 | 122 | "jumpapp -r gnome-terminal" 123 | shift + control + alt + t 124 | 125 | ## jumpappify-desktop-entry(1) 126 | 127 | **jumpapp** ships with a helper utility: 128 | 129 | Usage: jumpappify-desktop-entry SOMEFILE.desktop 130 | 131 | Given a desktop entry file (*.desktop), output a new desktop entry file that 132 | wraps the application's `Exec` in a call to jumpapp(1). 133 | 134 | EXAMPLES 135 | 136 | jumpappify-desktop-entry /usr/share/applications/chromium-browser.desktop \ 137 | > ~/.local/share/applications/chromium-browser.desktop 138 | 139 | Or convert multiple in one go: 140 | 141 | for entry in /usr/share/applications/{firefox,gnome-terminal}.desktop; do 142 | target=~/".local/share/applications/$(basename "$entry")" 143 | jumpappify-desktop-entry "$entry" >"$target" 144 | done 145 | 146 | ## Continue On Your Path To Keyboard Nirvana 147 | 148 | - [Blazing-Fast Application Switching in Linux](https://vickychijwani.me/blazing-fast-application-switching-in-linux/) — a blog post that talks about the advantages of this style of application switching 149 | - [Saka Key](https://key.saka.io/docs/about/introduction) / [Tridactyl](https://github.com/tridactyl/tridactyl) — navigate the web with your keyboard 150 | - [Vim](https://www.vim.org/) / [Neovim](https://neovim.io/) — the granddaddy of keyboard driven programs 151 | 152 | ## Uninstall jumpapp 153 | 154 | ### Ubuntu, Debian and Friends 155 | sudo apt remove jumpapp 156 | -------------------------------------------------------------------------------- /jumpapp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | show_usage() { 4 | local cmd=$(basename "${BASH_SOURCE[0]}") 5 | echo "Usage: $cmd [OPTION]... COMMAND [ARG]... 6 | 7 | Jump to (focus) the first open window for an application, if it's running. 8 | Otherwise, launch COMMAND (with optional ARGs) to start the application. 9 | 10 | Options: 11 | -r -- cycle through windows in reverse order 12 | -f -- force COMMAND to launch if process found but no windows found 13 | -m -- if a single window is already open and in focus - minimize it 14 | -n -- do not fork into background when launching COMMAND 15 | -N -- don't launch if no window is found 16 | -p -- always launch COMMAND when ARGs passed 17 | (see Argument Passthrough in man page) 18 | -L -- list matching windows for COMMAND and quit 19 | -t NAME -- process window has to have NAME as the window title 20 | -c NAME -- find window using NAME as WM_CLASS (instead of COMMAND) 21 | -i NAME -- find process using NAME as the command name (instead of COMMAND) 22 | -w -- only find the applications in the current workspace 23 | -R -- bring the application to the current workspace when raising 24 | (the default behaviour is to switch to the workspace that the 25 | application is currently on) 26 | -C -- center cursor when raising application" 27 | } 28 | 29 | main() { 30 | local classid cmdid force fork=1 list passthrough focusOrMinimize in_reverse matching_title workspace_filter mouse_center no_launch 31 | 32 | local OPTIND 33 | while getopts c:fhi:LmnNprt:wRC opt; do 34 | case "$opt" in 35 | c) classid="$OPTARG" ;; 36 | f) force=1 ;; 37 | h) show_usage; exit 0 ;; 38 | i) cmdid="$OPTARG" ;; 39 | L) list=1 ;; 40 | m) focusOrMinimize=1 ;; 41 | n) fork='' ;; 42 | N) no_launch=1; ;; 43 | p) passthrough=1; force=1 ;; # passthrough implies force 44 | r) in_reverse=1 ;; 45 | t) matching_title="$OPTARG"; force=1 ;; 46 | w) workspace_filter="$(get_active_workspace)"; force=1 ;; 47 | R) keep_workspace=1 ;; 48 | C) mouse_center=1 ;; 49 | esac 50 | done 51 | shift $(( OPTIND - 1 )) 52 | 53 | if (( ! $# )); then 54 | show_usage 55 | exit 0 56 | fi 57 | 58 | local cmd=$1 59 | shift 60 | 61 | check_for_prerequisites && 62 | jumpapp "$@" 63 | } 64 | 65 | SEP=$'\t' 66 | 67 | function join_words { 68 | local first=${1-} 69 | if shift; then 70 | printf %s "$first" "${@/#/$SEP}" 71 | fi 72 | } 73 | 74 | check_for_prerequisites() { 75 | if ! has_command wmctrl; then 76 | die 'Error: wmctrl(1) can not be found. Please install it to continue.' 77 | fi && 78 | 79 | if [[ -n "$mouse_center" ]] && ! has_command xdotool; then 80 | die 'Error: xdotool(1) can not be found. Please install it to use "-C".' 81 | fi 82 | } 83 | 84 | jumpapp() { 85 | if [[ -z "$cmdid" ]]; then 86 | local cmdid=$(basename "$cmd") 87 | fi 88 | 89 | if [[ -z "$classid" ]]; then 90 | local classid=$cmdid 91 | fi 92 | 93 | local pids=( $(list_pids_for_command "$cmdid") ) 94 | local windowids=( 95 | $(list_matching_windows "$classid" "${pids[@]}" | select_windowid) 96 | ) 97 | 98 | if [[ -n "$list" ]]; then 99 | printf 'Matched Windows [%d]\n' ${#windowids[@]} 100 | list_matching_windows "$classid" "${pids[@]}" | print_windows 101 | elif [[ -n "$focusOrMinimize" ]] && [[ ${#windowids[@]} -eq "1" ]] \ 102 | && [[ "$(get_active_windowid)" -eq "${windowids[0]}" ]]; then 103 | minimize_active_window 104 | elif (( ${#windowids[@]} )) && ! needs_passthrough "$@"; then 105 | local window=$(get_subsequent_window "${windowids[@]}") 106 | if [[ -n "$keep_workspace" ]]; then 107 | keep_workspace_activate_window "$window" || 108 | die "Error: unable to focus window for '$cmdid'" 109 | else 110 | change_workspace_activate_window "$window" || 111 | die "Error: unable to focus window for '$cmdid'" 112 | fi 113 | 114 | if [[ -n "$mouse_center" ]]; then 115 | center_cursor "$window" 116 | fi 117 | elif [[ -n "$no_launch" ]] && [[ ${#windowids[@]} -eq "0" ]]; then 118 | die "Not launching because the -N flag was given and no window was found for '$cmdid'" 119 | else 120 | if (( ${#pids[@]} )) && [[ -z "$force" ]]; then 121 | die "Error: found running process for '$cmdid', but found no window to jump to" 122 | else 123 | launch_command "$@" 124 | fi 125 | fi 126 | } 127 | 128 | needs_passthrough() { 129 | [[ "$passthrough" ]] && (( $# )) 130 | } 131 | 132 | list_matching_windows() { 133 | list_windows | 134 | where_title_matches "$matching_title" | 135 | where_class_or_pid_matches "$@" | 136 | where_workspace_matches | 137 | where_normal_window # spawns `xprop` process per-id, so do it last 138 | } 139 | 140 | where_workspace_matches() { 141 | while IFS="$SEP" read -r windowid hostname pid workspace class title; do 142 | if [[ -z "$workspace_filter" ]] || [[ "$workspace_filter" == "$workspace" ]] || [[ "$workspace" -lt 0 ]]; then 143 | printf '%s\n' "$(join_words "$windowid" "$hostname" "$pid" "$workspace" "$class" "$title")" 144 | fi 145 | done 146 | } 147 | 148 | where_title_matches() { 149 | while IFS="$SEP" read -r windowid hostname pid workspace class title; do 150 | if [[ "$matching_title" == '' || "$title" =~ $matching_title ]]; then 151 | printf '%s\n' "$(join_words "$windowid" "$hostname" "$pid" "$workspace" "$class" "$title")" 152 | fi 153 | done 154 | } 155 | 156 | where_class_or_pid_matches() { 157 | local target_class=$1 158 | shift 159 | 160 | local local_hostname=$(get_hostname) 161 | 162 | local windowid hostname pid workspace class title 163 | while IFS="$SEP" read -r windowid hostname pid workspace class title; do 164 | if equals_case_insensitive "$class" "$target_class"; then 165 | printf '%s\n' "$(join_words "$windowid" "$hostname" "$pid" "$workspace" "$class" "$title")" 166 | continue 167 | fi 168 | if equals_case_insensitive "$hostname" "$local_hostname" || [[ "$hostname" == "N/A" ]]; then 169 | for target_pid in "$@"; do 170 | if (( pid == target_pid )); then 171 | printf '%s\n' "$(join_words "$windowid" "$hostname" "$pid" "$workspace" "$class" "$title")" 172 | continue 2 173 | fi 174 | done 175 | fi 176 | done 177 | } 178 | 179 | where_normal_window() { 180 | local windowid rest 181 | while IFS="$SEP" read -r windowid rest; do 182 | case "$(get_window_types "$windowid")" in \ 183 | *_NET_WM_WINDOW_TYPE_DESKTOP* | \ 184 | *_NET_WM_WINDOW_TYPE_DOCK* | \ 185 | *_NET_WM_WINDOW_TYPE_TOOLBAR* | \ 186 | *_NET_WM_WINDOW_TYPE_MENU* | \ 187 | *_NET_WM_WINDOW_TYPE_UTILITY* | \ 188 | *_NET_WM_WINDOW_TYPE_SPLASH* | \ 189 | *_NET_WM_WINDOW_TYPE_DROPDOWN_MENU* | \ 190 | *_NET_WM_WINDOW_TYPE_POPUP_MENU* | \ 191 | *_NET_WM_WINDOW_TYPE_TOOLTIP* | \ 192 | *_NET_WM_WINDOW_TYPE_NOTIFICATION* | \ 193 | *_NET_WM_WINDOW_TYPE_COMBO* | \ 194 | *_NET_WM_WINDOW_TYPE_DND*) 195 | ;; 196 | *) 197 | printf '%s\n' "$(join_words "$windowid" "$rest")" 198 | ;; 199 | esac 200 | done 201 | } 202 | 203 | select_windowid() { 204 | local windowid rest 205 | while IFS="$SEP" read -r windowid rest; do 206 | printf '%s\n' "$windowid" 207 | done 208 | } 209 | 210 | print_windows() { 211 | local windowid hostname pid workspace class title 212 | while IFS="$SEP" read -r windowid hostname pid workspace class title; do 213 | printf '%s: %s\n' "$windowid $hostname $pid $workspace $class" "$title" 214 | done 215 | } 216 | 217 | get_subsequent_window() { 218 | _get_subsequent_window "$@" | head -1 219 | } 220 | 221 | _get_subsequent_window() { 222 | local active_window=$(get_active_windowid) 223 | 224 | if ! is_num_in_array "$active_window" "$@"; then 225 | if [[ -n $in_reverse ]]; then 226 | get_oldest_focused_window "$@" 227 | else 228 | get_most_recently_focused_window "$@" 229 | fi 230 | fi 231 | 232 | # always return a window here too in case the window manager doesn't return stacking order 233 | if [[ -n $in_reverse ]]; then 234 | get_prev_window "$active_window" "$@" 235 | else 236 | get_next_window "$active_window" "$@" 237 | fi 238 | } 239 | 240 | get_oldest_focused_window() { 241 | local windows_in_stacking_order=($(list_stacking_order)) 242 | 243 | for window in ${windows_in_stacking_order[@]}; do 244 | get_matching_window_from_list $window "$@" 245 | done 246 | } 247 | 248 | get_most_recently_focused_window() { 249 | local windows_in_stacking_order=($(list_stacking_order | reverse_words)) 250 | 251 | for window in ${windows_in_stacking_order[@]}; do 252 | get_matching_window_from_list $window "$@" 253 | done 254 | } 255 | 256 | get_matching_window_from_list() { 257 | local window_to_search_for=$1 258 | local window_list=${@:2} 259 | 260 | for window in ${window_list[@]}; do 261 | if [[ $window_to_search_for -eq $window ]]; then 262 | printf '%s\n' $window 263 | return 264 | fi 265 | done 266 | } 267 | 268 | reverse_words() { 269 | local i words 270 | IFS=' ' read -ra words 271 | for ((i=${#words[@]}; i>=0; i--)); do 272 | printf '%s ' "${words[i]}" 273 | done 274 | } 275 | 276 | get_prev_window() { 277 | local active=$1 prev 278 | shift 279 | 280 | if (( $1 == active )); then 281 | shift $(( $# - 1 )) 282 | printf '%s\n' "$1" 283 | else 284 | while [[ "$1" ]] && (( $1 != active )); do 285 | prev=$1 286 | shift 287 | done 288 | 289 | printf '%s\n' "$prev" 290 | fi 291 | } 292 | 293 | get_next_window() { 294 | local active=$1 295 | shift 296 | 297 | local first=$1 298 | 299 | while [[ "$1" ]] && (( $1 != active )); do 300 | shift 301 | done 302 | shift # get windowid *after* active 303 | 304 | if [[ "$1" ]]; then 305 | printf '%s\n' "$1" 306 | else 307 | printf '%s\n' "$first" 308 | fi 309 | } 310 | 311 | launch_command() { 312 | has_command "$cmd" || die "Error: unable to find command '$cmd'" 313 | 314 | printf 'Launching: %s\n' "$cmd $*" 315 | 316 | if [[ "$fork" ]]; then 317 | fork_command "$cmd" "$@" 318 | else 319 | exec_command "$cmd" "$@" 320 | fi 321 | } 322 | 323 | basename() { 324 | printf '%s\n' "${1##*/}" 325 | } 326 | 327 | is_num_in_array() { 328 | local element num=$1 329 | shift 330 | 331 | for element; do 332 | if [[ $num -eq $element ]]; then 333 | return 0 334 | fi 335 | done 336 | 337 | return 1 338 | } 339 | 340 | equals_case_insensitive() { 341 | [[ "${1^^}" == "${2^^}" ]] 342 | } 343 | 344 | 345 | ##### External Interfaces ##### 346 | 347 | # list_pids_for_command -- list all pids that have a matching argv[0] 348 | # A note on argv[0]: it's just a convention, not a kernel enforced value! 349 | # Programs are free to set it as they want, and so of course they do, ugh. 350 | # Some include the full path, others just the program name. Some 351 | # confusingly include all arguments in argv[0] (I'm looking at you 352 | # chromium-browser). 353 | list_pids_for_command() { 354 | if has_command pgrep; then 355 | list_pids_for_command_with_pgrep "$@" 356 | else 357 | list_pids_for_command_from_procfs "$@" 358 | fi 359 | } 360 | 361 | list_pids_for_command_with_pgrep() { 362 | pgrep -f "^(/.*/)?$1\b" 363 | } 364 | 365 | list_pids_for_command_from_procfs() { 366 | local cmd_argv0 367 | for path in /proc/*/cmdline; do 368 | read -rd '' cmd_argv0 <"$path" 369 | local cmd=${cmd_argv0##*/} # substring removal in-lined for performance 370 | if [[ "$cmd" == "$1"* ]]; then 371 | basename "${path%/cmdline}" 372 | fi 373 | done 374 | } 375 | 376 | # list_windows() -- list windowids with associated information 377 | # Column spec: windowid hostname pid workspace class 378 | # Where 'class' is the second WM_CLASS string (http://tronche.com/gui/x/icccm/sec-4.html#WM_CLASS) 379 | list_windows() { 380 | local windowid workspace pid class hostname title 381 | while read -r windowid workspace pid hostname title; do 382 | class="$(xprop -id "$windowid" ' $0+\n' WM_CLASS | 383 | sed -E -e 's/^.*", "(.*)"$/\1/' -e 's/[\\]"/"/g')" 384 | printf '%s\n' "$(join_words "$windowid" "$hostname" "$pid" "$workspace" "$class" "$title")" 385 | done < <(wmctrl -lp) 386 | } 387 | 388 | get_active_windowid() { 389 | local name windowid 390 | read name windowid < <(xprop -root ' $0\n' _NET_ACTIVE_WINDOW) 391 | printf '%s\n' "$windowid" 392 | } 393 | 394 | list_stacking_order() { 395 | local name ids 396 | read -r name ids < <(xprop -root ' $0+\n' _NET_CLIENT_LIST_STACKING) 397 | if [[ "$ids" == 'not found.' ]] || [[ "$ids" =~ 'no such atom' ]]; then 398 | # in case the window manager doesn't support _NET_CLIENT_LIST_STACKING (p.e. dwm) 399 | read -r name ids < <(xprop -root ' $0+\n' _NET_CLIENT_LIST) 400 | fi 401 | printf '%s\n' "$ids" | tr ',' ' ' 402 | } 403 | 404 | get_window_types() { 405 | local name window_types 406 | read -r name window_types < <(xprop -id "$1" ' $0+\n' _NET_WM_WINDOW_TYPE) 407 | if [[ "$window_types" != 'not found.' ]]; then 408 | printf '%s\n' "$window_types" 409 | fi 410 | } 411 | 412 | get_active_workspace() { 413 | local workspace flag rest 414 | while read -r workspace flag rest; do 415 | if [[ "$flag" == "*" ]]; then 416 | printf '%s\n' "$workspace" 417 | fi 418 | done < <(wmctrl -d) 419 | } 420 | 421 | change_workspace_activate_window() { 422 | wmctrl -i -a "$1" 423 | } 424 | 425 | keep_workspace_activate_window() { 426 | wmctrl -i -R "$1" 427 | } 428 | 429 | center_cursor() { 430 | xdotool mousemove -w "$1" $(wmctrl -lG | grep "$1" | awk '{ print $5/2 " " $6/2 }') 431 | } 432 | 433 | minimize_active_window() { 434 | xdotool getactivewindow windowminimize 435 | } 436 | 437 | has_command() { 438 | hash "$1" 2>/dev/null 439 | } 440 | 441 | fork_command() { 442 | ("$@" >/dev/null 2>&1) & 443 | } 444 | 445 | exec_command() { 446 | exec "$@" 447 | } 448 | 449 | get_hostname() { 450 | hostname 451 | } 452 | 453 | die() { 454 | printf '%s\n' "$1" >&2 455 | exit 1 456 | } 457 | 458 | is_script_executed() { 459 | [[ "${BASH_SOURCE[0]}" == "$0" ]] 460 | } 461 | 462 | 463 | if is_script_executed; then 464 | main "$@" 465 | fi 466 | -------------------------------------------------------------------------------- /t/test_jumpapp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | setUp() { 4 | active_windowid=0 5 | get_window_types_output=_NET_WM_WINDOW_TYPE_NORMAL 6 | list_pids_for_command_output= 7 | list_stacking_order_output= 8 | 9 | has_command_val=0 10 | 11 | change_workspace_activate_window_called= 12 | keep_workspace_activate_window_called= 13 | activate_window_called= 14 | activate_window_arg= 15 | center_cursor_called= 16 | center_cursor_arg= 17 | minimize_active_window_called= 18 | die_called= 19 | exec_command_called= 20 | focus_window_by_class_called= 21 | fork_command_called= 22 | } 23 | 24 | function join_lines { 25 | local first=${1-} 26 | local line_feed=$'\n' 27 | if shift; then 28 | printf %s "$first" "${@/#/$line_feed}" 29 | fi 30 | } 31 | 32 | it_echoes_help_when_run_with_0_args() { 33 | local output=$(main) 34 | [[ "$output" == Usage:* ]] || fail 'Output does not begin with "Usage:"' 35 | } 36 | 37 | it_echoes_help_when_run_with_dash_h() { 38 | local output=$(main -h) 39 | [[ "$output" == Usage:* ]] || fail 'Output does not begin with "Usage:"' 40 | } 41 | 42 | it_throws_an_error_when_wmctrl_is_not_installed() { 43 | has_command_val=1 44 | main someapp 45 | assertNotNull 'die() called' "$die_called" 46 | } 47 | 48 | it_calls_fork_command_when_process_is_not_running() { 49 | focus_window_by_class_val=1 50 | local output_file=$(mktemp) 51 | 52 | main someapp >|"$output_file" 53 | 54 | assertNotNull 'fork_command() called' "$fork_command_called" 55 | [[ "$(<"$output_file")" == *someapp* ]] || fail 'Output does not include "someapp"' 56 | assertEquals 'fork_command' someapp "$fork_command_cmd" 57 | 58 | rm -f "$output_file" 59 | } 60 | 61 | it_calls_exec_command_when_called_with_dash_n_and_process_is_not_running() { 62 | focus_window_by_class_val=1 63 | exec_command_cmd= 64 | local output_file=$(mktemp) 65 | 66 | main -n someapp >|"$output_file" 67 | 68 | assertNotNull 'exec_command() called' "$exec_command_called" 69 | [[ "$(<"$output_file")" == *someapp* ]] || fail 'Output does not include "someapp"' 70 | assertEquals 'exec_command' someapp "$exec_command_cmd" 71 | 72 | rm -f "$output_file" 73 | } 74 | 75 | it_throws_an_error_when_process_is_running_but_no_windows_found() { 76 | list_pids_for_command_output='123' 77 | main someapp >/dev/null 78 | assertNotNull 'die() called' "$die_called" 79 | } 80 | 81 | it_calls_fork_command_when_called_with_dash_f_and_process_is_running_but_no_windows_found() { 82 | list_pids_for_command_output='123' 83 | main -f someapp >/dev/null 84 | assertNotNull 'fork_command() called' "$fork_command_called" 85 | } 86 | 87 | it_calls_activate_window_when_window_found_by_pid() { 88 | list_pids_for_command_output='123' 89 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 90 | 91 | main someapp 92 | 93 | assertNotNull 'activate_window() called' "$activate_window_called" 94 | assertEquals 456 "$activate_window_arg" 95 | assertNull 'fork_command() not called' "$fork_command_called" 96 | } 97 | 98 | it_calls_change_workspace_activate_window_when_-R_is_not_passed() { 99 | list_pids_for_command_output='123' 100 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 101 | 102 | main someapp 103 | 104 | assertNull 'keep_workspace_activate_window() not called' "$keep_workspace_activate_window_called" 105 | assertNotNull 'change_workspace_activate_window() called' "$change_workspace_activate_window_called" 106 | assertEquals 456 "$activate_window_arg" 107 | assertNull 'fork_command() not called' "$fork_command_called" 108 | } 109 | 110 | it_calls_keep_workspace_activate_window_when_-R_is_passed() { 111 | list_pids_for_command_output='123' 112 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 113 | 114 | main -R someapp 115 | 116 | assertNull 'change_workspace_activate_window() not called' "$change_workspace_activate_window_called" 117 | assertNotNull 'keep_workspace_activate_window() called' "$keep_workspace_activate_window_called" 118 | assertEquals 456 "$activate_window_arg" 119 | assertNull 'fork_command() not called' "$fork_command_called" 120 | } 121 | 122 | it_calls_activate_window_only_on_local_windows_when_window_found_by_pid() { 123 | list_pids_for_command_output='123' 124 | list_windows_output="$(join_lines \ 125 | "$(join_words '456' 'otherhost' '123' '-1' 'some-class')" \ 126 | "$(join_words '567' 'somehost' '123' '-1' 'some-class')" \ 127 | )" 128 | 129 | main someapp 130 | 131 | assertNotNull 'activate_window() called' "$activate_window_called" 132 | assertEquals 567 "$activate_window_arg" 133 | assertNull 'fork_command() not called' "$fork_command_called" 134 | } 135 | 136 | it_calls_activate_window_on_windows_missing_hostname_when_window_found_by_pid() { 137 | list_pids_for_command_output='123' 138 | list_windows_output="$(join_lines \ 139 | "$(join_words '456' 'otherhost' '123' '-1' 'some-class')" \ 140 | "$(join_words '567' 'N/A' '123' '-1' 'some-class')" \ 141 | )" 142 | 143 | main someapp 144 | 145 | assertNotNull 'activate_window() called' "$activate_window_called" 146 | assertEquals 567 "$activate_window_arg" 147 | assertNull 'fork_command() not called' "$fork_command_called" 148 | } 149 | 150 | it_calls_activate_window_when_multiple_pids_exist() { 151 | list_pids_for_command_output='1 2 3 123' 152 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'Some-class')" 153 | 154 | main someapp 155 | 156 | assertNotNull 'activate_window() called' "$activate_window_called" 157 | assertNull 'fork_command() not called' "$fork_command_called" 158 | } 159 | 160 | it_calls_activate_window_when_window_found_by_class() { 161 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'someapp')" 162 | 163 | main someapp 164 | 165 | assertNotNull 'activate_window() called' "$activate_window_called" 166 | assertNull 'fork_command() not called' "$fork_command_called" 167 | } 168 | 169 | it_calls_activate_window_when_window_found_by_class_and_hostname_is_for_another_machine() { 170 | list_windows_output="$(join_words '456' 'otherhost' '123' '-1' 'someapp')" 171 | 172 | main someapp 173 | 174 | assertNotNull 'activate_window() called' "$activate_window_called" 175 | assertNull 'fork_command() not called' "$fork_command_called" 176 | } 177 | 178 | it_calls_activate_window_when_window_class_matches_with_different_case() { 179 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'SomeAPP')" 180 | 181 | main someapp 182 | 183 | assertNotNull 'activate_window() called' "$activate_window_called" 184 | assertNull 'fork_command() not called' "$fork_command_called" 185 | } 186 | 187 | it_calls_activate_window_with_second_windowid_when_first_is_active() { 188 | active_windowid=456 189 | list_stacking_order_output='999 456 999' 190 | list_windows_output="$(join_lines \ 191 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 192 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 193 | )" 194 | main someapp 195 | 196 | assertNotNull 'activate_window() called' "$activate_window_called" 197 | assertEquals 567 "$activate_window_arg" 198 | } 199 | 200 | it_calls_activate_window_with_first_windowid_when_last_is_active() { 201 | active_windowid=678 202 | list_windows_output="$(join_lines \ 203 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 204 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 205 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 206 | )" 207 | main someapp 208 | 209 | assertNotNull 'activate_window() called' "$activate_window_called" 210 | assertEquals 456 "$activate_window_arg" 211 | } 212 | 213 | it_calls_activate_window_with_rightmost_matching_windowid_in_list_stacking_order_when_active_window_isnt_in_list_stacking_order() { 214 | active_windowid=000 215 | list_stacking_order_output='999 456 789 999' 216 | list_windows_output="$(join_lines \ 217 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 218 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 219 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 220 | "$(join_words '789' 'somehost' '123' '-1' 'someapp')" \ 221 | )" 222 | main someapp 223 | 224 | assertNotNull 'activate_window() called' "$activate_window_called" 225 | assertEquals 789 "$activate_window_arg" 226 | } 227 | 228 | it_calls_activate_window_with_last_windowid_when_first_is_active_and_r_flag_is_passed() { 229 | active_windowid=456 230 | list_windows_output="$(join_lines \ 231 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 232 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 233 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 234 | )" 235 | main -r someapp 236 | 237 | assertNotNull 'activate_window() called' "$activate_window_called" 238 | assertEquals 678 "$activate_window_arg" 239 | } 240 | 241 | it_calls_activate_window_with_first_windowid_when_second_is_active_and_r_flag_is_passed() { 242 | active_windowid=567 243 | list_windows_output="$(join_lines \ 244 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 245 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 246 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 247 | )" 248 | main -r someapp 249 | 250 | assertNotNull 'activate_window() called' "$activate_window_called" 251 | assertEquals 456 "$activate_window_arg" 252 | } 253 | 254 | it_calls_activate_window_with_leftmost_matching_windowid_in_list_stacking_order_when_active_window_isnt_in_list_stacking_order() { 255 | active_windowid=000 256 | list_stacking_order_output='999 567 678 999' 257 | list_windows_output="$(join_lines \ 258 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 259 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 260 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 261 | "$(join_words '789' 'somehost' '123' '-1' 'someapp')" \ 262 | )" 263 | main -r someapp 264 | 265 | assertNotNull 'activate_window() called' "$activate_window_called" 266 | assertEquals 567 "$activate_window_arg" 267 | } 268 | 269 | it_matches_list_stacking_order_windowids_by_numeric_comparison() { 270 | active_windowid=000 271 | list_stacking_order_output='999 0xff 999' 272 | list_windows_output="$(join_lines \ 273 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 274 | "$(join_words '255' 'somehost' '123' '-1' 'someapp')" \ 275 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 276 | )" 277 | main someapp 278 | 279 | assertNotNull 'activate_window() called' "$activate_window_called" 280 | assertEquals 255 "$activate_window_arg" 281 | } 282 | 283 | it_calls_activate_window_with_windowid_that_matches_regex_title_passed_with_-t() { 284 | list_windows_output="$(join_lines \ 285 | "$(join_words '456' 'somehost' '123' '-1' 'someapp' 'not the window')" \ 286 | "$(join_words '567' 'somehost' '123' '-1' 'someapp' 'Some window and text')" \ 287 | "$(join_words '678' 'somehost' '123' '-1' 'someapp')" \ 288 | )" 289 | 290 | main -t "[Ss]ome window" someapp 291 | 292 | assertNotNull 'activate_window() called' "$activate_window_called" 293 | assertEquals 567 "$activate_window_arg" 294 | } 295 | 296 | it_calls_fork_command_if_no_matching_windows_but_matching_pids_and_title_passed_with_-t() { 297 | list_pids_for_command_output='123' 298 | 299 | main -t "Any window" someapp >/dev/null 300 | 301 | assertNull 'activate_window() not called' "$activate_window_called" 302 | assertNotNull 'fork_command() called' "$fork_command_called" 303 | } 304 | 305 | it_calls_activate_window_with_windowid_of_first_window_on_workspace_when_-w_passed() { 306 | active_workspace=1 307 | list_windows_output="$(join_lines \ 308 | "$(join_words '456' 'somehost' '123' '0' 'someapp')" \ 309 | "$(join_words '567' 'somehost' '123' '1' 'someapp')" \ 310 | "$(join_words '678' 'somehost' '123' '0' 'someapp')" \ 311 | )" 312 | main -w someapp 313 | 314 | assertNotNull 'activate_window() called' "$activate_window_called" 315 | assertEquals 567 "$activate_window_arg" 316 | } 317 | 318 | it_calls_activate_window_with_windowid_where_workspace_is_-1_when_-w_passed() { 319 | active_workspace=1 320 | list_windows_output="$(join_lines \ 321 | "$(join_words '456' 'somehost' '123' '0' 'someapp')" \ 322 | "$(join_words '567' 'somehost' '123' '-1' 'someapp')" \ 323 | "$(join_words '678' 'somehost' '123' '0' 'someapp')" \ 324 | )" 325 | main -w someapp 326 | 327 | assertNotNull 'activate_window() called' "$activate_window_called" 328 | assertEquals 567 "$activate_window_arg" 329 | } 330 | 331 | it_throws_an_error_when_process_is_running_but_all_window_type_are_known_not_normal_windows() { 332 | list_pids_for_command_output='123' 333 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'someapp')" 334 | get_window_types_output='_NET_WM_WINDOW_TYPE_DESKTOP, _NET_WM_WINDOW_TYPE_DOCK, _NET_WM_WINDOW_TYPE_TOOLBAR' 335 | 336 | main someapp >/dev/null 337 | 338 | assertNotNull 'die() called' "$die_called" 339 | } 340 | 341 | it_calls_activate_window_when_get_window_types_returns_normal_window() { 342 | get_window_types_output='_NET_WM_WINDOW_TYPE_NORMAL' 343 | list_pids_for_command_output='123' 344 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 345 | 346 | main someapp 347 | 348 | assertNotNull 'activate_window() called' "$activate_window_called" 349 | assertEquals 456 "$activate_window_arg" 350 | } 351 | 352 | it_calls_activate_window_when_get_window_types_returns_dialog_window() { 353 | get_window_types_output='_NET_WM_WINDOW_TYPE_NORMAL' 354 | list_pids_for_command_output='123' 355 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 356 | 357 | main someapp 358 | 359 | assertNotNull 'activate_window() called' "$activate_window_called" 360 | assertEquals 456 "$activate_window_arg" 361 | } 362 | 363 | it_calls_activate_window_when_get_window_types_returns_unknown_window_type() { 364 | get_window_types_output='_NET_WM_WINDOW_TYPE_DERP' 365 | list_pids_for_command_output='123' 366 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 367 | 368 | main someapp 369 | 370 | assertNotNull 'activate_window() called' "$activate_window_called" 371 | assertEquals 456 "$activate_window_arg" 372 | } 373 | 374 | it_calls_activate_window_when_get_window_types_returns_multiple_entires() { 375 | get_window_types_output='_NET_SOME_TYPE, _NET_WM_WINDOW_TYPE_NORMAL, _NET_ANOTHER_TYPE' 376 | list_pids_for_command_output='123' 377 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 378 | 379 | main someapp 380 | 381 | assertNotNull 'activate_window() called' "$activate_window_called" 382 | assertEquals 456 "$activate_window_arg" 383 | } 384 | 385 | it_calls_activate_window_with_second_windowid_when_first_is_not_normal() { 386 | # I haven't figured out how to test this yet, since we need to output 387 | # different values from `get_window_typees` and `get_window_types` is being 388 | # called in a subshell. 389 | : 390 | } 391 | 392 | it_calls_center_cursor_on_window_when_-C_is_passed() { 393 | list_pids_for_command_output='123' 394 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'some-class')" 395 | 396 | main -C someapp 397 | 398 | assertNotNull 'center_cursor() called' "$center_cursor_called" 399 | assertEquals 456 "$center_cursor_arg" 400 | } 401 | 402 | it_calls_list_pids_for_command_without_the_full_path() { 403 | list_pids_for_command_cmd_file=$(mktemp) 404 | main /usr/bin/someapp >/dev/null 405 | assertEquals someapp "$(<$list_pids_for_command_cmd_file)" 406 | rm -f "$list_pids_for_command_cmd_file" 407 | } 408 | 409 | it_calls_fork_command_if_-p_option_passed_and_a_command_argument_is_passed() { 410 | list_windows_output="$(join_words '456' 'somehost' '123' '-1' 'someapp')" # even if the application is already running 411 | 412 | main -p someapp somearg >/dev/null 413 | 414 | assertNull 'activate_window() not called' "$activate_window_called" 415 | assertNotNull 'fork_command() called' "$fork_command_called" 416 | } 417 | 418 | it_prints_count_0_when_called_with_-L() { 419 | list_windows_output='' 420 | 421 | local output=$(main -L someapp) 422 | [[ "$output" == "Matched Windows [0]" ]] || fail 'Output is not "Matched Windows [0]"' 423 | } 424 | 425 | it_prints_count_2_when_called_with_-L_and_2_match() { 426 | list_windows_output="$(join_lines \ 427 | "$(join_words '456' 'somehost' '123' '-1' 'someapp' 'SomeApp Window #1')" \ 428 | "$(join_words '567' 'somehost' '234' '-1' 'anotherapp' 'AnotherApp Window')" \ 429 | "$(join_words '678' 'somehost' '123' '-1' 'someapp' 'SomeApp Window #2')" \ 430 | )" 431 | 432 | local output=$(main -L someapp) 433 | [[ "$output" == "Matched Windows [2]"* ]] || fail 'Output does not start with "Matched Windows [2]"' 434 | } 435 | 436 | it_prints_matching_windows_when_called_with_-L() { 437 | list_windows_output="$(join_lines \ 438 | "$(join_words '456' 'somehost' '123' '-1' 'someapp' 'SomeApp Window #1')" \ 439 | "$(join_words '567' 'somehost' '234' '-1' 'anotherapp' 'AnotherApp Window')" \ 440 | "$(join_words '678' 'somehost' '123' '-1' 'someapp' 'SomeApp Window #2')" \ 441 | )" 442 | 443 | local output=$(main -L someapp) 444 | [[ "$output" == *"456 somehost 123 -1 someapp: SomeApp Window #1"* ]] || fail 'Output does not list Window #1' 445 | [[ "$output" != *"anotherapp"* ]] || fail 'Output lists anotherapp' 446 | [[ "$output" == *"678 somehost 123 -1 someapp: SomeApp Window #2"* ]] || fail 'Output does not list Window #2' 447 | } 448 | 449 | it_calls_minimize_active_window_when_called_with_-m_and_window_is_active() { 450 | active_windowid=456 451 | list_windows_output="$(join_lines \ 452 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 453 | "$(join_words '567' 'somehost' '234' '-1' 'anotherapp' 'AnotherApp Window')" \ 454 | )" 455 | main -m someapp 456 | 457 | assertNotNull 'minimize_active_window() called' "$minimize_active_window_called" 458 | } 459 | 460 | it_calls_activate_window_when_called_with_-m_but_the_window_is_not_active() { 461 | active_windowid=567 462 | list_windows_output="$(join_lines \ 463 | "$(join_words '456' 'somehost' '123' '-1' 'someapp')" \ 464 | "$(join_words '567' 'somehost' '234' '-1' 'anotherapp' 'AnotherApp Window')" \ 465 | )" 466 | main -m someapp 467 | 468 | assertNull 'minimize_active_window() not called' "$minimize_active_window_called" 469 | assertNotNull 'i() called' "$activate_window_called" 470 | assertEquals 456 "$activate_window_arg" 471 | } 472 | 473 | ##### Test Doubles ##### 474 | 475 | source ./jumpapp # load app now, so we can override it 476 | 477 | fork_command() { 478 | fork_command_called=1 479 | fork_command_cmd=$cmd 480 | } 481 | 482 | exec_command() { 483 | exec_command_called=1 484 | exec_command_cmd=$cmd 485 | } 486 | 487 | list_pids_for_command() { 488 | if [[ "$list_pids_for_command_cmd_file" ]]; then 489 | printf '%s\n' "$1" >|"$list_pids_for_command_cmd_file" 490 | fi 491 | printf '%s\n' "$list_pids_for_command_output" 492 | } 493 | 494 | list_windows() { 495 | printf '%s\n' "$list_windows_output" 496 | } 497 | 498 | get_active_windowid() { 499 | printf '%s\n' "$active_windowid" 500 | } 501 | 502 | get_window_types() { 503 | printf '%s\n' "$get_window_types_output" 504 | } 505 | 506 | get_active_workspace() { 507 | printf '%s\n' "$active_workspace" 508 | } 509 | 510 | change_workspace_activate_window() { 511 | change_workspace_activate_window_called=1 512 | activate_window_called=1 513 | activate_window_arg=$1 514 | return 0 515 | } 516 | 517 | keep_workspace_activate_window() { 518 | keep_workspace_activate_window_called=1 519 | activate_window_called=1 520 | activate_window_arg=$1 521 | return 0 522 | } 523 | 524 | center_cursor() { 525 | center_cursor_called=1 526 | center_cursor_arg=$1 527 | } 528 | 529 | minimize_active_window() { 530 | minimize_active_window_called=1 531 | } 532 | 533 | has_command() { 534 | return "$has_command_val" 535 | } 536 | 537 | get_hostname() { 538 | printf '%s\n' "somehost" 539 | } 540 | 541 | die() { 542 | die_called=1 543 | return 1 544 | } 545 | 546 | list_stacking_order() { 547 | printf '%s\n' "$list_stacking_order_output" 548 | } 549 | 550 | ##### Test Harness ##### 551 | 552 | # suite() -- find and register tests to be run 553 | # Derived from Gary Bernhardt's screencast #68 554 | # (https://www.destroyallsoftware.com/screencasts/catalog/test-driving-shell-scripts) 555 | suite() { 556 | tests=( $(grep ^it_ "$0" | cut -d '(' -f 1) ) 557 | for name in "${tests[@]}"; do 558 | suite_addTest "$name" 559 | done 560 | } 561 | 562 | if hash shunit2 2>/dev/null; then 563 | source shunit2 564 | else 565 | echo 'Error: shunit2(1) could not be located. Please install it on your $PATH.' >&2 566 | exit 1 567 | fi 568 | --------------------------------------------------------------------------------