Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .mise/tasks/zed/keymap/bind-snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
#MISE description="Bind a Zed key to insert an inline snippet"
#MISE dir="{{config_root}}"
#USAGE flag "--keystroke <keystroke>" required=#true help="Zed key binding, for example cmd-k i"
#USAGE flag "--snippet <snippet>" required=#true help="Inline Zed snippet text"
#USAGE flag "--context <context>" default="Editor" help="Zed keymap context"
#USAGE flag "--force" default=#false help="Replace an existing different binding"
#USAGE example "mise run zed:keymap:bind-snippet --context 'Editor && extension == md' --keystroke 'cmd-k i' --snippet '<!-- ! \"@ikma ${1:feedback}\" | mise comment -->$0'" header="Bind a Markdown snippet"
set -euo pipefail

source "$MISE_CONFIG_ROOT/lib/zed/keymap.sh"

binding_json="$(jq -n --arg snippet "$usage_snippet" '["editor::InsertSnippet", {snippet: $snippet}]')"
zed_keymap_upsert_binding_json "$usage_context" "$usage_keystroke" "$binding_json" "$usage_force"
13 changes: 13 additions & 0 deletions .mise/tasks/zed/keymap/check-snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
#MISE description="Check whether a Zed snippet key binding can be installed"
#MISE dir="{{config_root}}"
#USAGE flag "--keystroke <keystroke>" required=#true help="Zed key binding, for example cmd-k i"
#USAGE flag "--snippet <snippet>" required=#true help="Inline Zed snippet text"
#USAGE flag "--context <context>" default="Editor" help="Zed keymap context"
#USAGE example "mise run zed:keymap:check-snippet --context 'Editor && extension == md' --keystroke 'cmd-k i' --snippet '<!-- ! \"@ikma ${1:feedback}\" | mise comment -->$0'" header="Check a Markdown snippet binding"
set -euo pipefail

source "$MISE_CONFIG_ROOT/lib/zed/keymap.sh"

binding_json="$(jq -n --arg snippet "$usage_snippet" '["editor::InsertSnippet", {snippet: $snippet}]')"
zed_keymap_check_binding_json "$usage_context" "$usage_keystroke" "$binding_json"
137 changes: 119 additions & 18 deletions .mise/tasks/zed/tasks/upsert
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,131 @@
#USAGE flag "--label <label>" required=#true help="Task label"
#USAGE flag "--command <command>" required=#true help="Command to run"
#USAGE flag "--arg <arg>" default="" var=#true help="Command argument (repeatable)"
#USAGE flag "--save <save>" default="" help="Zed save policy, for example current"
#USAGE flag "--hide <hide>" default="" help="Zed hide policy, for example on_success"
#USAGE example "mise run zed:tasks:upsert --label 'comments: dispatch current file' --command comments --arg dispatch --arg '$ZED_FILE' --save current --hide on_success" header="Upsert a current-file task"
#USAGE flag "--save <save>" default="" help="Zed save policy: all, current, or none"
#USAGE flag "--reveal <reveal>" default="" help="Zed reveal policy: always, no_focus, or never"
#USAGE flag "--hide <hide>" default="" help="Zed hide policy: never, always, or on_success"
#USAGE flag "--shell-program <shell_program>" default="" help="Explicit Zed task shell program"
#USAGE flag "--shell-arg <shell_arg>" default="" var=#true help="Shell argument (repeatable; requires --shell-program)"
#USAGE flag "--env <env>" default="" var=#true help="Task environment entry as name=value (repeatable)"
#USAGE example "mise run zed:tasks:upsert --label 'comments: dispatch current file' --command comments --arg dispatch --arg '$ZED_FILE' --save current --reveal never --hide on_success" header="Upsert a current-file task"
#USAGE example "mise run zed:tasks:upsert --label review --command comments --shell-program /bin/zsh --shell-arg=-f --env COMMENT_CHAT_AS=or" header="Set an explicit shell and environment"
set -euo pipefail

source "$MISE_CONFIG_ROOT/lib/zed/tasks.sh"

usage_values() {
local raw="$1"
[ -n "$raw" ] || return 0
printf '%s' "$raw" | xargs printf '%s\n'
}

json_array() {
if [ "$#" -eq 0 ]; then
printf '[]\n'
else
printf '%s\n' "$@" | jq -R . | jq -s .
fi
}

