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
31 changes: 24 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Like AeroSpace, uses virtual workspaces instead of macOS native Spaces:
- **Window rules** (riverctl-style) - glob patterns, actions: ignore, float, tags, output, position, dimensions
- **Cursor warp** - `disabled`, `on-output-change`, `on-focus-change`
- **Auto-raise** (focus follows mouse) - `disabled`, `enabled` with optional delay
- **Keybinding modes** (river-style) - named modes (normal, resize, passthrough, etc.) with per-mode bindings
- **State streaming** - real-time events via `/tmp/yashiki-events.sock`

## Layout Protocol
Expand All @@ -80,17 +81,23 @@ Focus notification: `focus-changed <window_id>` sent automatically on focus chan

## State Streaming

Events via `/tmp/yashiki-events.sock` (JSON lines). Client sends `SubscribeRequest` with optional snapshot and filter. Events: WindowCreated/Destroyed/Updated, WindowFocused, DisplayFocused/Added/Removed/Updated, TagsChanged, LayoutChanged, Snapshot.
Events via `/tmp/yashiki-events.sock` (JSON lines). Client sends `SubscribeRequest` with optional snapshot and filter. Events: WindowCreated/Destroyed/Updated, WindowFocused, DisplayFocused/Added/Removed/Updated, TagsChanged, LayoutChanged, ModeChanged, Snapshot.

## CLI Usage

Tags use bitmask: tag 1 = 1, tag 2 = 2, tag 3 = 4, tags 1+2 = 3

```sh
yashiki start # Start daemon
yashiki bind alt-1 tag-view 1 # Bind hotkey
yashiki unbind alt-1 # Unbind hotkey
yashiki list-bindings # List bindings
yashiki declare-mode resize # Declare a keybinding mode
yashiki enter-mode resize # Switch to a mode
yashiki get-mode # Get current mode
yashiki bind alt-1 tag-view 1 # Bind hotkey (normal mode)
yashiki bind --mode resize h layout-cmd dec-main-ratio # Bind in specific mode
yashiki unbind alt-1 # Unbind hotkey (normal mode)
yashiki unbind --mode resize h # Unbind in specific mode
yashiki list-bindings # List all bindings (all modes)
yashiki list-bindings --mode normal # List bindings for specific mode
yashiki tag-view 1 # Switch to tag
yashiki tag-view --output 2 1 # Switch on specific display
yashiki tag-toggle 2 # Toggle tag visibility
Expand Down Expand Up @@ -125,7 +132,7 @@ yashiki get-auto-raise
yashiki set-outer-gap <all>|<v h>|<t r b l>
yashiki set-log-level <level> # Runtime log level (file mode only)
yashiki get-log-level
yashiki subscribe [--snapshot] [--filter events]
yashiki subscribe [--snapshot] [--filter events] # filter: window,focus,display,tags,layout,mode
yashiki quit
```

Expand Down Expand Up @@ -153,6 +160,12 @@ yashiki bind alt-f window-toggle-fullscreen
yashiki set-outer-gap 10
yashiki layout-cmd --layout tatami set-inner-gap 10

# Keybinding modes (river-style)
yashiki declare-mode resize
yashiki bind alt-r enter-mode resize
yashiki bind --mode resize h layout-cmd dec-main-ratio
yashiki bind --mode resize escape enter-mode normal

# Window rules
yashiki rule-add --app-name Finder float
yashiki rule-add --subrole AXUnknown ignore
Expand Down Expand Up @@ -246,7 +259,7 @@ Each group sorted alphabetically, blank lines between groups.
3. Wait for explicit approval
4. Only then use Edit/Write tools
5. If unexpected changes are needed, STOP and ask again
6. Run `cargo fmt --all` at the end
6. Run `cargo clippy --all` and `cargo fmt --all` at the end

#### When user asks a question during implementation:
- ANSWER THE QUESTION ONLY
Expand All @@ -264,8 +277,12 @@ Each group sorted alphabetically, blank lines between groups.
## Design Decisions

### Hotkey Management
- Bindings in `HashMap<Hotkey, Command>`, dirty flag for deferred tap recreation
- Per-mode bindings in `HashMap<String, HashMap<Hotkey, Command>>`, dirty flag for deferred tap recreation
- `Arc<RwLock<String>>` shares current mode between main thread and CGEventTap callback
- `enter_mode()` only updates RwLock (no tap rebuild); `bind()`/`unbind()` set dirty flag (tap rebuild needed)
- CGEventTap callback captures all modes' bindings at creation, reads current mode via RwLock at runtime
- CGEventTap callback signals CFRunLoopSource for immediate processing
- Default mode: `"normal"` (always exists, implicit)

