Build anything you can imagine and launch it with a keystroke. Martillo is a powerful actions launcher for macOS powered by Hammerspoon. Create custom actions, automate repetitive tasks, and access everything through a command palette with fuzzy search.
Your productivity hub, your way. An open-source alternative to Raycast and Alfred with no vendor lock-in, zero dependencies, and full configuration through a single Lua file. Write your own actions, use ready-made bundles, customize every keybinding, add aliases for lightning-fast access, and share your creations freely. All configuration lives in a single file, inspired by lazy.nvim's declarative plugin system.
Screen.Recording.2025-11-14.at.06.41.37.mp4
- Fast & Lightweight: Pure Lua, zero dependencies, no compilation needed
- Command Palette: Searchable actions with fuzzy search
- App Launcher: Quick app switching with single hotkeys
- Browser Routing: Smart URL routing to different browsers
- My Schedule: Lists today’s upcoming events in your menu bar, click any entry to jump into the meeting
- Clipboard Manager: History of your text, images, and files with fuzzy search
- Store: Install external actions from any Git repo via the built-in store loader, drop in community actions freely
- Batteries Included:
libhelpers (icons, events, search, chooser, leader) make building your own actions easy and dependency-free
Every action below ships with Martillo and is available through ActionsLauncher (search, bind hotkeys, or set aliases).
| Category | Action ID | What it does | Enter / ⇧Enter | Search & notes |
|---|---|---|---|---|
| Window | switch_window | Switch to any open window with fuzzy search | Enter: focus window ⇧Enter: same |
Child chooser with fuzzy search Shows app icons and titles |
| Window | window_maximize | Maximize window to full screen | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_almost_maximize | Resize to 90% of screen, centered | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_reasonable_size | Resize to ~70% of screen, centered | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_center | Center window without resizing | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_left | Left half | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_right | Right half | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_up | Top half | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_down | Bottom half | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_top_left | Top-left quarter | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_top_right | Top-right quarter | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_bottom_left | Bottom-left quarter | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_bottom_right | Bottom-right quarter | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_left_third | Left third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_center_third | Center third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_right_third | Right third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_left_two_thirds | Left two thirds | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_right_two_thirds | Right two thirds | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_top_third | Top third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_middle_third | Middle third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_bottom_third | Bottom third | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_top_two_thirds | Top two thirds | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_bottom_two_thirds | Bottom two thirds | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_left_fourth | Left fourth | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_center_left_fourth | Center-left fourth | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_center_right_fourth | Center-right fourth | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_right_fourth | Right fourth | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_left_three_fourths | Left three-fourths | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_right_three_fourths | Right three-fourths | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_maximize_horizontal | Maximize width (keep height and position) | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Window | window_maximize_vertical | Maximize height (keep width and position) | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| System | toggle_caffeinate | Toggle system sleep prevention | Enter: toggle ⇧Enter: same |
Global fuzzy search Direct action |
| System | toggle_system_appearance | Toggle Dark/Light Mode | Enter: toggle ⇧Enter: same |
Global fuzzy search Direct action |
| System | system_information | Live CPU/memory/battery/network stats with auto-refresh | Enter: copy field ⇧Enter: same |
Child chooser with fuzzy search on fields |
| Utilities | generate_uuid | Generate UUID v4 and copy | Enter: copy ⇧Enter: same |
Global fuzzy search Direct action |
| Utilities | word_count | Live counts for characters/words/sentences/paragraphs | Enter: copy metric ⇧Enter: same |
Child chooser Type/paste text to update results (no fuzzy search) |
| Converter | converter_time | Convert Unix/ISO/RFC2822/relative times | Enter: copy selected conversion ⇧Enter: same |
Child chooser Type any time value to populate results |
| Converter | converter_colors | HEX ↔ RGB with color preview | Enter: copy selected conversion ⇧Enter: same |
Child chooser Type/paste a color; results update live |
| Converter | converter_base64 | Base64 encode/decode | Enter: copy encoded/decoded value ⇧Enter: same |
Child chooser Type/paste text to see both directions |
| Converter | converter_jwt | Decode JWT header/payload | Enter: copy decoded part ⇧Enter: same |
Child chooser Paste JWT to inspect sections |
| Keyboard | keyboard_lock | Lock keyboard for cleaning (unlock with <leader>+Enter) |
Enter/⇧Enter: instruction only Unlock via leader+Enter |
Child chooser shows unlock hint Keyboard input blocked |
| Keyboard | keyboard_keep_alive | Toggle F15 keep-alive ping | Enter: toggle ⇧Enter: same |
Global fuzzy search Direct action |
| Clipboard | clipboard_history | Persistent clipboard with fuzzy search | Enter: paste (or copy-only in secure apps) ⇧Enter: copy |
Child chooser with fuzzy search Supports text/images/files |
| Processes | kill_process | Manage running processes with CPU/mem display | Enter: kill ⇧Enter: copy PID |
Child chooser with fuzzy search Auto-refreshes process list |
| Network | network_ip_geolocation | Show local/public IP, geolocation, ISP | Enter: copy selected value ⇧Enter: same |
Child chooser with fuzzy search Auto-fetches and refreshes |
| Network | network_connected_devices | Scan LAN (/24), show IP/MAC/vendor | Enter: copy IP ⇧Enter: copy MAC |
Child chooser with fuzzy search Vendor lookup + caching |
| Network | network_speed_test | Latency/download/upload test | Enter/⇧Enter: display only | Child chooser with fuzzy search Live-updating results |
| Browser | safari_tabs | Switch Safari tabs across windows | Enter: focus tab ⇧Enter: copy URL |
Child chooser with fuzzy search Shows favicons |
| Screen | screen_confetti | Confetti celebration animation | Enter: run ⇧Enter: same |
Global fuzzy search Direct action |
| Screen | screen_ruler | On-screen ruler overlay | Enter: show ruler ⇧Enter: same |
Global fuzzy search Direct action |
| Martillo | martillo_reload | Reload Martillo/Hammerspoon config | Enter: reload ⇧Enter: same |
Global fuzzy search Direct action |
| Martillo | martillo_update | Pull latest Martillo changes from git | Enter: update then reload ⇧Enter: same |
Global fuzzy search Direct action |
| Store | f1_standings | View current F1 driver standings | Enter: copy driver/team summary ⇧Enter: same |
Child chooser with fuzzy search (by name, team, nationality) |
| Store | idonthavespotify | Convert music links across platforms | Enter: open target link ⇧Enter: copy link |
Child chooser (no fuzzy search) Populates from clipboard link |
# Install Hammerspoon
brew install --cask hammerspoon
# Clone Martillo
git clone https://github.com/sjdonado/martillo ~/.martillo
# Create configuration
cat > ~/.hammerspoon/init.lua << 'EOF'
package.path = package.path .. ";" .. os.getenv("HOME") .. "/.martillo/?.lua"
return require("martillo").setup({
leader_key = { "alt", "ctrl" },
{
"ActionsLauncher",
actions = {
{ "toggle_system_appearance", alias = "ta" },
{ "toggle_caffeinate", alias = "tc" },
{ "system_information", alias = "si" },
{ "screen_ruler", alias = "ru" },
{ "switch_window", keys = { { "<leader>", "tab" } }, opts = { success_toast = false } },
{ "window_maximize", alias = "wm" },
{ "window_almost_maximize", keys = { { "<leader>", "up" } } },
{ "window_reasonable_size", keys = { { "<leader>", "down" } } },
{ "window_center", keys = { { "<leader>", "return" } } },
{ "window_left_two_thirds", keys = { { "<leader>", "left" } } },
{ "window_right_two_thirds", keys = { { "<leader>", "right" } } },
{ "clipboard_history", keys = { { "<leader>", "-" } } },
{ "kill_process", keys = { { "<leader>", "=" } } },
{ "safari_tabs", keys = { { "alt", "tab" } } },
{ "generate_uuid", alias = "gu" },
{ "word_count", alias = "wc" },
{ "converter_time", alias = "ct" },
{ "converter_colors", alias = "cc" },
{ "converter_base64", alias = "cb" },
{ "converter_jwt", alias = "cj" },
{ "network_ip_geolocation", alias = "ni" },
{ "network_speed_test", alias = "ns" },
{ "keyboard_lock", alias = "kl" },
{ "keyboard_keep_alive", alias = "ka" },
{ "screen_confetti", alias = "cf" },
{ "f1_standings", alias = "f1" },
{ "martillo_reload", alias = "mr" },
{ "martillo_update", alias = "mu" },
},
keys = {
{ "<leader>", "space" },
},
},
{
"LaunchOrToggleFocus",
keys = {
{ "<leader>", "c", app = "Calendar" },
{ "<leader>", "f", app = "Finder" },
{ "<leader>", ";", app = "Ghostty" },
{ "<leader>", "h", app = "Helium" },
{ "<leader>", "l", app = "Music" },
{ "<leader>", "e", app = "Mail" },
{ "<leader>", "m", app = "Messages" },
{ "<leader>", "n", app = "Notes" },
{ "<leader>", "p", app = "Passwords" },
{ "<leader>", "r", app = "Reminders" },
{ "<leader>", "b", app = "Safari" },
{ "<leader>", "s", app = "Slack" },
{ "<leader>", "t", app = "Kagi Translate" },
{ "<leader>", "y", app = "Yaak" },
},
},
{
"MySchedule",
config = function(spoon)
spoon:compile()
spoon:start()
end,
},
{
"BrowserRedirect",
opts = {
default_app = "Safari",
redirect = {
{ match = { "*localhost*", "*127.0.0.1*", "*0.0.0.0*" }, app = "Helium" },
{ match = { "*fly.dev*" }, app = "Helium" },
{ match = { "*meet.google*" }, app = "Helium" },
},
},
config = function(spoon)
spoon:start()
end,
},
})
EOF
# Reload HammerspoonThe System Information action works out of the box and displays:
- Memory (App Memory + Wired + Compressed)
- Battery status and percentage
- Network upload/download speeds
- Uptime with boot date
For advanced metrics (CPU cluster residency, GPU, Thermal, and Power), configure passwordless sudo for powermetrics:
echo "$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/powermetrics" | sudo tee /private/etc/sudoers.d/powermetrics
sudo chmod 0440 /private/etc/sudoers.d/powermetricsThen add the include directive to your sudoers file:
sudo visudoAdd this line at the end:
#includedir /private/etc/sudoers.d
Note: CPU, GPU, Thermal, and Power metrics all require sudo access to powermetrics. Without it, these metrics will show "Loading..." but Memory, Battery, Network, and Uptime will work normally.
The store/ directory is for custom actions that extend Martillo. All actions in store/ are automatically loaded - just drop a new folder with an init.lua file and it's ready to use!
Example Store Structure:
store/
f1_standings/
init.lua # F1 Drivers Championship standings (included example)
my_action/
init.lua # Your custom action module
icon.png # Optional custom icon (overrides default icons)
Each action module should return an array of actions, just like bundles. Custom icons placed in store folders will override default icons with the same name.
See store/README.md for details on creating custom actions.
Martillo includes a built-in CLI for managing external store actions. The CLI uses Git sparse checkout to fetch only the specific action folder you need, without downloading entire repositories.
Add an external action:
./scripts/store-cli.sh add <github-url>
# Example: Install I Don't Have Spotify action
./scripts/store-cli.sh add https://github.com/sjdonado/idonthavespotify/tree/master/extra/martilloList installed actions:
./scripts/store-cli.sh listUpdate an action:
./scripts/store-cli.sh update idonthavespotify
# Update all actions
./scripts/store-cli.sh updateRemove an action:
./scripts/store-cli.sh remove idonthavespotify- Lock File: All installations are tracked in
store.lock.jsonwith commit hashes for version control - Sparse Checkout: Only downloads the specific folder you need (efficient!)
- Automatic Loading: Installed actions are automatically available in ActionsLauncher
- Safe Updates: Detects upstream changes by comparing commit hashes
After installation, add the action to your config:
{
"ActionsLauncher",
actions = {
{ "idonthavespotify", alias = "idhs" }, -- Automatically loaded!
},
}To make your action installable via the Store CLI, create a folder structure in your repository:
your-repo/
extra/martillo/ # Or any path ending with /martillo
init.lua # Action definition (required)
icon.png # Optional icon
Users can then install it with:
./scripts/store-cli.sh add https://github.com/you/your-repo/tree/main/extra/martilloAvailable Community Actions:
- idonthavespotify - Convert music links across streaming platforms
return require("martillo").setup({
-- Global configuration
leader_key = { "alt", "ctrl" }, -- Expand <leader> in keybindings
alertDuration = 1, -- Alert duration in seconds
-- Spoons go here...
})Use <leader> as a placeholder in any keybinding. It expands to your configured leader_key:
{ "<leader>", "space" } -- Expands to { "alt", "ctrl", "space" }
{ { "<leader>", "cmd" }, "p" } -- Expands to { "alt", "ctrl", "cmd", "p" }The central command palette with all your actions. Martillo automatically loads all built-in bundles and store actions:
{
"ActionsLauncher",
actions = {
-- Assign keybindings and aliases to specific actions
{ "window_center", keys = { { "<leader>", "return" } } },
{ "toggle_caffeinate", alias = "tc" },
{ "clipboard_history", keys = { { "<leader>", "v" } } },
{ "f1_standings", alias = "f1" }, -- From store/
},
keys = {
{ "<leader>", "space", desc = "Toggle Actions Launcher" }
}
}Action Fields:
keys- Keybindings for direct accessalias- Short name for faster fuzzy searchdesc- Optional description for the keybindingopts- Action-specific options (e.g.,{ success_toast = false }to disable success toasts)
Note: All bundles from bundle/ and custom actions from store/ are automatically loaded. You don't need to manually import them!
bundle.window- Window positioning (halves, quarters, thirds, maximize, center) - 26 actions totalbundle.switch_window- Window switcher with fuzzy searchbundle.system- System management (caffeinate, dark mode, system information)bundle.utilities- Text utilities (UUID generation, word count)bundle.converter- Converters (time, colors, base64, JWT)bundle.keyboard- Keyboard actions (lock, keep-alive)bundle.clipboard_history- Clipboard manager with historybundle.kill_process- Process killer with fuzzy searchbundle.network- Network utilities (IP geolocation, speed test)bundle.safari_tabs- Safari tab switcherbundle.screen- Screen effects (confetti, ruler)bundle.martillo- Martillo management (reload, update)
Quick app switching with single hotkeys:
{
"LaunchOrToggleFocus",
keys = {
{ "<leader>", "b", app = "Safari" },
{ "<leader>", ";", app = "Ghostty" },
{ "<leader>", "c", app = "Calendar" },
{ "<leader>", "e", app = "Mail" },
}
}Calendar integration in menu bar:
{
"MySchedule",
config = function(spoon)
spoon:compile()
spoon:start()
end
}Smart URL routing to different browsers:
{
"BrowserRedirect",
opts = {
default_app = "Safari",
redirect = {
{ match = { "*localhost*", "*127.0.0.1*" }, app = "Chromium" },
{ match = { "*meet.google*" }, app = "Google Meet" },
},
mapper = {
{
name = "googleToKagi",
from = "*google.com*/search*",
to = "https://kagi.com/search?q={query.q|encode}"
}
}
},
config = function(spoon)
spoon:start()
end
}- Search: Type to fuzzy search actions and aliases
- Enter: Execute selected action
- ESC: Close launcher
- Shift+ESC: Force close (same as ESC for main launcher)
- Type: Filter results in real-time
- Enter: Execute action (paste, convert, etc.)
- Shift+Enter: Alternate action (copy only, etc.)
- ESC: Navigate back to parent chooser
- Shift+ESC: Close all choosers and return to desktop
- Click outside: Close all choosers and return to desktop
Create a new file in the bundle/ directory for core actions:
-- bundle/my_custom_bundle.lua
-- My Custom Actions Bundle
local icons = require 'lib.icons'
return {
{
id = 'my_action',
name = 'My Action',
icon = icons.preset.star,
description = 'Does something awesome',
handler = function()
hs.alert.show('Hello from my action!')
end,
},
}Create a new folder in store/ with an init.lua file for custom actions:
-- store/my_action/init.lua
local icons = require 'lib.icons'
return {
{
id = 'my_store_action',
name = 'My Store Action',
icon = icons.preset.message,
description = 'Custom action from store',
handler = function()
-- Your action logic here
end,
},
}That's it! The store auto-loader will automatically discover and load your action. No need to manually import it - just drop a new folder in store/ and use it in your config:
{
"ActionsLauncher",
actions = {
{ "my_store_action", alias = "ma" }, -- Automatically available!
},
}Contributions welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create feature branch (
git checkout -b feature/AmazingFeature) - Commit changes (
git commit -m 'Add AmazingFeature') - Push to branch (
git push origin feature/AmazingFeature) - Open Pull Request
- Raycast - Inspiration for the launcher UX and workflow philosophy
- Hammerspoon - The powerful macOS automation tool that makes this possible
- lazy.nvim - Inspiration for declarative config style
- 3dicons.co - Beautiful 3D icon collection