validate_optional_enum() {
local field="$1"
local value="$2"
shift 2

[ -n "$value" ] || return 0

local allowed
for allowed in "$@"; do
[ "$value" != "$allowed" ] || return 0
done

printf 'ctl zed tasks upsert: invalid %s value: %s\n' "$field" "$value" >&2
return 1
}

save="${usage_save:-}"
reveal="${usage_reveal:-}"
hide="${usage_hide:-}"
shell_program="${usage_shell_program:-}"

validate_optional_enum save "$save" all current none
validate_optional_enum reveal "$reveal" always no_focus never
validate_optional_enum hide "$hide" never always on_success

args=()
if [ -n "${usage_arg:-}" ]; then
while IFS= read -r arg; do
[ -z "$arg" ] && continue
args+=("$arg")
done < <(printf '%s' "$usage_arg" | xargs printf '%s\n')
while IFS= read -r value; do
[ -n "$value" ] || continue
args+=("$value")
done < <(usage_values "${usage_arg:-}")
args_json="$(json_array ${args[@]+"${args[@]}"})"

shell_args=()
while IFS= read -r value; do
[ -n "$value" ] || continue
shell_args+=("$value")
done < <(usage_values "${usage_shell_arg:-}")

if [ "${#shell_args[@]}" -gt 0 ] && [ -z "$shell_program" ]; then
echo "ctl zed tasks upsert: --shell-arg requires --shell-program" >&2
exit 1
fi

shell_json='null'
if [ -n "$shell_program" ]; then
if [ "${#shell_args[@]}" -gt 0 ]; then
shell_args_json="$(json_array ${shell_args[@]+"${shell_args[@]}"})"
shell_json="$(
jq -n \
--arg program "$shell_program" \
--argjson args "$shell_args_json" \
'{with_arguments: {program: $program, args: $args}}'
)"
else
shell_json="$(jq -n --arg program "$shell_program" '{program: $program}')"
fi
fi

args_json=$(printf '%s\n' ${args[@]+"${args[@]}"} | jq -R . | jq -s .)
task_json=$(jq -n \
--arg label "$usage_label" \
--arg command "$usage_command" \
--argjson args "$args_json" \
--arg save "${usage_save:-}" \
--arg hide "${usage_hide:-}" \
'{label: $label, command: $command, args: $args}
+ (if $save == "" then {} else {save: $save} end)
+ (if $hide == "" then {} else {hide: $hide} end)')
env_json='{}'
while IFS= read -r entry; do
[ -n "$entry" ] || continue

case "$entry" in
*=*) ;;
*)
printf 'ctl zed tasks upsert: invalid --env entry (expected name=value): %s\n' "$entry" >&2
exit 1
;;
esac

name="${entry%%=*}"
value="${entry#*=}"
if ! [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
printf 'ctl zed tasks upsert: invalid environment name: %s\n' "$name" >&2
exit 1
fi
if jq -e --arg name "$name" 'has($name)' <<<"$env_json" >/dev/null; then
printf 'ctl zed tasks upsert: duplicate environment name: %s\n' "$name" >&2
exit 1
fi
env_json="$(jq --arg name "$name" --arg value "$value" '. + {($name): $value}' <<<"$env_json")"
done < <(usage_values "${usage_env:-}")

task_json="$(
jq -n \
--arg label "$usage_label" \
--arg command "$usage_command" \
--argjson args "$args_json" \
--arg save "$save" \
--arg reveal "$reveal" \
--arg hide "$hide" \
--argjson shell "$shell_json" \
--argjson env "$env_json" \
'{label: $label, command: $command, args: $args}
+ (if $save == "" then {} else {save: $save} end)
+ (if $reveal == "" then {} else {reveal: $reveal} end)
+ (if $hide == "" then {} else {hide: $hide} end)
+ (if $shell == null then {} else {shell: $shell} end)
+ (if ($env | length) == 0 then {} else {env: $env} end)'
)"

zed_tasks_upsert_json "$task_json"
48 changes: 30 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Boring JSON surgery for tools that should not each own it.