### Focus
- `next`/`prev`: Stack-based (sorted by window ID)
Expand Down
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ macOS tiling window manager written in Rust.
- **Window rules** - Automatically configure windows by app name, bundle identifier, or title
- **Cursor warp** - Mouse follows focus (configurable: disabled, on-output-change, on-focus-change)
- **Auto-raise** - Focus follows mouse with optional delay (focus window when cursor enters)
- **Keybinding modes** - Named modes (river-style) for context-dependent hotkeys (e.g., resize mode, passthrough mode)
- **State streaming** - Real-time events for status bars and external tools
- **No SIP disable required** - Uses only public Accessibility API
- **Shell script configuration** - Config is just a shell script (`~/.config/yashiki/init`)
Expand Down Expand Up @@ -151,9 +152,31 @@ yashiki version # Show version
### Hotkey Management

```sh
yashiki bind alt-1 tag-view 1 # Bind hotkey
yashiki unbind alt-1 # Unbind hotkey
yashiki list-bindings # List all bindings
yashiki bind alt-1 tag-view 1 # Bind hotkey (normal mode)
yashiki unbind alt-1 # Unbind hotkey (normal mode)
yashiki list-bindings # List all bindings (all modes)
yashiki list-bindings --mode normal # List bindings for a specific mode
```

### Keybinding Modes

River-style modal keybindings. All bindings default to `normal` mode. Use `--mode` to bind in other modes.

```sh
yashiki declare-mode resize # Declare a new mode
yashiki declare-mode passthrough # Another mode

yashiki bind alt-r enter-mode resize # Enter resize mode with alt-r
yashiki bind --mode resize h layout-cmd dec-main-ratio # h in resize mode
yashiki bind --mode resize l layout-cmd inc-main-ratio # l in resize mode
yashiki bind --mode resize escape enter-mode normal # escape to exit

# Passthrough mode: only one binding to exit
yashiki bind alt-p enter-mode passthrough
yashiki bind --mode passthrough alt-p enter-mode normal

yashiki get-mode # Get current mode
yashiki enter-mode normal # Switch mode via CLI
```

