Summary
Add a new Launcher resource type that displays a text input field and a Go button in the menu bar popup. When the user types/pastes a value and clicks Go (or presses Enter), BarKeeper executes a configured command template, substituting the user input into the command.
Motivation
Common workflow: paste an MSX record ID (e.g. 7-3H6CBCNA7Q), click Go, and a browser tab opens at the correct Dynamics 365 search URL. Currently this requires switching to a browser, navigating to MSX, finding the search bar, pasting, and clicking. A Launcher resource turns this into a one-step action from the menu bar.
This pattern is generic — it works for any "paste an identifier and open/run something":
- Open an MSX record by ID →
open "https://microsoftsales.crm.dynamics.com/main.aspx?forceUCI=1&pagetype=search&searchText={input}"
- Open a Jira ticket →
open "https://jira.example.com/browse/{input}"
- SSH into a host by name
- Look up a package →
open "https://www.npmjs.com/package/{input}"
Design
Resource model changes (Resource.swift)
Add a new case to ResourceType:
Add a new optional field to Resource:
/// Placeholder text shown in the Launcher input field.
var inputPlaceholder: String?
The actionScript field (already on Resource) is reused. It must contain the {input} placeholder token, which is replaced at runtime with the sanitized user input.
Config schema
{
"id": "...",
"name": "MSX Lookup",
"type": "launcher",
"actionScript": "open \"https://microsoftsales.crm.dynamics.com/main.aspx?forceUCI=1&pagetype=search&searchText={input}\"",
"inputPlaceholder": "Paste MSX record ID"
}
| Field |
Type |
Description |
actionScript |
String |
Shell command with {input} placeholder. |
inputPlaceholder |
String? |
Hint text for the input field. Defaults to "Enter value…" if nil. |
Menu bar row (ResourceRowView.swift)
The Launcher row should render:
- A 🚀 (rocket) or 🔗 (link) system icon as the status indicator (use
link SF Symbol).
- The resource name as a label.
- A TextField (compact, ~150pt wide) with the configured placeholder.
- A Go button (small, bordered) that triggers execution. The Go Button should look similar that the one in ResourceType
Button
- Pressing Enter in the text field should also trigger execution.
The text field value is local @State — it does not persist across menu open/close.
Execution (ResourceManager.swift)
Add a new method:
func runLauncher(_ resourceId: UUID, input: String)
Logic:
- Guard the resource is
.launcher and actionScript is non-empty.
- Sanitize
input: trim whitespace, reject empty input.
- Replace all occurrences of
{input} in actionScript with the shell-escaped input value.
- Execute via
ShellExecutor.run(...).
- Handle success/failure the same way
runAction does (notifications, error state).
Security: The input value must be shell-escaped before substitution to prevent command injection. Use a helper that wraps the value in single quotes with internal single quotes properly escaped, or use Process arguments array to avoid shell interpretation entirely.
Editor (ResourceEditorView.swift)
When type == .launcher:
- Show an Action Script text editor (reuse existing
scriptEditor) with placeholder: Command with {input} placeholder, e.g. open "https://example.com/{input}"
- Show an Input Placeholder
TextField for customizing the hint text shown in the menu bar input field.
Settings picker (ResourceEditorView.swift)
Add to the type Picker:
Label("Launcher", systemImage: "link").tag(ResourceType.launcher)
buildResource() in ResourceEditorView.swift
Include inputPlaceholder for .launcher type (add a new @State variable and wire it up).
README updates
- Add Launcher to the "Resource Types" section with a description and example.
- Add a row to the "JSON Schema" table.
- Update the architecture ASCII tree if needed.
- Add Launcher to the "Script Convention" table: Exit code 0 = success, non-zero = error shown in tooltip.
Acceptance criteria
Summary
Add a new Launcher resource type that displays a text input field and a Go button in the menu bar popup. When the user types/pastes a value and clicks Go (or presses Enter), BarKeeper executes a configured command template, substituting the user input into the command.
Motivation
Common workflow: paste an MSX record ID (e.g.
7-3H6CBCNA7Q), click Go, and a browser tab opens at the correct Dynamics 365 search URL. Currently this requires switching to a browser, navigating to MSX, finding the search bar, pasting, and clicking. A Launcher resource turns this into a one-step action from the menu bar.This pattern is generic — it works for any "paste an identifier and open/run something":
open "https://microsoftsales.crm.dynamics.com/main.aspx?forceUCI=1&pagetype=search&searchText={input}"open "https://jira.example.com/browse/{input}"open "https://www.npmjs.com/package/{input}"Design
Resource model changes (
Resource.swift)Add a new case to
ResourceType:Add a new optional field to
Resource:The
actionScriptfield (already onResource) is reused. It must contain the{input}placeholder token, which is replaced at runtime with the sanitized user input.Config schema
{ "id": "...", "name": "MSX Lookup", "type": "launcher", "actionScript": "open \"https://microsoftsales.crm.dynamics.com/main.aspx?forceUCI=1&pagetype=search&searchText={input}\"", "inputPlaceholder": "Paste MSX record ID" }actionScriptString{input}placeholder.inputPlaceholderString?"Enter value…"if nil.Menu bar row (
ResourceRowView.swift)The Launcher row should render:
linkSF Symbol).ButtonThe text field value is local
@State— it does not persist across menu open/close.Execution (
ResourceManager.swift)Add a new method:
Logic:
.launcherandactionScriptis non-empty.input: trim whitespace, reject empty input.{input}inactionScriptwith the shell-escaped input value.ShellExecutor.run(...).runActiondoes (notifications, error state).Security: The input value must be shell-escaped before substitution to prevent command injection. Use a helper that wraps the value in single quotes with internal single quotes properly escaped, or use
Processarguments array to avoid shell interpretation entirely.Editor (
ResourceEditorView.swift)When
type == .launcher:scriptEditor) with placeholder:Command with {input} placeholder, e.g. open "https://example.com/{input}"TextFieldfor customizing the hint text shown in the menu bar input field.Settings picker (
ResourceEditorView.swift)Add to the type
Picker:buildResource()inResourceEditorView.swiftInclude
inputPlaceholderfor.launchertype (add a new@Statevariable and wire it up).README updates
Acceptance criteria
launchercase inResourceTypeenum.inputPlaceholderoptional field onResourcewith backward-compatible decoding.{input}inactionScriptis replaced with shell-escaped user input.ResourceEditorViewshows appropriate fields for Launcher type.xcodegen generate && xcodebuild build -scheme BarKeeper -configuration Debug.