![shape: mise + BATS](https://img.shields.io/badge/shape-mise%20%2B%20BATS-4EAA25?style=flat&logo=gnubash&logoColor=white)
[![tests: 23](https://img.shields.io/badge/tests-23-brightgreen?style=flat)](test/)
[![tests: 34](https://img.shields.io/badge/tests-34-brightgreen?style=flat)](test/)
![lints: 9](https://img.shields.io/badge/lints-9-blue?style=flat)
![README: TSX](https://img.shields.io/badge/README-TSX-f472b6?style=flat)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=flat)](LICENSE)
Expand All @@ -18,7 +18,7 @@ Boring JSON surgery for tools that should not each own it.

## What this is

`ctl` is a shiv-installable CLI for app and editor integrations that need a small, reusable command surface. The first version manages project-local Zed tasks in `.zed/tasks.json`.
`ctl` is a shiv-installable CLI for app and editor integrations that need a small, reusable command surface. Its first namespace manages Zed task and keymap JSON.

The immediate extraction target is `comments integrations zed`: it should not own generic Zed JSON upsert logic forever. `ctl zed tasks ...` gives that logic one home so other tools can reuse it.

Expand Down Expand Up @@ -46,13 +46,17 @@ ctl zed tasks upsert \
--arg dispatch \
--arg '$ZED_FILE' \
--save current \
--hide on_success
--reveal never \
--hide on_success \
--shell-program /bin/zsh \
--shell-arg=-f \
--env COMMENT_CHAT_AS=or

# Remove tasks with a matching label.
ctl zed tasks remove --label "comments: dispatch current file"
```

All commands target the caller directory's `.zed/tasks.json`. Existing tasks are preserved. Upsert replaces tasks with the same `label` and appends when the label is new. Invalid JSON or a non-array tasks file fails without clobbering the file.
All commands target the caller directory's `.zed/tasks.json`. Existing tasks are preserved. Upsert replaces tasks with the same `label` and appends when the label is new. Optional reveal, shell, and environment fields are emitted only when the caller supplies them. Invalid values, invalid JSON, or a non-array tasks file fail without clobbering the file.

## Zed keymap

Expand All @@ -69,6 +73,10 @@ ctl zed keymap bind-task --keystroke cmd-shift-d --task "comments: dispatch
# Check/bind rerun with fresh Zed context.
ctl zed keymap check-rerun --keystroke cmd-shift-r
ctl zed keymap bind-rerun --keystroke cmd-shift-r --reevaluate-context

# Check/bind a concrete inline snippet action.
ctl zed keymap check-snippet --context 'Editor && extension == md' --keystroke 'cmd-k i' --snippet '<!-- ! "@ikma ${1:feedback}" | mise comment -->$0'
ctl zed keymap bind-snippet --context 'Editor && extension == md' --keystroke 'cmd-k i' --snippet '<!-- ! "@ikma ${1:feedback}" | mise comment -->$0'
```

Keymap commands target Zed's global `keymap.json`. Existing bindings are preserved. If a requested keystroke already has a different binding in the target context, `ctl` fails without clobbering it unless `--force` is passed.
Expand All @@ -87,6 +95,8 @@ mise run zed:keymap:check-task --keystroke cmd-shift-d --task example
mise run zed:keymap:bind-task --keystroke cmd-shift-d --task example
mise run zed:keymap:check-rerun --keystroke cmd-shift-r
mise run zed:keymap:bind-rerun --keystroke cmd-shift-r
mise run zed:keymap:check-snippet --context Editor --keystroke 'cmd-k i' --snippet 'Hello ${1:there}$0'
mise run zed:keymap:bind-snippet --context Editor --keystroke 'cmd-k i' --snippet 'Hello ${1:there}$0'
```

## Project-local path resolution
Expand All @@ -95,19 +105,21 @@ When installed by shiv, the shim exports `CTL_CALLER_PWD` before running the tas

## Tasks

| Task | Description |
| --------------------------------- | ------------------------------------------------------ |
| `mise run doctor` | Check local development setup |
| `mise run test` | Run BATS tests |
| `mise run zed:keymap:bind-rerun` | Bind a Zed key to rerun the last task |
| `mise run zed:keymap:bind-task` | Bind a Zed key to spawn a named task |
| `mise run zed:keymap:check-rerun` | Check whether a Zed rerun key binding can be installed |
| `mise run zed:keymap:check-task` | Check whether a Zed task key binding can be installed |
| `mise run zed:keymap:path` | Print the global Zed keymap.json path |
| `mise run zed:tasks:list` | Print project-local Zed tasks as JSON |
| `mise run zed:tasks:path` | Print the project-local Zed tasks.json path |
| `mise run zed:tasks:remove` | Remove a project-local Zed task by label |
| `mise run zed:tasks:upsert` | Insert or replace a project-local Zed task by label |
| Task | Description |
| ----------------------------------- | -------------------------------------------------------- |
| `mise run doctor` | Check local development setup |
| `mise run test` | Run BATS tests |
| `mise run zed:keymap:bind-rerun` | Bind a Zed key to rerun the last task |
| `mise run zed:keymap:bind-snippet` | Bind a Zed key to insert an inline snippet |
| `mise run zed:keymap:bind-task` | Bind a Zed key to spawn a named task |
| `mise run zed:keymap:check-rerun` | Check whether a Zed rerun key binding can be installed |
| `mise run zed:keymap:check-snippet` | Check whether a Zed snippet key binding can be installed |
| `mise run zed:keymap:check-task` | Check whether a Zed task key binding can be installed |
| `mise run zed:keymap:path` | Print the global Zed keymap.json path |
| `mise run zed:tasks:list` | Print project-local Zed tasks as JSON |
| `mise run zed:tasks:path` | Print the project-local Zed tasks.json path |
| `mise run zed:tasks:remove` | Remove a project-local Zed task by label |
| `mise run zed:tasks:upsert` | Insert or replace a project-local Zed task by label |

## Repo inventory

Expand Down Expand Up @@ -152,7 +164,7 @@ readme build --check
git diff --check
```

The suite currently has **23 tests** and **11 public tasks**. CI runs on **ubuntu-latest + macos-latest**.
The suite currently has **34 tests** and **13 public tasks**. CI runs on **ubuntu-latest + macos-latest**.

<div align="center">

Expand Down
28 changes: 21 additions & 7 deletions README.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ const readme = (
<Section title="What this is">
<Paragraph>
<Code>ctl</Code>
{" is a shiv-installable CLI for app and editor integrations that need a small, reusable command surface. The first version manages project-local Zed tasks in "}
<Code>.zed/tasks.json</Code>
{"."}
{" is a shiv-installable CLI for app and editor integrations that need a small, reusable command surface. Its first namespace manages Zed task and keymap JSON."}
</Paragraph>

<Paragraph>
Expand Down Expand Up @@ -200,7 +198,11 @@ ctl zed tasks upsert \\
--arg dispatch \\
--arg '$ZED_FILE' \\
--save current \\
--hide on_success
--reveal never \\
--hide on_success \\
--shell-program /bin/zsh \\
--shell-arg=-f \\
--env COMMENT_CHAT_AS=or

# Remove tasks with a matching label.
ctl zed tasks remove --label "comments: dispatch current file"`}</CodeBlock>
Expand All @@ -210,7 +212,7 @@ ctl zed tasks remove --label "comments: dispatch current file"`}</CodeBlock>
<Code>.zed/tasks.json</Code>
{". Existing tasks are preserved. Upsert replaces tasks with the same "}
<Code>label</Code>
{" and appends when the label is new. Invalid JSON or a non-array tasks file fails without clobbering the file."}
{" and appends when the label is new. Optional reveal, shell, and environment fields are emitted only when the caller supplies them. Invalid values, invalid JSON, or a non-array tasks file fail without clobbering the file."}
</Paragraph>
</Section>

Expand All @@ -232,7 +234,17 @@ ctl zed keymap bind-task \
ctl zed keymap check-rerun --keystroke cmd-shift-r
ctl zed keymap bind-rerun \
--keystroke cmd-shift-r \
--reevaluate-context`}</CodeBlock>
--reevaluate-context

# Check/bind a concrete inline snippet action.
ctl zed keymap check-snippet \
--context 'Editor && extension == md' \
--keystroke 'cmd-k i' \
--snippet '<!-- ! "@ikma \${1:feedback}" | mise comment -->$0'
ctl zed keymap bind-snippet \
--context 'Editor && extension == md' \
--keystroke 'cmd-k i' \
--snippet '<!-- ! "@ikma \${1:feedback}" | mise comment -->$0'`}</CodeBlock>

<Paragraph>
{"Keymap commands target Zed's global "}
Expand All @@ -258,7 +270,9 @@ mise run zed:keymap:path
mise run zed:keymap:check-task --keystroke cmd-shift-d --task example
mise run zed:keymap:bind-task --keystroke cmd-shift-d --task example
mise run zed:keymap:check-rerun --keystroke cmd-shift-r
mise run zed:keymap:bind-rerun --keystroke cmd-shift-r`}</CodeBlock>
mise run zed:keymap:bind-rerun --keystroke cmd-shift-r
mise run zed:keymap:check-snippet --context Editor --keystroke 'cmd-k i' --snippet 'Hello \${1:there}$0'
mise run zed:keymap:bind-snippet --context Editor --keystroke 'cmd-k i' --snippet 'Hello \${1:there}$0'`}</CodeBlock>
</Section>

<Section title="Project-local path resolution">
Expand Down
Loading