### Tag Operations
Expand Down Expand Up @@ -273,7 +296,7 @@ yashiki subscribe --snapshot # Get initial snapshot on connect
yashiki subscribe --filter focus,tags # Filter specific events
```

**Event types:** `window`, `focus`, `display`, `tags`, `layout`
**Event types:** `window`, `focus`, `display`, `tags`, `layout`, `mode`

Events are streamed as JSON lines to stdout.

Expand Down
58 changes: 36 additions & 22 deletions completions/zsh/_yashiki
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ _yashiki_event_filters() {
'display:Display add/remove/update events'
'tags:Tag change events'
'layout:Layout change events'
'mode:Mode change events'
)
_describe -t filters 'event filter' filters
}
Expand All @@ -98,6 +99,9 @@ _yashiki_subcommands() {
local commands=(
'start:Start the yashiki daemon'
'version:Show version information'
'declare-mode:Declare a keybinding mode'
'enter-mode:Switch to a keybinding mode'
'get-mode:Get the current keybinding mode'
'bind:Bind a hotkey to a command'
'unbind:Unbind a hotkey'
'list-bindings:List all hotkey bindings'
Expand Down Expand Up @@ -160,6 +164,7 @@ _yashiki_bind_commands() {
'layout-set-default:Set the default layout engine'
'layout-set:Set layout engine for tags'
'layout-cmd:Send command to layout engine'
'enter-mode:Switch to a keybinding mode'
'exec:Execute a shell command'
'exec-or-focus:Focus app if running, otherwise execute command'
'quit:Quit the yashiki daemon'
Expand All @@ -169,16 +174,16 @@ _yashiki_bind_commands() {

_yashiki_rule_options() {
_arguments -s \
'--app-name=[Application name pattern]:pattern:' \
'--app-id=[Bundle identifier pattern]:pattern:' \
'--title=[Window title pattern]:pattern:' \
'--ax-id=[AXIdentifier pattern]:pattern:' \
'--subrole=[AXSubrole pattern]:pattern:' \
'--window-level=[Window level]:level:_yashiki_window_levels' \
'--close-button=[Close button state]:state:_yashiki_button_states' \
'--fullscreen-button=[Fullscreen button state]:state:_yashiki_button_states' \
'--minimize-button=[Minimize button state]:state:_yashiki_button_states' \
'--zoom-button=[Zoom button state]:state:_yashiki_button_states' \
'--app-name[Application name pattern]:pattern:' \
'--app-id[Bundle identifier pattern]:pattern:' \
'--title[Window title pattern]:pattern:' \
'--ax-id[AXIdentifier pattern]:pattern:' \
'--subrole[AXSubrole pattern]:pattern:' \
'--window-level[Window level]:level:_yashiki_window_levels' \
'--close-button[Close button state]:state:_yashiki_button_states' \
'--fullscreen-button[Fullscreen button state]:state:_yashiki_button_states' \
'--minimize-button[Minimize button state]:state:_yashiki_button_states' \
'--zoom-button[Zoom button state]:state:_yashiki_button_states' \
'*:action:_yashiki_rule_actions'
}

Expand All @@ -193,21 +198,30 @@ _yashiki() {
case $state in
args)
case $line[1] in
start|version|list-bindings|tag-view-last|window-toggle-fullscreen|window-toggle-float|window-close|list-outputs|get-state|focused-window|exec-path|list-rules|get-cursor-warp|get-auto-raise|get-outer-gap|quit)
start|version|tag-view-last|window-toggle-fullscreen|window-toggle-float|window-close|list-outputs|get-state|focused-window|exec-path|list-rules|get-cursor-warp|get-auto-raise|get-outer-gap|get-mode|quit)
# No arguments
;;
declare-mode|enter-mode)
_arguments '1:mode name:'
;;
bind)
_arguments \
'--mode[Keybinding mode (default: normal)]:mode:' \
'1:hotkey:' \
'2:command:_yashiki_bind_commands' \
'*:args:'
;;
unbind)
_arguments '1:hotkey:'
_arguments \
'--mode[Keybinding mode (default: normal)]:mode:' \
'1:hotkey:'
;;
list-bindings)
_arguments '--mode[Filter by mode]:mode:'
;;
tag-view|tag-toggle)
_arguments \
'--output=[Output ID or name]:output:' \
'--output[Output ID or name]:output:' \
'1:tags bitmask:'
;;
window-move-to-tag|window-toggle-tag)
Expand All @@ -220,25 +234,25 @@ _yashiki() {
_arguments '1:direction:_yashiki_output_directions'
;;
retile)
_arguments '--output=[Output ID or name]:output:'
_arguments '--output[Output ID or name]:output:'
;;
layout-set-default)
_arguments '1:layout:_yashiki_layouts'
;;
layout-set)
_arguments \
'--tags=[Tags bitmask]:tags:' \
'--output=[Output ID or name]:output:' \
'--tags[Tags bitmask]:tags:' \
'--output[Output ID or name]:output:' \
'1:layout:_yashiki_layouts'
;;
layout-get)
_arguments \
'--tags=[Tags bitmask]:tags:' \
'--output=[Output ID or name]:output:'
'--tags[Tags bitmask]:tags:' \
'--output[Output ID or name]:output:'
;;
layout-cmd)
_arguments \
'--layout=[Target layout engine]:layout:_yashiki_layouts' \
'--layout[Target layout engine]:layout:_yashiki_layouts' \
'1:command:' \
'*:args:'
;;
Expand All @@ -254,7 +268,7 @@ _yashiki() {
;;
exec-or-focus)
_arguments \
'--app-name=[Application name to focus]:app name:' \
'--app-name[Application name to focus]:app name:' \
'1:shell command:'
;;
set-exec-path)
Expand All @@ -273,7 +287,7 @@ _yashiki() {
;;
set-auto-raise)
_arguments \
'--delay=[Delay in milliseconds before raising]:delay (ms):' \
'--delay[Delay in milliseconds before raising]:delay (ms):' \
'1:mode:_yashiki_auto_raise_modes'
;;
set-outer-gap)
Expand All @@ -282,7 +296,7 @@ _yashiki() {
subscribe)
_arguments \
'--snapshot[Request snapshot on connection]' \
'--filter=[Event filter]:filter:_yashiki_event_filters'
'--filter[Event filter]:filter:_yashiki_event_filters'
;;
esac
;;
Expand Down
19 changes: 19 additions & 0 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,25 @@ yashiki bind alt-shift-return exec "open -n /Applications/Ghostty.app"
yashiki bind alt-s exec-or-focus --app-name Safari "open -a Safari"
yashiki bind alt-c exec-or-focus --app-name "Google Chrome" "open -a 'Google Chrome'"

# Keybinding modes (river-style)
yashiki declare-mode resize
yashiki declare-mode passthrough

# Enter resize/passthrough mode
yashiki bind alt-r enter-mode resize
yashiki bind alt-p enter-mode passthrough

# Resize mode: h/j/k/l to adjust layout, escape/return to exit
yashiki bind --mode resize h layout-cmd dec-main-ratio
yashiki bind --mode resize l layout-cmd inc-main-ratio
yashiki bind --mode resize j layout-cmd dec-main-count
yashiki bind --mode resize k layout-cmd inc-main-count
yashiki bind --mode resize escape enter-mode normal
yashiki bind --mode resize return enter-mode normal

# Passthrough mode: all keys pass through, alt-p to exit
yashiki bind --mode passthrough alt-p enter-mode normal

# Companion tools (terminated on yashiki quit)
yashiki exec --track "borders active_color=0xffe1e3e4"

Expand Down
Loading