Add mac app agent and Terminal.app specific keybinds
This commit is contained in:
@@ -25,6 +25,10 @@
|
|||||||
shellHook = ''
|
shellHook = ''
|
||||||
export QMK_HOME="$PWD/qmk_firmware"
|
export QMK_HOME="$PWD/qmk_firmware"
|
||||||
ln -sfn "$PWD/keymaps/custom" "$QMK_HOME/keyboards/system76/launch_3/keymaps/custom"
|
ln -sfn "$PWD/keymaps/custom" "$QMK_HOME/keyboards/system76/launch_3/keymaps/custom"
|
||||||
|
git -C "$QMK_HOME" checkout -- .
|
||||||
|
for p in "$PWD"/patches/*.patch; do
|
||||||
|
[ -f "$p" ] && patch -d "$QMK_HOME" -p1 < "$p"
|
||||||
|
done
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -40,6 +44,9 @@
|
|||||||
|
|
||||||
src = ./qmk_firmware;
|
src = ./qmk_firmware;
|
||||||
|
|
||||||
|
patches = builtins.map (f: ./patches + "/${f}")
|
||||||
|
(builtins.attrNames (builtins.readDir ./patches));
|
||||||
|
|
||||||
postUnpack = ''
|
postUnpack = ''
|
||||||
cp -r ${./keymaps/custom} $sourceRoot/keyboards/system76/launch_3/keymaps/custom
|
cp -r ${./keymaps/custom} $sourceRoot/keyboards/system76/launch_3/keymaps/custom
|
||||||
'';
|
'';
|
||||||
|
|||||||
51
keymaps/custom/app_focus.c
Normal file
51
keymaps/custom/app_focus.c
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#include "raw_hid.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "app_focus.h"
|
||||||
|
|
||||||
|
#define CMD_SET_FOCUSED_APP 0x80
|
||||||
|
#define APP_NAME_BUF_SIZE 64
|
||||||
|
|
||||||
|
focused_app_t focused_app = APP_UNKNOWN;
|
||||||
|
|
||||||
|
static char app_name_buf[APP_NAME_BUF_SIZE];
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
focused_app_t app;
|
||||||
|
} app_mapping_t;
|
||||||
|
|
||||||
|
static const app_mapping_t app_map[] = {
|
||||||
|
{ "Terminal", APP_TERMINAL },
|
||||||
|
{ "iTerm2", APP_TERMINAL },
|
||||||
|
{ "Xcode", APP_XCODE },
|
||||||
|
{ "Code", APP_VSCODE }, // "Code" is VSCode's localizedName
|
||||||
|
{ "Visual Studio Code", APP_VSCODE },
|
||||||
|
};
|
||||||
|
|
||||||
|
static focused_app_t resolve_app(const char *name) {
|
||||||
|
for (uint8_t i = 0; i < sizeof(app_map) / sizeof(app_map[0]); i++) {
|
||||||
|
if (strcmp(name, app_map[i].name) == 0) {
|
||||||
|
return app_map[i].app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APP_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from system76_ec.c's raw_hid_receive (injected via sed at build time).
|
||||||
|
// Returns true if the command was handled.
|
||||||
|
bool app_focus_raw_hid_intercept(uint8_t *data, uint8_t length) {
|
||||||
|
if (data[0] == CMD_SET_FOCUSED_APP) {
|
||||||
|
uint8_t copy_len = length - 1;
|
||||||
|
if (copy_len >= APP_NAME_BUF_SIZE) {
|
||||||
|
copy_len = APP_NAME_BUF_SIZE - 1;
|
||||||
|
}
|
||||||
|
memcpy(app_name_buf, &data[1], copy_len);
|
||||||
|
app_name_buf[copy_len] = '\0';
|
||||||
|
focused_app = resolve_app(app_name_buf);
|
||||||
|
data[1] = 0; // success
|
||||||
|
raw_hid_send(data, length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
15
keymaps/custom/app_focus.h
Normal file
15
keymaps/custom/app_focus.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
APP_UNKNOWN,
|
||||||
|
APP_TERMINAL, // Terminal.app, iTerm2
|
||||||
|
APP_XCODE,
|
||||||
|
APP_VSCODE, // Visual Studio Code
|
||||||
|
} focused_app_t;
|
||||||
|
|
||||||
|
extern focused_app_t focused_app;
|
||||||
|
|
||||||
|
bool app_focus_raw_hid_intercept(uint8_t *data, uint8_t length);
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "keymap_introspection.h"
|
#include "keymap_introspection.h"
|
||||||
#include "dynamic_keymap.h"
|
#include "dynamic_keymap.h"
|
||||||
#include "os_mode.h"
|
#include "os_mode.h"
|
||||||
|
#include "app_focus.h"
|
||||||
|
|
||||||
// System76 per-layer RGB state (defined in system76_ec.c, declared in launch_3.c)
|
// System76 per-layer RGB state (defined in system76_ec.c, declared in launch_3.c)
|
||||||
extern rgb_config_t layer_rgb[];
|
extern rgb_config_t layer_rgb[];
|
||||||
@@ -26,6 +27,7 @@ static const mod_rewrite_t ALT_TO_SUPER = { MOD_MASK_ALT,
|
|||||||
static const mod_rewrite_t SUPER_TO_SUPER = { MOD_MASK_GUI, MOD_MASK_GUI };
|
static const mod_rewrite_t SUPER_TO_SUPER = { MOD_MASK_GUI, MOD_MASK_GUI };
|
||||||
static const mod_rewrite_t SUPER_TO_CTRL = { MOD_MASK_GUI, MOD_MASK_CTRL };
|
static const mod_rewrite_t SUPER_TO_CTRL = { MOD_MASK_GUI, MOD_MASK_CTRL };
|
||||||
static const mod_rewrite_t NONE_TO_SUPER = { 0, MOD_MASK_GUI };
|
static const mod_rewrite_t NONE_TO_SUPER = { 0, MOD_MASK_GUI };
|
||||||
|
static const mod_rewrite_t NONE_TO_CTRL = { 0, MOD_MASK_CTRL };
|
||||||
|
|
||||||
// Collapse left/right mods into modifier types for comparison.
|
// Collapse left/right mods into modifier types for comparison.
|
||||||
// e.g. either LCTL (0x01) or RCTL (0x10) both become 0x01.
|
// e.g. either LCTL (0x01) or RCTL (0x10) both become 0x01.
|
||||||
@@ -34,9 +36,9 @@ static uint8_t normalize_mods(uint8_t mods) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if current mods exactly match a rule's `from` mods.
|
// Check if current mods exactly match a rule's `from` mods.
|
||||||
// Returns false if FN layer is active (skip rewrites when FN is held).
|
// Returns false if FN layer is active (skip rewrites when FN + any modifier is held).
|
||||||
static bool mods_match(uint8_t mods, mod_rewrite_t rule) {
|
static bool mods_match(uint8_t mods, mod_rewrite_t rule) {
|
||||||
if (layer_state_is(1)) return false;
|
if (layer_state_is(1) && get_mods() != 0) return false;
|
||||||
return normalize_mods(mods) == normalize_mods(rule.from);
|
return normalize_mods(mods) == normalize_mods(rule.from);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +225,11 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
|||||||
case KC_C:
|
case KC_C:
|
||||||
case KC_V:
|
case KC_V:
|
||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false;
|
|
||||||
if (rewrite_mods(keycode, CTRL_SHIFT_TO_SUPER)) return false;
|
if (rewrite_mods(keycode, CTRL_SHIFT_TO_SUPER)) return false;
|
||||||
|
// Terminal: plain Ctrl+C/V passes through (SIGINT, etc.)
|
||||||
|
if (focused_app != APP_TERMINAL) {
|
||||||
|
if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Mac mode rewrites:
|
// Mac mode rewrites:
|
||||||
@@ -274,11 +279,19 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
|||||||
// End -> Super+Right (line end)
|
// End -> Super+Right (line end)
|
||||||
case KC_HOME:
|
case KC_HOME:
|
||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
|
// Terminal: Home -> Ctrl+A (line start)
|
||||||
|
if (focused_app == APP_TERMINAL) {
|
||||||
|
if (rewrite_mods_and_key(NONE_TO_CTRL, KC_A)) return false;
|
||||||
|
}
|
||||||
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_LEFT)) return false;
|
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_LEFT)) return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KC_END:
|
case KC_END:
|
||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
|
// Terminal: End -> Ctrl+E (line end)
|
||||||
|
if (focused_app == APP_TERMINAL) {
|
||||||
|
if (rewrite_mods_and_key(NONE_TO_CTRL, KC_E)) return false;
|
||||||
|
}
|
||||||
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_RGHT)) return false;
|
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_RGHT)) return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
OS_DETECTION_ENABLE = yes
|
OS_DETECTION_ENABLE = yes
|
||||||
SRC += os_detect.c
|
SRC += os_detect.c
|
||||||
|
SRC += app_focus.c
|
||||||
|
|||||||
21
mac-agent/com.user.kb-focus.plist
Normal file
21
mac-agent/com.user.kb-focus.plist
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.user.kb-focus</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/bin/python3</string>
|
||||||
|
<string>kb-focus.py</string>
|
||||||
|
</array>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/USER/keyboard/mac-agent</string>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/tmp/kb-focus.stdout.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/tmp/kb-focus.stderr.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
67
mac-agent/kb-focus.py
Normal file
67
mac-agent/kb-focus.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import signal, sys
|
||||||
|
from AppKit import NSWorkspace
|
||||||
|
from Foundation import NSRunLoop, NSDate
|
||||||
|
import hid
|
||||||
|
|
||||||
|
VID, PID = 0x3384, 0x0009
|
||||||
|
USAGE_PAGE, USAGE = 0xFF60, 0x61
|
||||||
|
CMD_SET_FOCUSED_APP = 0x80
|
||||||
|
|
||||||
|
def find_keyboard_path():
|
||||||
|
for info in hid.enumerate(VID, PID):
|
||||||
|
if info["usage_page"] == USAGE_PAGE and info["usage"] == USAGE:
|
||||||
|
return info["path"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_app_name(dev, name):
|
||||||
|
payload = name.encode("utf-8", errors="ignore")[:31]
|
||||||
|
payload = payload.decode("utf-8", errors="ignore").encode("utf-8")
|
||||||
|
data = bytes([0x00, CMD_SET_FOCUSED_APP]) + payload + b"\x00" * (32 - 1 - len(payload))
|
||||||
|
dev.write(data)
|
||||||
|
dev.read(32, timeout=1000)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
||||||
|
ws = NSWorkspace.sharedWorkspace()
|
||||||
|
dev = None
|
||||||
|
last_app = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
connected = find_keyboard_path() is not None
|
||||||
|
|
||||||
|
# Detect disconnect
|
||||||
|
if dev is not None and not connected:
|
||||||
|
print("Keyboard disconnected")
|
||||||
|
dev = None
|
||||||
|
last_app = None
|
||||||
|
|
||||||
|
# Connect and send current app
|
||||||
|
if dev is None and connected:
|
||||||
|
try:
|
||||||
|
dev = hid.Device(path=find_keyboard_path())
|
||||||
|
print("Keyboard connected")
|
||||||
|
last_app = ws.frontmostApplication().localizedName()
|
||||||
|
print(f"App: {last_app}")
|
||||||
|
send_app_name(dev, last_app)
|
||||||
|
except Exception:
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
# App change
|
||||||
|
if dev is not None:
|
||||||
|
app = ws.frontmostApplication().localizedName()
|
||||||
|
if app != last_app:
|
||||||
|
print(f"App: {app}")
|
||||||
|
last_app = app
|
||||||
|
try:
|
||||||
|
send_app_name(dev, app)
|
||||||
|
except Exception:
|
||||||
|
print("Keyboard disconnected")
|
||||||
|
dev = None
|
||||||
|
last_app = None
|
||||||
|
|
||||||
|
NSRunLoop.currentRunLoop().runUntilDate_(
|
||||||
|
NSDate.dateWithTimeIntervalSinceNow_(0.25)
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
mac-agent/requirements.txt
Normal file
2
mac-agent/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pyobjc-framework-Cocoa
|
||||||
|
hid
|
||||||
15
patches/app_focus_intercept.patch
Normal file
15
patches/app_focus_intercept.patch
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
--- a/keyboards/system76/system76_ec.c
|
||||||
|
+++ b/keyboards/system76/system76_ec.c
|
||||||
|
@@ -1,3 +1,4 @@
|
||||||
|
+#include "app_focus.h"
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 System76
|
||||||
|
* Copyright (C) 2021 Jimmy Cassis <KernelOops@outlook.com>
|
||||||
|
@@ -278,6 +279,7 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
void raw_hid_receive(uint8_t *data, uint8_t length) {
|
||||||
|
+ if (app_focus_raw_hid_intercept(data, length)) return;
|
||||||
|
// Error response by default, set to success by commands
|
||||||
|
data[1] = 1;
|
||||||
|
|
||||||
Reference in New Issue
Block a user