Update (2024-03-08):
- Removed embedded gist and linked to GitHub instead.
- Add banner image and demo gif to this post.
- JetBrains has since released an official plugin on August 2023 but if you’re running Linux (Stream Deck SDK only supports Windows and Mac currently) you may still find this article guide useful.
I’ve had a Stream Deck for a while now but haven’t really configured it for any coding related workflows. I use several JetBrains IDEs for work and personal use (Intellij, PyCharm, WebStorm to name a few), so I started looking into what the possible options were.
The simplest solution would be to just invoke keyboard shortcut for a specific but the downside of this approach is you can only have so much keycodes assigned (which may conflict with system shortcuts and possibly other apps), not to mention you’d have to change these when you switch keymaps or use another operating system.
Ideally there would be a command where you can provide an action to the IDE, possibly through a plugin.
Table of Contents
IDE Scripting Console
While reading through the Intellij documentation, I stumbled upon the IDE scripting console.
The IDE Scripting Console can be used to write simple scripts that automate IntelliJ IDEA features and extract various information. With access to the IntelliJ platform API, you can think of it as a lightweight alternative to a plugin, which adds or modifies some behavior of the IDE.
By default, it supports scripts written in Kotlin, JavaScript, and Groovy. However, you can use any scripting language that is compliant with JSR 223, for example, Python, Ruby, Clojure, and so on.
This looks promising; I copied and modified some sample code from the docs to display a “Hello World” message in a dialog in the IDE.
import com.intellij.openapi.ui.Messages
val b = bindings as Map<String, Any>
val IDE = b["IDE"] as com.intellij.ide.script.IDE
Messages.showInfoMessage("Hello World")
Invoking Actions
The next step was figuring out how to invoke actions in the IDE. The Intellij Platform Plugin SDK defines the requirements in its Action system documentation.
An ActionManager
instance is used to execute an IDE Action
that is referred to by its unique id
– this can either be a custom action from an installed plugin or the standard IntelliJ Platform actions.
The following snippet executes the standard NextTab action:
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
val b = bindings as Map<String, Any>
val IDE = b["IDE"] as com.intellij.ide.script.IDE
val actionManager: ActionManager = ActionManager.getInstance()
// move focus to the next editor tab
val action: AnAction = actionManager.getAction("NextTab")
actionManager.tryToExecute(action, null, null, null, false)
Invoking the script from the command line
Up to now, I’ve been able to play around with the scripts from within the IDEs – the next step is to find a way to invoke them externally.
Fortunately, this feature is already available since version 2021.1.
It requires the command line scripts for the IDEs to be installed e.g. idea
for Intellij, this can be configured via the JetBrains Toolbox.
The command to run script(s) is idea ideScript <files>
.
Caveat: The script does not work (when invoked from the command line at least) unless the IDE action execution code is wrapped in the following snippet:
IDE.application.invokeLater {
// action execution code goes here
}
My guess is that it needs to be non-blocking, since it is invoked externally. One other thing to note is that if you have multiple windows of an IDE running, the action will be executed in the last active window.
Parameterizing the script
The last thing I wanted to add was to parameterize the script, where I can pass in the action name e.g. “Run tests” and the IDE where the aforementioned action would be executed.
I tried a couple of things; my first implementation involved passing the params as environment
variables and parsing them in the script. That didn’t seem to work as when I tried to log the params
they would always be null
. The next thing I tried was to reading the params from a text file which
also did not work.
Eventually, my solution involved creating a shell script that:
- Requires (and parses) the IDE name and action name as arguments
- Writes out the IDE script to a file (taking params into account)
- Executes the IDE script
- Deletes the IDE script file
So if I wanted to perform an action on an Intellij IDEA window, the command will look like
ide-script.sh --ide idea --action action_name_to_perform
Putting it all together
I’ve tested this script on both macOS and Linux. The steps should be similar on Windows; you also might be able to reuse the shell script if you are running WSL but I imagine you’ll need to update the path of the command-line IDE launchers.
#!/usr/bin/env bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
ide_script_name="ide_script.groovy"
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-f] -i ide -a action
Shell script wrapper that executes actions on JetBrains IDEs via the scripting console.
Available options:
-h, --help Print this help and exit
-i, --ide JetBrains IDE command-line launcher e.g. idea, webstorm, pycharm
-a, --action IDE action to execute
EOF
exit
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
rm -f "$script_dir/$ide_script_name"
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
die() {
local msg=$1
local code=${2-1} # default exit status 1
msg "$msg"
exit "$code"
}
parse_params() {
ide=''
action=''
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-f | --flag) flag=1 ;;
-i | --ide)
ide="${2-}"
shift
;;
-a | --action)
action="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
[[ -z "${ide-}" ]] && die "Missing required parameter: ide"
[[ -z "${action-}" ]] && die "Missing required parameter: action"
return 0
}
# Invoke supported IDE actions using the IDE scripting console. See links for more details.
#
# Supported IDE actions: https://github.com/JetBrains/intellij-community/blob/idea/231.8109.175/platform/ide-core/src/com/intellij/openapi/actionSystem/IdeActions.java
# IDE scripting console: https://www.jetbrains.com/help/idea/ide-scripting-console.html
generate_ide_script() {
cat <<EOF
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.ui.Messages
var actionManager = ActionManager.getInstance()
IDE.application.invokeLater {
try {
var action = actionManager.getAction("$action")
var result = actionManager.tryToExecute(action, null, null, null, false)
if (result.rejected) {
Messages.showErrorDialog(result.error, "IDE action error")
}
} catch (ex) {
Messages.showErrorDialog(ex.message, "IDE action error")
}
}
EOF
}
parse_params "$@"
setup_colors
# Generate IDE script and execute the given action
ide_script="$script_dir/$ide_script_name"
generate_ide_script > "$ide_script"
$ide ideScript "$ide_script"
cleanup
The code is also available on GitHub as a gist.
Demo: Launching the Help Menu programmatically
The Help Menu
action is invoked programmatically by clicking on a configured button on the Stream Deck.
View full size image here.
I hope others find this useful; now on to configuring the Stream Deck.