Artur Dumchev
4 Jan 2023
•
7 min read
If you are looking for an alternative to bash scripts and already know Clojure, consider using it for your scripting needs. While the Babashka library is a popular choice, there are other options available as well.
Sometimes, we don't want to worry about the command line API. For example, to begin working on my current project, I need to do three things first. Open the Docker application and wait until it's ready. Press cmd+space
and type "Terminal" to open a terminal emulator. Then, go to the path where the project is located with cd <path>
. Here, I want to run docker-compose up
. Next, I press cmd+t
to open a new tab and run lein repl
in it.
I know how to do these steps manually, but I don't know how to do it with bash. Using the robot library, we'll show you how to create a desktop script that completes these tasks by pressing keys. Basically, we'll implement it in three steps:
Before we can begin writing our script, we need to install leiningen
and java
in order to create a project and run jars:
brew install leiningen
brew install java
If you have Docker installed, you can use it. If not, you can simulate the work using the Thread/sleep
function.
Next, we will create a project template using the following command:
lein new app working
Now we will add the cli
and robot
dependencies to our project.clj
file:
(defproject working "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.11.1"]
[robot/robot "0.2.1-SNAPSHOT"] ;; new one
[org.clojure/tools.cli "1.0.206"] ;; new one
]
:main ^:skip-aot working.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
Next, we will update the working.core
source file with the following code:
(ns working.core
(:require
[clojure.tools.cli :as cli])
(:gen-class))
(def cli-options
[["-d" "--directory PATH" "Path to directory with the project"
:default "~/IdeaProjects/work/server/"
:parse-fn str]])
(defn -main [& args]
(let [{{d :directory} :options} (cli/parse-opts args cli-options)]
(println "Dir is" d)))
Now we can build a jar
file and run it to ensure everything is working properly:
lein uberjar
java -jar target/uberjar/working-0.1.0-SNAPSHOT-standalone.jar -d "path"
You should see the output "Dir is path".
To continue working on our script, we will start a REPL by running the following command:
lein repl
If you don't know how to make a REPL connection, consider checking out the Clojure docs on the subject. The Show me your REPL video series is a good place for ideas and inspiration.
After establishing a connection with the REPL, evaluate the namespace working.core
.
Our first task is to ensure that the docker process is running. To do this, we will add the clojure.java.shell
dependency and create a cmd!
function that allows us to run shell commands. We can then use this function to check that we are able to perform actions like ls
:
(ns working.core
(:require
[clojure.tools.cli :as cli]
[clojure.java.shell :refer [sh]]) ;; new one
(:gen-class))
(defn cmd! [cmd] (sh "sh" "-c" cmd))
(comment
(sh "ls") ;=>
{:exit 0,
:out
"CHANGELOG.md\nLICENSE\nREADME.md\ndoc\nproject.clj\nresources\nsrc\ntarget\ntest\n",
:err ""})
With the cmd!
, we can now create a function that opens the docker process and waits until it is ready:
(defn open-docker! []
(cmd! "open -a Docker")
(while (-> (cmd! "docker stats --no-stream") :out empty?)
(Thread/sleep 2000)))
If you do not have Docker installed, you can use the Thread/sleep
function to simulate the work. For example, you could replace the open-docker!
function with the following:
(defn open-docker! []
(Thread/sleep 2000))
This will pause the script for 2 seconds, simulating the time it would take to start the docker process.
Next, we will open a terminal application and run the docker-compose up
command. I am using WezTerm
as my terminal client, but you can use any terminal application that you prefer.
(defn open-terminal! []
(cmd! "open /Applications/WezTerm.app"))
Or using the robot
library:
(ns working.core
(:require
[clojure.tools.cli :as cli]
[robot.core :as robot] ;; new one
[clojure.java.shell :refer [sh]])
(:gen-class))
(defn open-terminal! []
(robot/hot-keys! [:cmd :space]) ;; to open spotlight or alfred
(robot/sleep 100)
(robot/type-text! "WezTerm")
(robot/sleep 100)
(robot/type! :enter))
This code will press the cmd and space keys to open the spotlight or Alfred search function, pause for 100 milliseconds, type the text "WezTerm" to search for the terminal application, and press the enter key to launch it.
To open the terminal, we will use the first variant of the open-terminal!
function. To simplify the process of pasting text, we will add another function.
(defn paste! [s]
(robot/clipboard-put! s)
(robot/hot-keys! [:cmd :v])
(robot/sleep 200)
(robot/type! :enter))
With these functions, we can define the run-docker-compose!
function that opens a terminal, switches to a new tab, and runs the docker-compose up command:
(defn run-docker-compose! [path]
(open-terminal!)
(robot/sleep 200)
(robot/hot-keys! [:cmd :t]) ;; be sure to open new tab
(robot/sleep 200)
(paste! (str "cd " path))
(robot/sleep 200)
(paste! "docker-compose up"))
We can also create the run-repl-in-new-tab!
function to open a new tab and run the REPL command:
(defn run-repl-in-new-tab! [path]
(robot/sleep 100)
(robot/hot-keys! [:cmd :t])
(robot/sleep 100)
(paste! (str "cd " path)) ;; if you terminal opens a new tab in $HOME
(robot/sleep 100)
(paste! "lein with-profile +test repl"))
To test the run-repl-in-new-tab!
function, you can use the following code in the REPL:
(do
(cmd! "open /Applications/WezTerm.app")
(run-repl-in-new-tab!))
Finally, the main function to run all the tasks will look like this:
(defn -main [& args]
(let [{{dir :directory} :options} (cli/parse-opts args cli-options)]
(println "Starting docker process")
(open-docker!)
(println "Running docker-compose in" dir)
(run-docker-compose! dir)
(println "Starting lein repl")
(run-repl-in-new-tab! dir)))
This function will parse the command line arguments to get the working directory, start the docker process, run the docker-compose up command in a new terminal tab, and start the lein repl
in another new tab.
To build and run the script, you can use the following commands:
lein uberjar
java -jar target/uberjar/working-0.1.0-SNAPSHOT-standalone.jar -d ~/IdeaProjects/work/server/
Once we have built the jar file, we can bind it to a hotkey using a tool like skhd. For example, I will bind it to ctrl+alt+1
by adding the following line to my skhd configuration:
ctrl + alt - 1 : java -jar ~/IdeaProjects/clojure/working/target/uberjar/working-0.1.0-SNAPSHOT-standalone.jar -p ~/IdeaProjects/work/server/
This allows me to start everything I need for my work with a single keystroke. Here is a demonstration of the script in action (when I press ctrl+alt+1
):
Pros:
Cons:
robot/sleep
calls between commands, we may encounter problems as the keys may be pressed faster than the desktop UI can respond.To target both OSX and Linux (with Gnome Terminal support), the bash script would look something like:
#!/usr/bin/env bash
# Set the terminal emulator to use (default is Terminal on macOS, Gnome Terminal on Linux)
if [[ "$OSTYPE" == "darwin"* ]]; then
TERMINAL=${2:-Terminal}
else
TERMINAL=${2:-gnome-terminal}
fi
# Start the docker process
open -a Docker
# Wait until docker is running
while ! docker stats --no-stream; do
sleep 2
done
# Open $2 terminal emulator, go to $1 path, launch docker-compose
if [[ "$OSTYPE" == "darwin"* ]]; then
open -a "$TERMINAL"
osascript -e "tell application \"$TERMINAL\" to activate"
osascript -e 'tell application "System Events" to keystroke "t" using command down'
osascript -e "tell application \"System Events\" to keystroke \"cd $1\""
osascript -e 'tell application "System Events" to keystroke return'
osascript -e 'tell application "System Events" to keystroke "docker-compose up"'
osascript -e 'tell application "System Events" to keystroke return'
else
$TERMINAL --tab --working-directory="$1" -e "docker-compose up"
fi
# Open another tab in the terminal emulator and run lein repl in the specified directory
if [[ "$OSTYPE" == "darwin"* ]]; then
osascript -e "tell application \"$TERMINAL\" to activate"
osascript -e 'tell application "System Events" to keystroke "t" using command down'
osascript -e "tell application \"System Events\" to keystroke \"cd $1\""
osascript -e 'tell application "System Events" to keystroke return'
osascript -e 'tell application "System Events" to keystroke "lein with-profile +test repl"'
osascript -e 'tell application "System Events" to keystroke return'
else
$TERMINAL --tab -e "bash -c 'cd $1; lein with-profile +test repl'"
fi
Clojure allows us to easily write scripts to manipulate the desktop using the same keys and applications that we would use manually. You have access to a wide range of tools and libraries that make it easy to implement such scripts quickly. If you are already familiar with Clojure, you can avoid having to learn Bash and leverage your existing knowledge.
However, it is worth noting that there may be issues to consider such as startup time and script size. Regardless, using Clojure with robot
for desktop scripting can be a convenient solution, that would be reusable on all systems that support Java.
However, it is worth noting that there may be issues to consider, such as startup time and script size. Regardless, using Clojure with robot for desktop scripting is a convenient solution that can be reused on all systems that support Java.
You may find the whole code here.
I would like to thank OpenAI's for the assistance in generating some of the content for this article.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!