Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/9e3c3b8a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: patch
---

Add cmdcmd:// URL commands for gesture launchers, plus an optional interactive 3-finger trackpad swipe setting. Swipe up starts after a small threshold and commits at 33% to reveal cmdcmd; swipe down 33% returns to the originally focused window; reversing direction restores the prior state. Settings now warns about conflicting macOS 3-finger gestures and can help disable them. Keyboard/click selection stays binary and tied to the explicitly highlighted or clicked tile. A new preview quality setting can keep the grid efficient and switch to sharper Retina captures only when picking, clicking, peeking, or popping a tile. Fast cmd-cmd taps are handled more reliably by using one event source, preserving the opposite Command side on side-bit-only events, and removing per-trigger debug state dumps. Live capture is now bounded to reduce CPU and memory pressure, idle indicators work again by sampling tiny frame signatures instead of treating every poll as activity, and a perf-audit helper captures ps/vmmap/sample/log snapshots.
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ let package = Package(
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.0"),
],
targets: [
.target(
name: "CMultitouch",
linkerSettings: [
.linkedFramework("CoreFoundation"),
]
),
.executableTarget(
name: "cmdcmd",
dependencies: [
"CMultitouch",
.product(name: "Sparkle", package: "Sparkle"),
],
linkerSettings: [
.linkedFramework("AppKit"),
.linkedFramework("ScreenCaptureKit"),
.linkedFramework("Carbon"),
.linkedFramework("CoreFoundation"),
]
),
]
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ Or download `cmdcmd.zip` from the [latest release](https://github.com/peterp/cmd

**⌘ + ⌘** — tap left and right Command at the same time (no other key in between). Tap again, or press `esc`, to dismiss.

**3-finger trackpad swipe** — optional in Settings. Swipe up after a small movement threshold and 33% to commit opening the overlay; swipe down 33% to return to the originally focused window. Keyboard and click/tap are binary selection modes: they immediately pick the explicitly highlighted/clicked tile with no threshold. Gesture mode is analog: vertical finger travel drives the reveal/dismiss animation, and reversing direction before release restores the prior state. If macOS 3-finger Mission Control/App Exposé is enabled, Settings can help disable those system gestures.

## URL commands

cmdcmd registers the `cmdcmd://` URL scheme for external launchers and gesture tools:

```sh
open 'cmdcmd://show' # show/focus the overlay
open 'cmdcmd://toggle' # toggle the overlay
open 'cmdcmd://swipe-up' # show/focus the overlay
open 'cmdcmd://swipe-down' # dismiss the overlay
open 'cmdcmd://dismiss' # hide the overlay
```

## Keybindings (overlay)

| Key | Action |
Expand Down Expand Up @@ -74,6 +88,8 @@ Right-click the `⌘ ⌘` Dock icon and pick **Open Config…** — that opens `

`animationSpeed` is a multiplier for animated transitions: `1.0` is normal, `2.0` is twice as fast, and `0.5` is half speed.

`previewQuality` controls preview capture resolution: `"efficient"` uses nominal-resolution captures for every tile, `"balanced"` switches to Retina only when you pick, click, peek, or pop a tile, and `"sharp"` asks for Retina captures for every tile.

`trigger` chooses what summons the overlay. Default `"cmd-cmd"` is the both-Command-keys chord. Anything else is treated as a regular hotkey spec — e.g. `"cmd+shift+space"` or `"f13"` (uses the same shortcut grammar as `bindings`). Hotkeys other than the chord require Accessibility permission to be globally observable.

Binding spec — modifier tokens: `cmd`, `shift`, `opt` (or `option`/`alt`), `ctrl`. Special keys: `esc`, `space`, `return`, `delete`, `left`, `right`, `up`, `down`. Anything else is a single character.
Expand All @@ -95,6 +111,12 @@ swift build
.build/debug/cmdcmd
```

Quick performance snapshot of a running app:

```sh
scripts/perf-audit.sh
```

## Releasing

Each user-visible change drops a markdown entry into `.changeset/`:
Expand Down Expand Up @@ -137,11 +159,16 @@ Sources/cmdcmd/
Config.swift # JSON config loader (animations, speed, trigger, bindings)
Keymap.swift # default shortcuts + override resolver
HotkeyMonitor.swift # global hotkey trigger (alternative to CmdChord)
TrackpadSwipeMonitor.swift # 3-finger trackpad swipe trigger
TrackpadGestureSettings.swift # detects/disables conflicting macOS gestures
InputInertiaShield.swift # swallows trailing swipe/scroll inertia after dismiss
MouseClickInterceptor.swift # catches overlay clicks before they fall through
Tile.swift # per-window SCStream preview layer
GridLayout.swift # grid sizing for N tiles at the screen aspect ratio
CmdChord.swift # left+right Command chord detector
SpaceTracker.swift # private CGS/SkyLight space + window enumeration
Log.swift # stderr logger
Sources/CMultitouch/ # tiny MultitouchSupport bridge for trackpad swipes
Resources/ # Info.plist + AppIcon.icns + AppIcon.png
build-app.sh # swift build → .app bundle + ad-hoc codesign
make-icon.sh # regenerate Resources/AppIcon.icns + .png
Expand Down
11 changes: 11 additions & 0 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@
<string>⌘ ⌘</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.p4p8.cmdcmd</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cmdcmd</string>
</array>
</dict>
</array>
<key>CFBundleShortVersionString</key>
<string>0.5.0</string>
<key>CFBundleVersion</key>
Expand Down
228 changes: 228 additions & 0 deletions Sources/CMultitouch/CMultitouch.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#include "CMultitouch.h"

#include <CoreFoundation/CoreFoundation.h>
#include <dlfcn.h>
#include <math.h>
#include <stddef.h>

#define CMD_MAX_TOUCH_DEVICES 16

typedef void *MTDeviceRef;

typedef struct {
float x;
float y;
} MTPoint;

typedef struct {
MTPoint position;
MTPoint velocity;
} MTVector;

typedef struct {
int frame;
double timestamp;
int identifier;
int state;
int fingerID;
int handID;
MTVector normalized;
float size;
int zero1;
float angle;
float majorAxis;
float minorAxis;
MTVector mm;
int zero2[2];
float density;
} Finger;

typedef int (*MTContactCallbackFunction)(int, Finger *, int, double, int);
typedef CFArrayRef (*MTDeviceCreateListFunction)(void);
typedef void (*MTRegisterContactFrameCallbackFunction)(MTDeviceRef, MTContactCallbackFunction);
typedef int (*MTDeviceStartFunction)(MTDeviceRef, int);
typedef int (*MTDeviceStopFunction)(MTDeviceRef);

static void *gFramework = NULL;
static MTDeviceCreateListFunction gCreateList = NULL;
static MTRegisterContactFrameCallbackFunction gRegisterCallback = NULL;
static MTDeviceStartFunction gStartDevice = NULL;
static MTDeviceStopFunction gStopDevice = NULL;

static MTDeviceRef gDevices[CMD_MAX_TOUCH_DEVICES];
static CFIndex gDeviceCount = 0;
static CFArrayRef gDeviceList = NULL;
static CMDSwipeCallback gCallback = NULL;
static void *gContext = NULL;

static bool gTracking = false;
static double gStartX = 0.0;
static double gStartY = 0.0;
static double gLastDX = 0.0;
static double gLastDY = 0.0;

static bool gTapTracking = false;
static double gTapStartTime = 0.0;
static double gTapStartX = 0.0;
static double gTapStartY = 0.0;
static double gTapMaxMovement = 0.0;
static double gTapSuppressedUntil = 0.0;

static const double CMD_TAP_MAX_DURATION = 0.30;
static const double CMD_TAP_MAX_MOVEMENT = 0.035;
static const double CMD_TAP_SUPPRESS_AFTER_SWIPE = 0.25;

static bool load_framework(void) {
if (gFramework) { return true; }

gFramework = dlopen("/System/Library/PrivateFrameworks/MultitouchSupport.framework/MultitouchSupport", RTLD_LAZY);
if (!gFramework) { return false; }

gCreateList = (MTDeviceCreateListFunction)dlsym(gFramework, "MTDeviceCreateList");
gRegisterCallback = (MTRegisterContactFrameCallbackFunction)dlsym(gFramework, "MTRegisterContactFrameCallback");
gStartDevice = (MTDeviceStartFunction)dlsym(gFramework, "MTDeviceStart");
gStopDevice = (MTDeviceStopFunction)dlsym(gFramework, "MTDeviceStop");

return gCreateList && gRegisterCallback && gStartDevice;
}

static void centroid(Finger *fingers, int count, double *x, double *y) {
double sx = 0.0;
double sy = 0.0;
for (int i = 0; i < count; i++) {
sx += fingers[i].normalized.position.x;
sy += fingers[i].normalized.position.y;
}
*x = sx / (double)count;
*y = sy / (double)count;
}

static void finish_tap_if_needed(CMDSwipeCallback callback, double now, int fingerCount) {
if (!gTapTracking) { return; }

double duration = now - gTapStartTime;
bool isTap = fingerCount == 0 &&
duration >= 0.01 &&
duration <= CMD_TAP_MAX_DURATION &&
gTapMaxMovement <= CMD_TAP_MAX_MOVEMENT;
gTapTracking = false;

if (isTap) {
callback(CMDSwipePhaseTap, 0.0, 0.0, gContext);
}
}

static int contact_callback(int device, Finger *fingers, int fingerCount, double timestamp, int frame) {
(void)device;
(void)frame;

CMDSwipeCallback callback = gCallback;
if (!callback) { return 0; }

double now = timestamp > 0.0 ? timestamp : CFAbsoluteTimeGetCurrent();

if (fingerCount == 3 && fingers) {
gTapTracking = false;
double x = 0.0;
double y = 0.0;
centroid(fingers, fingerCount, &x, &y);

if (!gTracking) {
gTracking = true;
gStartX = x;
gStartY = y;
gLastDX = 0.0;
gLastDY = 0.0;
callback(CMDSwipePhaseBegin, 0.0, 0.0, gContext);
return 0;
}

gLastDX = x - gStartX;
gLastDY = y - gStartY;
callback(CMDSwipePhaseUpdate, gLastDX, gLastDY, gContext);
return 0;
}

if (gTracking) {
gTracking = false;
callback(CMDSwipePhaseEnd, gLastDX, gLastDY, gContext);
gLastDX = 0.0;
gLastDY = 0.0;
gTapTracking = false;
gTapSuppressedUntil = now + CMD_TAP_SUPPRESS_AFTER_SWIPE;
return 0;
}

if (fingerCount == 1 && fingers && now >= gTapSuppressedUntil) {
double x = fingers[0].normalized.position.x;
double y = fingers[0].normalized.position.y;
if (!gTapTracking) {
gTapTracking = true;
gTapStartTime = now;
gTapStartX = x;
gTapStartY = y;
gTapMaxMovement = 0.0;
} else {
double movement = hypot(x - gTapStartX, y - gTapStartY);
if (movement > gTapMaxMovement) { gTapMaxMovement = movement; }
}
} else {
finish_tap_if_needed(callback, now, fingerCount);
}

return 0;
}

bool CMDSwipeMonitorStart(CMDSwipeCallback callback, void *context) {
if (!callback || !load_framework()) { return false; }

if (gDeviceCount > 0) {
gCallback = callback;
gContext = context;
return true;
}

CFArrayRef devices = gCreateList();
if (!devices) { return false; }

CFIndex count = CFArrayGetCount(devices);
if (count <= 0) {
CFRelease(devices);
return false;
}

if (count > CMD_MAX_TOUCH_DEVICES) { count = CMD_MAX_TOUCH_DEVICES; }

gCallback = callback;
gContext = context;
gDeviceList = devices;
gDeviceCount = count;

for (CFIndex i = 0; i < count; i++) {
MTDeviceRef device = (MTDeviceRef)CFArrayGetValueAtIndex(devices, i);
gDevices[i] = device;
gRegisterCallback(device, contact_callback);
gStartDevice(device, 0);
}

return true;
}

void CMDSwipeMonitorStop(void) {
for (CFIndex i = 0; i < gDeviceCount; i++) {
if (gStopDevice && gDevices[i]) {
gStopDevice(gDevices[i]);
}
gDevices[i] = NULL;
}
gDeviceCount = 0;
gTracking = false;
gTapTracking = false;
gCallback = NULL;
gContext = NULL;

if (gDeviceList) {
CFRelease(gDeviceList);
gDeviceList = NULL;
}
}
28 changes: 28 additions & 0 deletions Sources/CMultitouch/include/CMultitouch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef CMULTITOUCH_H
#define CMULTITOUCH_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef enum CMDSwipePhase: int32_t {
CMDSwipePhaseBegin = 0,
CMDSwipePhaseUpdate = 1,
CMDSwipePhaseEnd = 2,
CMDSwipePhaseCancel = 3,
CMDSwipePhaseTap = 4,
} CMDSwipePhase;

typedef void (*CMDSwipeCallback)(CMDSwipePhase phase, double deltaX, double deltaY, void *context);

bool CMDSwipeMonitorStart(CMDSwipeCallback callback, void *context);
void CMDSwipeMonitorStop(void);

#ifdef __cplusplus
}
#endif

#endif
Loading
Loading