OMC (OnMyCommand) is a low-code macOS app development engine that lets you create standalone applets with custom UI and shell/Python scripting. This guide covers the fundamentals of building an OMC applet from scratch.
- Applet: A standalone mini-app (
.appbundle) built on Abracode.framework - Command: An action handler defined in
Command.plistthat executes a script - Command Group: Multiple commands sharing the same
NAMEwith uniqueCOMMAND_IDs - NIB: Interface Builder file containing window/layout definitions
- Subcommand: A command triggered by UI events (button click, dialog init, table selection)
- OMC Release: Download from https://github.com/abra-code/OMC/releases
- Xcode: Required for building applets and editing nibs
- Git: For version control (recommended)
For AI Agents: To check the latest OMC release:
# Fetch latest releases from GitHub API curl -s "https://api.github.com/repos/abra-code/OMC/releases?per_page=3" # Filter for first non-draft, non-prerelease release # Download URL: https://github.com/abra-code/OMC/releases/download/vX.X.X/OMC_X.X.X.zipCompare the latest version to local OMC version if given access to, e.g.:
~/Downloads/OMC_X.X.X/
| Template | Description | Use When |
|---|---|---|
OMCApplet.app |
Base template, minimal size | Shell scripts only, no Python needed |
OMCPythonApplet.app |
Includes embedded Python 3 | Need Python modules (e.g., watchdog) |
For AI Agents: Always use the provided scripts to create applets:
build_applet.sh- Creates applet from template, handles renaming, Info.plist updatescodesign_applet.sh- Signs the applet after modificationsDo NOT manually copy files from the template - let the scripts handle it.
cd ~/Downloads/OMC_4.4.1/
./Scripts/build_applet.sh \
--omc-applet="Products/Applications/OMCApplet.app" \
--icon="MyApp/Icon/MyIcon.icon" \
--bundle-id=com.example.myapp \
--creator=MApp \
MyApp/MyApp.app./Scripts/codesign_applet.sh "MyApp/MyApp.app"MyApp.app/
├── Contents/
│ ├── Frameworks/Abracode.framework (OMC engine)
│ ├── MacOS/MyApp (app binary)
│ ├── Resources/
│ │ ├── Command.plist (command definitions)
│ │ ├── Scripts/ (action handler scripts)
│ │ └── Base.lproj/
│ │ └── *.nib (window definitions)
│ └── _CodeSignature/ (codesigning)
└── Library/ (Python if OMCPythonApplet)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
<plist version="1.0">
<dict>
<key>COMMAND_LIST</key>
<array>
<!-- Commands go here -->
</array>
<key>VERSION</key>
<integer>2</integer>
</dict>
</plist><dict>
<key>NAME</key>
<string>MyApp</string> <!-- Menu label -->
<key>COMMAND_ID</key>
<string>myapp.mycommand</string> <!-- Unique identifier for subcommands -->
</dict>Main Command: The first command in COMMAND_LIST (without COMMAND_ID) is the main command. It is executed when files/folders are dropped on the applet. Subcommands have unique COMMAND_ID values.
For AI Agents: Understanding command structure is critical:
- The main command (first in list, no
COMMAND_ID) handles file/folder drops on the app- Subcommands have
COMMAND_IDand are triggered by UI events (buttons, table selection)- Multiple commands with same
NAMEbut differentCOMMAND_IDform a command group
| Mode | Description |
|---|---|
exe_script_file |
Executes script file from Scripts/ directory |
exe_shell_script |
Executes inline shell command |
exe_script_file_with_output_window |
Script + visible output pane |
exe_system |
Synchronous, no env vars (use __FOO__) |
exe_applescript |
AppleScript execution |
| Mode | Description |
|---|---|
act_always |
Always available |
act_file |
File(s) selected/dropped |
act_folder |
Folder(s) |
act_file_or_folder |
Either |
<dict>
<key>NAME</key>
<string>MyApp</string> <!-- No COMMAND_ID = main command -->
<key>ACTIVATION_MODE</key>
<string>act_folder</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
</dict>Scripts: Scripts/MyApp.main.sh
Scripts for exe_script_file execution mode live in MyApp.app/Contents/Resources/Scripts/ and are named based on command type:
| Command Type | Script Location | Naming Convention |
|---|---|---|
| Main command | Scripts/ |
<NAME>.main.<ext> (e.g., MyApp.main.sh) |
| Subcommand | Scripts/ |
<NAME>.<COMMAND_ID>.<ext> (e.g., MyApp.mycommand.sh) |
| Extension | Interpreter |
|---|---|
.sh |
/bin/sh |
.py |
/usr/bin/python3 |
.pl |
/usr/bin/perl |
.applescript, .scpt |
/usr/bin/osascript |
.zsh |
/bin/zsh |
.bash |
/bin/bash |
.csh |
/bin/csh |
.tcsh |
/bin/tcsh |
.dash |
/bin/dash |
.rb |
/usr/bin/ruby |
.js, .mjs |
JavaScriptCore (macOS 11+) |
| Variable | Description |
|---|---|
$OMC_OBJ_PATH |
Selected file/folder path |
$OMC_OBJ_NAME |
Base name |
$OMC_APP_BUNDLE_PATH |
Applet bundle path |
$OMC_OMC_SUPPORT_PATH |
OMC support tools |
$OMC_NIB_DLG_GUID |
Dialog instance ID |
$OMC_CURRENT_COMMAND_GUID |
Command execution ID |
#!/bin/bash
echo "Processing: ${OMC_OBJ_PATH}"
# Your code here
ls -la "${OMC_OBJ_PATH}"For exe_script_file, environment variables that are not auto-exported must be explicitly declared using ENVIRONMENT_VARIABLES:
<dict>
<key>COMMAND_ID</key>
<string>my.action</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
<key>ENVIRONMENT_VARIABLES</key>
<dict>
<key>OMC_DLG_INPUT_TEXT</key>
<string></string>
</dict>
</dict>For AI Agents: Use
ENVIRONMENT_VARIABLESdictionary for forcing env var exports. See Runtime Context Reference for details.
| Format | Editable | Loadable | Notes |
|---|---|---|---|
.xib |
Yes | No | Xcode format, needs compilation |
.nib (flat) |
No | Yes | Compiled by Xcode |
.nib (bundled) |
Yes | Yes | Contains designable.nib + keyedobjects.nib |
Recommendation: Use bundled .nib format for in-place editing.
For AI Agents: Prefer editing existing NIB files rather than creating new ones. Use WatchdogMonitor.nib as a reference. Key patterns:
Adding OMC properties via
userDefinedRuntimeAttributes:<userDefinedRuntimeAttributes> <userDefinedRuntimeAttribute type="string" keyPath="commandID" value="watchdog.ok"/> <userDefinedRuntimeAttribute type="string" keyPath="selectionCommandID" value="watchdog.selection"/> </userDefinedRuntimeAttributes>Tags: Most controls use direct
tag="123"attribute. Only addtagtouserDefinedRuntimeAttributesfor OMC controls without native tag support (OMCBox, OMCIKImageView, OMCPDFView, OMCProgressIndicator, OMCTextView, OMCView, OMCWebView). See omc_controls_user_defined_runtime_attributes.md for full property list.Special chars: base64-encoded in
base64-UTF8="YES"(e.g.,\n=Cg==,\t=JQ==)
- Open Xcode
- File → New → File → macOS → User Interface → Window
- Save as
MyDialog.xib
/usr/bin/xcrun ibtool --compile MyDialog.nib --flatten NO MyDialog.xibResult:
MyDialog.nib/
designable.nib <- Editable XML
keyedobjects.nib <- Loadable binary
MyApp.app/Contents/Resources/Base.lproj/MyDialog.nib
<dict>
<key>NAME</key>
<string>MyApp</string>
<key>COMMAND_ID</key>
<string>myapp.show.window</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
<key>NIB_DIALOG</key>
<dict>
<key>NIB_NAME</key>
<string>MyDialog</string>
<key>IS_BLOCKING</key>
<false/> <!-- Non-modal -->
<key>INIT_SUBCOMMAND_ID</key>
<string>myapp.window.init</string>
<key>END_CANCEL_SUBCOMMAND_ID</key>
<string>myapp.window.close</string>
</dict>
</dict>| Key | Description |
|---|---|
NIB_NAME |
NIB filename without extension |
IS_BLOCKING |
true=modal, false=non-modal |
INIT_SUBCOMMAND_ID |
Command on window open |
END_OK_SUBCOMMAND_ID |
Command on OK button |
END_CANCEL_SUBCOMMAND_ID |
Command on Cancel/close |
| Control | Class in Xcode | Purpose |
|---|---|---|
| Button | OMCButton | Triggers command on click |
| TextField | OMCTextField | Text input |
| TableView | OMCTableView | Data display |
| PopUpButton | OMCPopUpButton | Dropdown menu |
| Slider | OMCSlider | Value selection |
| SearchField | OMCSearchField | Search input |
| ProgressIndicator | OMCProgressIndicator | Progress display |
| Property | Type | Description |
|---|---|---|
commandID |
String | Command to trigger on click |
mappedOnValue |
String | Value when checked (checkbox) |
mappedOffValue |
String | Value when unchecked |
acceptFileDrop |
Boolean | Accept file drops |
acceptTextDrop |
Boolean | Accept text drops |
| Property | Type | Description |
|---|---|---|
selectionCommandID |
String | Command on selection change |
doubleClickCommandID |
String | Command on double-click |
combinedSelectionSeparator |
String | Separator for multi-select |
multipleColumnSeparator |
String | Column separator |
| Property | Type | Description |
|---|---|---|
commandID |
String | Command on return/confirm |
escapingMode |
String | Text escaping mode |
- Tag = 0: Control not findable by OMC (use for buttons that only trigger commands)
- Tag > 0: Control findable via
OMC_NIB_DIALOG_CONTROL_<TAG>_VALUE
Example: TextField with tag=4 → value available as $OMC_NIB_DIALOG_CONTROL_4_VALUE
- Add NSTableView inside NSScrollView
- Set
customClass="OMCTableView" - Add columns with header cells
- Set
tag="1"directly as attribute on the table view
Tip: Copy from WatchdogMonitor.nib and modify for your needs.
Instead of NIBs, you can define windows using ActionUI — a declarative JSON format for SwiftUI-based views. Requires macOS 14.6+.
<dict>
<key>NAME</key>
<string>MyApp</string>
<key>COMMAND_ID</key>
<string>myapp.show.window</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
<key>ACTIONUI_WINDOW</key>
<dict>
<key>JSON_NAME</key>
<string>MyDialog</string>
<key>IS_BLOCKING</key>
<false/>
<key>INIT_SUBCOMMAND_ID</key>
<string>myapp.window.init</string>
<key>END_CANCEL_SUBCOMMAND_ID</key>
<string>myapp.window.close</string>
<key>WINDOW_TITLE</key>
<string>My Dialog</string>
<key>WINDOW_TYPE</key>
<string>floating</string>
</dict>
</dict>| Key | Description |
|---|---|
JSON_NAME |
JSON filename without extension (in Resources) |
IS_BLOCKING |
true=modal, false=non-modal |
INIT_SUBCOMMAND_ID |
Command on window open |
END_OK_SUBCOMMAND_ID |
Command on OK button |
END_CANCEL_SUBCOMMAND_ID |
Command on Cancel/close |
WINDOW_TITLE |
Static window title (overrides default) |
WINDOW_TYPE |
floating (utility panel) or global_floating (always-on-top utility panel) |
dialog_tool="$OMC_OMC_SUPPORT_PATH/omc_dialog_control"
window_uuid="$OMC_ACTIONUI_WINDOW_UUID"
# Set a value on view with id 1
"${dialog_tool}" "${window_uuid}" "1" "Hello"
# Set a property
"${dialog_tool}" "${window_uuid}" "1" omc_set_property "disabled" true
# Set window title dynamically
"${dialog_tool}" "${window_uuid}" omc_window "New Title"
# Close the window
"${dialog_tool}" "${window_uuid}" omc_window omc_terminate_cancelSee Command Reference — ACTIONUI_WINDOW for full details.
Located at $OMC_OMC_SUPPORT_PATH/omc_dialog_control
dialog_tool="$OMC_OMC_SUPPORT_PATH/omc_dialog_control"
dlg_guid="$OMC_NIB_DLG_GUID"# Set table columns
"${dialog_tool}" "${dlg_guid}" "1" "omc_table_set_columns" "Col1" "Col2" "Col3"
# Set column widths
"${dialog_tool}" "${dlg_guid}" "1" "omc_table_set_column_widths" "100" "100" "200"
# Add rows (tab-separated)
echo -e "row1\tdata1\tdata2" | "${dialog_tool}" "${dlg_guid}" "1" "omc_table_add_rows_from_stdin"
# Clear table
"${dialog_tool}" "${dlg_guid}" "1" "omc_table_remove_all_rows"
# Enable/disable control
"${dialog_tool}" "${dlg_guid}" "5" "omc_enable"
"${dialog_tool}" "${dlg_guid}" "5" "omc_disable"
# Set window title
"${dialog_tool}" "${dlg_guid}" "omc_window" "My Window Title"
# Close dialog
"${dialog_tool}" "${dlg_guid}" "omc_window" "omc_terminate_ok"Values are available as environment variables:
# TextField with tag 4
PATTERN="${OMC_NIB_DIALOG_CONTROL_4_VALUE}"
# Checkbox (1=on, 0=off)
IS_RECURSIVE="${OMC_NIB_DIALOG_CONTROL_2_VALUE}"
# Table column 4 of selected row
FILE_PATH="${OMC_NIB_TABLE_1_COLUMN_4_VALUE}"
# All rows, column 0 (special: all columns combined)
ALL_DATA="${OMC_NIB_TABLE_1_COLUMN_0_ALL_ROWS}"- Columns are 1-based (1, 2, 3, ...)
- Column 0 is special: all columns combined into single tab-separated string
<dict>
<key>NAME</key>
<string>Export</string>
<key>COMMAND_ID</key>
<string>myapp.export</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
<key>SAVE_AS_DIALOG</key>
<dict>
<key>MESSAGE</key>
<string>Save As...</string>
<key>DEFAULT_FILE_NAME</key>
<array>
<string>export_</string>
<string>__OBJ_NAME__</string>
<string>.txt</string>
</array>
<key>DEFAULT_LOCATION</key>
<array>
<string>~</string>
</array>
</dict>
</dict>Result: $OMC_DLG_SAVE_AS_PATH contains selected path.
<dict>
<key>INPUT_DIALOG</key>
<dict>
<key>INPUT_TYPE</key>
<string>text</string>
<key>TITLE</key>
<string>Enter Name</string>
<key>MESSAGE</key>
<string>Please enter a name:</string>
<key>DEFAULT_VALUE</key>
<string>__OBJ_NAME__</string>
</dict>
</dict>Result: $OMC_DLG_INPUT_TEXT contains user input.
<dict>
<key>NEXT_COMMAND_ID</key>
<string>myapp.step2</string>
</dict>next_command_tool="$OMC_OMC_SUPPORT_PATH/omc_next_command"
"${next_command_tool}" "$OMC_CURRENT_COMMAND_GUID" "MyApp.step2"Background processes started by scripts can outlive the parent applet. Handle cleanup properly.
OMC dispatches commands for app lifecycle events when a matching COMMAND_ID is defined in COMMAND_LIST:
COMMAND_ID |
When it fires |
|---|---|
app.will.launch |
Before the app finishes launching |
app.did.launch |
After the app finishes launching |
app.did.activate |
App came to foreground (e.g., user switched back to it) |
app.did.deactivate |
App went to background (e.g., user switched to another app) |
app.will.terminate |
App is about to quit |
app.did.activate is useful for detecting external changes made while the app was in the background. For per-window activation, use WINDOW_DID_ACTIVATE_SUBCOMMAND_ID in ACTIONUI_WINDOW or NIB_DIALOG (see Command Reference).
Special command with COMMAND_ID=app.will.terminate runs when app quits:
<dict>
<key>NAME</key>
<string>MyApp</string>
<key>COMMAND_ID</key>
<string>app.will.terminate</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file</string>
</dict>Script Scripts/app.will.terminate.sh:
#!/bin/bash
# Kill processes started by this app
PROCESS_PATH="${OMC_APP_BUNDLE_PATH}/Contents/Library/Python/bin/"
/usr/bin/pkill -U "${USER}" -f "${PROCESS_PATH}.*"For complex scenarios, write PIDs to preferences:
# Store PID
echo "${PROCESS_PID}" > "${HOME}/Library/Preferences/com.example.myapp.pids"
# On termination, read and kill
while read -r pid; do
kill "${pid}" 2>/dev/null
done < "${HOME}/Library/Preferences/com.example.myapp.pids"See AIChatApp for full example.
- Always register
app.will.terminatehandler - Use
-U ${USER}to avoid killing other users' processes - Consider storing PIDs for multi-window scenarios
- Clean up on window close if processes are window-specific
For commands that show results:
<dict>
<key>EXECUTION_MODE</key>
<string>exe_script_file_with_output_window</string>
<key>OUTPUT_WINDOW_SETTINGS</key>
<dict>
<key>WINDOW_TITLE</key>
<string>Results</string>
<key>WINDOW_TYPE</key>
<string>floating</string>
<key>WINDOW_WIDTH</key>
<integer>600</integer>
<key>WINDOW_HEIGHT</key>
<integer>400</integer>
<key>TEXT_FONT</key>
<string>Monaco</string>
<key>TEXT_SIZE</key>
<integer>11</integer>
</dict>
</dict>MyApp.app/
└── Contents/
├── Frameworks/
│ └── Abracode.framework/
├── MacOS/
│ └── MyApp
├── Resources/
│ ├── Command.plist <- Command definitions
│ ├── Scripts/
│ │ ├── MyApp.main.sh <- Main launch script
│ │ ├── MyApp.window.init <- Window init handler
│ │ ├── MyApp.window.close <- Window close handler
│ │ └── MyApp.app.will.terminate <- App quit handler
│ ├── Base.lproj/
│ │ └── MyDialog.nib <- Window definition
│ └── *.lproj/ <- Localizations
└── _CodeSignature/
-
View environment variables:
env | grep "OMC" | sort
-
Enable output window temporarily: Change
exe_script_filetoexe_script_file_with_output_window -
Control-click to show output: Hold Control when starting command to see stdout
-
Test nib loading:
omc_dialog_control <GUID> omc_window omc_show
- WatchdogApp — File system monitoring with custom UI (see
building_watchdog.mdfor detailed step-by-step guide) - FindApp
- DeltaApp
- XattrApp
- AIChatApp
- omc_dialog_control — Manipulate dialog controls
- omc_next_command — Chain to another command
- alert — Show alert dialogs
- pasteboard — Copy to clipboard
- notify — Send notifications
- plister — Read/write plist files
- filt — Text filtering utility
- b64 — Base64 encoding/decoding
- loco — Localization string extraction
<dict>
<key>NAME</key>
<string>FileInfo</string>
<key>ACTIVATION_MODE</key>
<string>act_file_or_folder</string>
<key>EXECUTION_MODE</key>
<string>exe_script_file_with_output_window</string>
<key>OUTPUT_WINDOW_SETTINGS</key>
<dict>
<key>TEXT_FONT</key>
<string>Monaco</string>
<key>TEXT_SIZE</key>
<integer>11</integer>
</dict>
</dict>#!/bin/bash
/usr/bin/stat -x "${OMC_OBJ_PATH}"Note: The main command (first in list, no
COMMAND_ID) is executed when files/folders are dropped on the applet. The scriptFileInfo.main.shis the handler for this main command.
Drop a file/folder on the app → output window shows file information.
For AI Agents: When creating example applets:
- Use the FileInfo example as a template for minimal applets
- The script MUST have a
.sh(or appropriate) extension- The script name must match:
<NAME>.main.<ext>where<NAME>is the command's NAME field