jaro is a just another resource opener. It runs the appropriate application to open a given file or URL based on given configurations.
Run jaro followed by the file path or URI you wish to open. If configured, jaro will select the appropriate application automatically:
jaro ~/document.pdf
jaro https://example.comjaro can also read from stdin.
echo "/path/to/file" | jaroYou can disable stdin feature with passing --no-stdin parameter.
General usage:
jaro [OPTIONS] <URI>-t,--mime-type: Print the MIME type of the URI.-c,--cold-run: Simulate the actions without executing them.-f,--binding-file=FILE: Use a specific binding configuration file (default:~/.config/associations).-m,--method=METHOD: Use a specific method to run the application.-N,--no-stdin: Do not read URI from standard input. This is helpful if you want to use jaro in a pipeline but you don't care about the results of the earlier steps.-h,--help: Display help information.
To use jaro, ensure you have Guile (>= 1.8) installed on your system. Place the jaro script in a directory that is in your system's PATH.
# Fedora
sudo dnf install guile
# Debian/Ubuntu etc.
sudo apt-get install guile-3.0
# Arch
sudo pacman -S guilecurl -L -o /usr/local/bin/jaro https://raw.githubusercontent.com/isamert/jaro/refs/heads/master/jaro
chmod +x /usr/local/bin/jaroFor enhanced mimetype detection, install Perl MimeInfo, otherwise jaro will fallback to standard file utility for mimetype detection, which is far more inferior. To install it:
# Fedora
sudo dnf install perl-File-MimeInfo
# Debian/Ubuntu etc.
sudo apt-get install libfile-mimeinfo-perl
# Arch
sudo pacman -S perl-file-mimeinfoI simply recommend replacing xdg-open with jaro so that all file/URL opening requests are redirected to jaro instead of xdg-open. Easiest way to do this would be shadowing the real xdg-open binary.
Create a symbolic link named xdg-open pointing to jaro in a directory that precedes the xdg-open binary's directory in the PATH. Assume jaro is located at /usr/local/bin/jaro and the actual xdg-open is at /usr/bin/xdg-open.
ln -s /usr/local/bin/jaro /usr/local/bin/xdg-openAssuming /usr/local/bin/ precedes /usr/bin in the PATH variable, you will successfully shadow xdg-open with jaro. This method is preferable to simply removing xdg-open and replacing it with jaro, as it prevents disruptions to system packages.
Check your PATH and ensure xdg-open points to jaro:
# Ensure that the xdg-open that you created is inside a folder that
# precedes the real xdg-open's directory:
echo $PATH
# Should output the link you created, instead of the real xdg-open:
which xdg-open
# Ensure that xdg-open link you've created points to jaro:
stat $(which xdg-open)jaro looks for the file ~/.config/associations and loads it. This file contains multiple (bind ...) definitions and arbitrary Scheme code. jaro will try to match the given URI with each association in order. I'll go trough some examples that shows you associating files/uris with programs.
(bind
#:pattern "image/.*"
#:program '(sxiv %f))Here is another example that opens all YouTube links with mpv:
(bind
#:pattern "^https?://(www.)?youtube.com/watch\\?.*v="
#:program '(mpv %f))Let's go back to first example, and make a small addition:
(bind
#:pattern "image/.*"
#:program '(sxiv %f)
#:gallery '(nomacs %f))When you run jaro an-image.png, this does exactly the same thing as the first binding. When you run jaro --method=gallery an-image.png however, instead of opening the image with sxiv, jaro uses nomacs now.
Some of the keywords (things that start with #:) have a reserved meaning in jaro. In addition to them, you can define arbitrary methods like the #:gallery example from above. The following keywords are reserved:
#:name: Assigns a unique identifier to a binding, which can be referenced by other bindings or methods.#:program: Specifies the command or application to be executed when the pattern matches. Can be a string, list, or Scheme procedure.#:pattern: Defines the regular expression(s) or list of expressions that determine which files or URIs the binding applies to. The match is done against the file/URI or the mimetype.#:test: An optional command or procedure to run before executing the main program. If this test passes, the main program is run; otherwise, it triggers the#:on-failmethod.#:on-fail: Specifies an alternative command or procedure to execute if the#:testfails.#:on-success: Defines a command or procedure to run if the#:programexecutes successfully.#:on-error: Specifies a command or procedure to execute if the#:programfails. This can also be set to 'continue' to try alternative bindings.
These are discussed in detail in Configuration Reference.
Here is a commented configuration that illustrates advanced features of jaro:
;; -*- mode: scheme; -*-
;;; Configuration
;; Optional, for dynamically selecting programs/methods on runtime:
(set!
dynamic-menu-program
(oscase
#:darwin "choose"
#:gnu/linux "rofi -dmenu"))
;; An example conditional runner definition for detecting the Kitty
;; terminal
(define-conditional-runner (kitty _)
(getenv "KITTY_PID"))
;;; Bindings
;; Open pdf/ps/epub etc. with zathura, based on mimetype
(bind
#:pattern '("(application|text)/(x-)?(pdf|postscript|ps|epub.*)" "image/(x-)?eps")
#:program '(zathura %f))
;; Open torrent files and magnet links using qBittorrent
(bind
#:pattern '("^magnet:" "\\.torrent$")
#:program '(qbittorrent --skip-dialog=false %f))
;; Open images using `imv` program
(bind
#:pattern "^image/.*"
;; imv does not directly load images of the directory, so we start
;; imv in the directory or our image and set the first image to the
;; image that we want to open
#:program '(imv -n %f %d)
;; If we are inside the Kitty terminal, simply use it's ability to
;; show images instead of using an external program
#:kitty '(kitty +kitten icat %f)
;; If the jaro is started with --method=gallery option, then defer
;; opening this file to nomacs definition down below
#:gallery 'nomacs)
;; Open a Zoom link. Extract the meeting number and password from the
;; link using regexp capture groups and feed it into the zoom app
;; using t %1 and %2
(bind
#:pattern "https://.*zoom\\.us/j/(\\w+)\\?pwd=(\\w+)"
#:program '(zoom zoommtg://zoom.us/join?confno=%1&pwd=%2))
(bind
#:pattern "https://.*zoom\\.us/j/(\\w+)\\?pwd=(\\w+)"
#:program '(zoom zoommtg://zoom.us/join?confno=%1&pwd=%2))
;; If a compressed file is opened with jaro, then display a menu using
;; rofi (on Linux) or choose (on MacOS) to as user what to do with
;; this file. See beginning of this example file for menu program
;; configuration.
(bind
;; Give this binding a name, which we will utilize later
#:name 'archive
#:pattern "^application/(x-)?(tar|gzip|bzip2|lzma|xz|compress|7z|rar|gtar|zip)(-compressed)?"
;; Instead of doing something directly, let user select one of the
;; methods (#:unpack, #:unpack-to-directory, #:view) of this binding.
#:program (select-one-of #:methods)
;; Unpack the archive using atool
#:unpack '(atool --extract %f)
;; Let user select a directory with `zenity` to extract the archive
;; into, using atool again
#:unpack-to-directory "atool --extract-to=$(zenity --file-selection --title='Choose a directory' --directory) %f"
;; Open the archive using `file-roller`.
#:view '(file-roller %f))
(bind
;; Given a jar or apk file...
#:pattern ".(jar|apk)$"
;; ...show a menu of: run, archive.unpack, archive.unpack-to-directory
#:program (select-one-of #:methods 'archive.view 'archive.unpack 'archive.unpack-to-directory)
;; ^^ #:methods refers the methods of this binding. There is only one: "run"
;; ^^ 'archive.<method> refers to the methods of 'archive binding.
;; Here, instead of directly running an external command we use the
;; "program" syntax. It simply let's us run arbitrary Guile scheme
;; code. Inside "program", the variables %1 %2 %3... etc are bound to
;; the capture groups from the #:pattern and the "run" let's you run
;; external programs using the syntax that you are familiar from the
;; earlier bindings.
#:run (program
(match %1
["jar" (run (java -jar %f))]
["apk" (run (notify-send "Can't run APK files. Install an Android Emulator?"))])))
;; A named binding, referenced above
(bind
#:name 'nomacs
#:pattern "^image/.*"
#:program '(nomacs %f))Defines regular expressions to match against URIs or MIME types. Can be:
- Single regex string (
"image/.*") - Compiled regex object
- List of patterns (
'("\.txt$" "text/.*"))
Patterns are checked against both the input URI and its detected MIME type. Capture groups can be referenced in commands using %1, %2, etc.
Example:
(bind
#:pattern "^https://example.com/(\\w+)/"
#:program '(open-section %1)) ; Capture path componentThe primary command to execute when the pattern matches. Can be:
- String (
"sxiv %f") - List of arguments (
'(sxiv %f)) - Scheme procedure
- Reference to another binding (
'nomacs)
Additional methods can be defined as arbitrary keywords (e.g., #:gallery) for alternative opening modes. These are invoked with --method=METHOD.
Example:
(bind
#:pattern "\\.md$"
#:program '(glow %f) ; Default method
#:preview '(mdcat %f)) ; Custom preview modeAssigns a unique identifier to a binding for cross-referencing. Named bindings can be invoked using:
(bind #:pattern ... #:program 'named-binding)or reference specific methods:
(bind #:pattern ... #:program 'named-binding.method)Example:
(bind
#:name 'pdf-viewer
#:pattern "\\.pdf$"
#:program '(zathura %f)
#:edit '(xournalpp %f))
(bind
#:pattern "\\.ps$"
#:program 'pdf-viewer
#:edit 'pdf-viewer.edit)Optional precondition check that must succeed before running the main program. If the test fails, triggers #:on-fail:
Example:
(bind
#:name 'browser
#:pattern '("^https?://.*" "^.*\\.html?(#[\\w_-]+)?")
#:test '(pgrep qutebrowser) ; Check if qutebrowser is running or not
#:program '(qutebrowser %f) ; If it's running, open the url with it
#:on-fail '(firefox %f)) ; If not, fallback to FirefoxSpecifies fallback behavior when the main program fails. Special values:
'continue: Try subsequent bindings- Procedure or command list: Execute custom error handling
Example:
(bind
#:pattern "\\.mkv$"
#:program '(mpv --hwdec %f)
#:on-error '(vlc %f)) ; Fallback playerRuns after successful execution of the main program. Useful for cleanup or notifications:
Example:
(bind
#:pattern "\\.enc$"
#:program "decrypt-file %f"
#:on-success "rm %f.enc") ; Cleanup after successBoolean flag (default: #f) that when true, continues to subsequent bindings after any error in the current binding.
Example:
(bind
#:pattern "\\.jpg$"
#:program "non-existing-program %f" ; This binding will fail because the program does not exist.
#:continue-on-error #t)
;; The next matching pattern will be used.
(bind
#:pattern "image/*"
#:program "imv %f")Define custom methods for context-specific opening:
(bind
#:pattern "image/.*"
#:program '(imv %f)
#:edit '(gimp %f))and run this:
jaro --method=edit photo.jpgAlso consider aliasing your common use-cases:
alias open="jaro"
alias edit="jaro --method=edit"
alias view="jaro --method=view"
alias gallery="jaro --method=gallery"Now you can do the following instead:
edit photo.jpg
view photo.jpg
open photo.jpgAutomatically select methods based on runtime environment using define-conditional-runner:
Built-in conditionals: emacs, tmux, term, vim. Conditional runners are already defined for these environments but you can override them as well.
(define-conditional-runner (kitty _)
(getenv "KITTY_PID"))
;; Open images using `imv` program
(bind
#:pattern "^image/.*"
#:program '(imv %f)
;; If we are inside the Kitty terminal, simply use it's ability to
;; show images instead of using an external program.
;; #:kitty keyword is introduced by the define-conditional-runner
;; call above
#:kitty '(kitty +kitten icat %f))Interactive selection menu for multiple options. Supported selectors:
#:methods: Current binding's own methods#:alternatives: MIME type associations from system#:binaries: Installed system commands/binaries#:bindings: All named bindings'<binding-name>.<method-name>: Direct reference to methods of other bindings.
Example offering extraction options for archives:
;; For select-one-of to work, you need to set the dynamic-menu-program
;; Here we use choose[1] for macOS, rofi[2] for GNU/Linux.
;;
;; [1]: https://github.com/chipsenkbeil/choose
;; [2]: https://github.com/davatorium/rofi
(set!
dynamic-menu-program
(oscase
#:darwin "choose"
#:gnu/linux "rofi -dmenu"))
(bind
#:pattern "\\.tar\\..*"
;; Following shows a menu consisting of following items:
;; - view
;; - extract
;; - ...all other programs that can open a tar files in your system
#:program (select-one-of #:methods #:alternatives)
;; Runs when "view" is selected
#:view '(tar tvf %f)
;; Runs when "extract" is selected
#:extract '(tar xvf %f))(open-with 'binding-name): Reference other bindings(program ...): Scheme procedure wrapper with access to:$input: Original URI$mimetype: Detected type$matches: Regex capture groups%1-%5: Individual capture groups(run (...)): Program runner. It takes a parameter like what you supply to#:programand runs it.
;; open-with
(bind
#:pattern "\\.txt$"
#:program (open-with 'editor)) ; Delegate to 'editor binding
(bind
#:name 'editor
#:pattern "^text/"
#:term '(vim %f))
;; program
(bind
#:pattern "https://github.com/([^/]+)/([^/]+)/?"
#:program (program
(if (string-suffix? ".git" $input)
(run (git clone %f)) ; Clone the repo if the URL ends with .git
(run (xdg-open %f))))) ; Open it otherwiseExecute Emacs Lisp code directly through emacsclient, using the elisp form:
(bind
#:pattern "\\.org$"
#:program (elisp (find-file "%F")))Supports all URI formatting placeholders (%f, %F, %U).
Contributions are welcome! Please make sure to include tests with any major changes.
While developing, I tend use the run-tests.sh script to continuously execute my tests upon file modifications; they run fairly quickly. You need the entr utility for this script to function. Alternatively you can simply use guile tests.scm.