From 843190633714dbb8fe68360baca5d80ecb434dd3 Mon Sep 17 00:00:00 2001 From: googlebot Date: Sat, 14 Mar 2026 19:02:08 -0700 Subject: [PATCH] Basic mac vs linux modes --- .gitignore | 1 - .gitmodules | 3 + flake.nix | 19 +++- keymaps/custom/keymap.c | 217 ++++++++++++++++++++++++++++++++++++- keymaps/custom/mac_mode.h | 6 + keymaps/custom/os_detect.c | 31 ++++++ keymaps/custom/rules.mk | 2 + qmk_firmware | 1 + shell.nix | 1 + 9 files changed, 271 insertions(+), 10 deletions(-) create mode 100644 .gitmodules create mode 100644 keymaps/custom/mac_mode.h create mode 100644 keymaps/custom/os_detect.c create mode 100644 keymaps/custom/rules.mk create mode 160000 qmk_firmware create mode 100644 shell.nix diff --git a/.gitignore b/.gitignore index 7b1a688..510afb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ result .build/ -qmk_firmware/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f696703 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "qmk_firmware"] + path = qmk_firmware + url = https://github.com/system76/qmk_firmware.git diff --git a/flake.nix b/flake.nix index c96322f..f5e88be 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + self.submodules = true; }; outputs = { self, nixpkgs }: @@ -23,11 +24,7 @@ shellHook = '' export QMK_HOME="$PWD/qmk_firmware" - - # Symlink tracked keymap into QMK tree - if [ -d "$QMK_HOME/keyboards/system76/launch_3" ]; then - ln -sfn "$PWD/keymaps/custom" "$QMK_HOME/keyboards/system76/launch_3/keymaps/custom" - fi + ln -sfn "$PWD/keymaps/custom" "$QMK_HOME/keyboards/system76/launch_3/keymaps/custom" ''; }; }); @@ -35,20 +32,32 @@ packages = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; in { + default = self.packages.${system}.firmware; + firmware = pkgs.stdenv.mkDerivation { pname = "launch3-firmware"; version = "0.1.0"; src = ./qmk_firmware; + postUnpack = '' + cp -r ${./keymaps/custom} $sourceRoot/keyboards/system76/launch_3/keymaps/custom + ''; + + postPatch = '' + patchShebangs util/uf2conv.py + ''; + nativeBuildInputs = [ pkgs.qmk pkgs.python3Packages.appdirs + pkgs.python3 ]; buildPhase = '' export HOME=$(mktemp -d) export QMK_HOME="$PWD" + export SKIP_GIT=true qmk compile -kb system76/launch_3 -km custom ''; diff --git a/keymaps/custom/keymap.c b/keymaps/custom/keymap.c index 3efec2d..1e83406 100644 --- a/keymaps/custom/keymap.c +++ b/keymaps/custom/keymap.c @@ -1,4 +1,66 @@ #include QMK_KEYBOARD_H +#include "keymap_introspection.h" +#include "dynamic_keymap.h" +#include "mac_mode.h" + +enum custom_keycodes { + CK_MCMD = SAFE_RANGE, // toggle mac-command mode +}; + +typedef struct { + uint8_t from; + uint8_t to; +} mod_rewrite_t; + +static const mod_rewrite_t CTRL_TO_SUPER = { MOD_MASK_CTRL, MOD_MASK_GUI }; +static const mod_rewrite_t CTRL_SHIFT_TO_SUPER = { MOD_MASK_CTRL | MOD_MASK_SHIFT, MOD_MASK_GUI }; +static const mod_rewrite_t CTRL_SHIFT_TO_CMD_SHIFT = { MOD_MASK_CTRL | MOD_MASK_SHIFT, MOD_MASK_GUI | MOD_MASK_SHIFT }; +static const mod_rewrite_t CTRL_ALT_TO_CTRL = { MOD_MASK_CTRL | MOD_MASK_ALT, MOD_MASK_CTRL }; +static const mod_rewrite_t CTRL_TO_ALT = { MOD_MASK_CTRL, MOD_MASK_ALT }; +static const mod_rewrite_t ALT_TO_SUPER = { MOD_MASK_ALT, 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 NONE_TO_SUPER = { 0, MOD_MASK_GUI }; + +// Collapse left/right mods into modifier types for comparison. +// e.g. either LCTL (0x01) or RCTL (0x10) both become 0x01. +static uint8_t normalize_mods(uint8_t mods) { + return (mods | (mods >> 4)) & 0x0F; +} + +// Check if current mods exactly match a rule's `from` mods. +// Returns false if FN layer is active (skip rewrites when FN is held). +static bool mods_match(uint8_t mods, mod_rewrite_t rule) { + if (layer_state_is(1)) return false; + return normalize_mods(mods) == normalize_mods(rule.from); +} + +// Rewrite modifier combos: swap exact `from` mod types to `to` mods, tap keycode, restore. +// Left/right variants of the same modifier are treated as equivalent. +// Returns true if rewrite occurred. +static bool rewrite_mods(uint16_t keycode, mod_rewrite_t rule) { + uint8_t mods = get_mods(); + if (mods_match(mods, rule)) { + set_mods(normalize_mods(rule.to)); + tap_code(keycode); + set_mods(mods); + return true; + } + return false; +} + +// Rewrite modifier combo AND change the keycode sent. +// Returns true if rewrite occurred. +static bool rewrite_mods_and_key(mod_rewrite_t rule, uint16_t keycode_out) { + uint8_t mods = get_mods(); + if (mods_match(mods, rule)) { + set_mods(normalize_mods(rule.to)); + tap_code(keycode_out); + set_mods(mods); + return true; + } + return false; +} const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { @@ -20,7 +82,7 @@ ________________________________________________________________________________ | SHIFT | Z | X | C | V | B | N | M | < | > | ? | SHIFT | UP | |________________|________|________|________|________|________|________|________|________|________|________|____________|________|_________ | | | | | | | | | | | | | | - | CTRL | LALT | FN | LGUI | SPACE | SPACE | RCTRL | RALT | FN | | LEFT | DOWN | RIGHT | + | CTRL | FN | LGUI | LALT | SPACE | SPACE | RALT | RCTRL | FN | | LEFT | DOWN | RIGHT | |____________|________|_______|________|_________________|_________________|________|________|_____________| |________|________|________| */ @@ -30,13 +92,13 @@ ________________________________________________________________________________ KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_END, KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, - KC_LCTL, KC_LALT, MO(1), KC_LGUI, KC_SPC, KC_SPC, KC_RCTL, KC_RALT, MO(1), KC_LEFT, KC_DOWN, KC_RGHT + KC_LCTL, MO(1), KC_LGUI, KC_LALT, KC_SPC, KC_SPC, KC_RALT, KC_RCTL, MO(1), KC_LEFT, KC_DOWN, KC_RGHT ), /* Layer 1, function layer __________________________________________________________________________________________________________________________________ ________ | | | | | | | | | | | | | | || PLAY/ | -| QK_BOOT | | | | | | | | | | | | | || PAUSE | +|QK_BOOT | | | | | | | | | | | | | || PAUSE | |________|________|________|________|________|________|________|________|________|________|________|________|________|____________||________| | | | | | | | | | | | LED | LED | LED | || VOLUME | | | | | | | | | | | | TOGGLE | DOWN | UP | || UP | @@ -59,7 +121,7 @@ ________________________________________________________________________________ */ [1] = LAYOUT( - QK_BOOT, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_MPLY, + QK_BOOT, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, CK_MCMD, KC_MPLY, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, RGB_TOG, RGB_VAD, RGB_VAI, KC_TRNS, KC_VOLU, KC_PSCR, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_HOME, KC_PGDN, KC_PGUP, KC_END, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_VOLD, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_LEFT, KC_DOWN, KC_UP, KC_RGHT, KC_TRNS, KC_TRNS, KC_TRNS, KC_MUTE, @@ -86,6 +148,153 @@ ________________________________________________________________________________ ), }; +// Override layer resolution: in mac mode, any modifier forces layer 0 (ignores FN) +uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key) { + if (mac_mode && (get_mods() & (MOD_MASK_CTRL | MOD_MASK_SHIFT | MOD_MASK_ALT | MOD_MASK_GUI))) { + layer = 0; + } + if (key.row < MATRIX_ROWS && key.col < MATRIX_COLS) { + return keycode_at_keymap_location(layer, key.row, key.col); + } + return KC_NO; +} + +static bool gui_tapped = false; + bool process_record_user(uint16_t keycode, keyrecord_t *record) { + // Any other key pressed while GUI held means it's being used as a modifier + if (record->event.pressed && keycode != KC_LGUI) { + gui_tapped = false; + } + + switch (keycode) { + // Mac mode: tap Super -> Cmd+Space (Spotlight) + // Linux: tap Super passes through normally (KDE app menu) + case KC_LGUI: + if (mac_mode) { + if (record->event.pressed) { + gui_tapped = true; + register_mods(MOD_BIT(KC_LGUI)); + } else { + if (gui_tapped) { + tap_code(KC_SPC); // GUI still held, sends Super+Space + gui_tapped = false; + } + unregister_mods(MOD_BIT(KC_LGUI)); + } + return false; + } + break; + case CK_MCMD: + if (record->event.pressed) { + mac_mode = !mac_mode; + mac_mode_manual = true; + // Flash LEDs to indicate mode: green = mac, red = normal + rgb_matrix_mode_noeeprom(RGB_MATRIX_SOLID_COLOR); + if (mac_mode) { + rgb_matrix_sethsv_noeeprom(HSV_GREEN); + } else { + rgb_matrix_sethsv_noeeprom(HSV_BLUE); + } + } + return false; + // Mac mode rewrites: + // Ctrl+C/V -> Super+C/V (copy/paste) + // Ctrl+Shift+C/V -> Super+C/V (terminal copy/paste) + case KC_C: + case KC_V: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; + if (rewrite_mods(keycode, CTRL_SHIFT_TO_SUPER)) return false; + } + break; + // Mac mode rewrites: + // Ctrl+X/A/S/F -> Super+X/A/S/F (cut, select all, save, find) + // Ctrl+W/L/R -> Super+W/L/R (close tab, address bar, reload) + case KC_X: + case KC_A: + case KC_S: + case KC_F: + case KC_W: + case KC_L: + case KC_R: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; + } + break; + // Mac mode rewrites: + // Ctrl+Z/T/N -> Super+Z/T/N (undo, new tab, new window) + // Ctrl+Shift+Z/T/N -> Super+Shift+Z/T/N (redo, reopen tab, incognito) + case KC_Z: + case KC_T: + case KC_N: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; + if (rewrite_mods(keycode, CTRL_SHIFT_TO_CMD_SHIFT)) return false; + } + break; + // Mac mode rewrites: + // Ctrl+Alt+Left/Right -> Ctrl+Left/Right (switch workspace) + // Ctrl+Left/Right -> Alt+Left/Right (word navigation) + case KC_LEFT: + case KC_RGHT: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, CTRL_ALT_TO_CTRL)) return false; + if (rewrite_mods(keycode, CTRL_TO_ALT)) return false; + } + break; + // Mac mode rewrites: + // Ctrl+Backspace -> Alt+Backspace (delete word backward) + case KC_BSPC: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, CTRL_TO_ALT)) return false; + } + break; + // Mac mode rewrites: + // Home -> Super+Left (line start) + // End -> Super+Right (line end) + case KC_HOME: + if (mac_mode && record->event.pressed) { + if (rewrite_mods_and_key(NONE_TO_SUPER, KC_LEFT)) return false; + } + break; + case KC_END: + if (mac_mode && record->event.pressed) { + if (rewrite_mods_and_key(NONE_TO_SUPER, KC_RGHT)) return false; + } + break; + // Mac mode rewrites: + // Alt+F4 -> Super+Q (quit app) + case KC_F4: + if (mac_mode && record->event.pressed) { + if (rewrite_mods_and_key(ALT_TO_SUPER, KC_Q)) return false; + } + break; + // Mac mode rewrites: + // Alt+Tab -> Super+Tab (app switcher) + case KC_TAB: + if (mac_mode && record->event.pressed) { + if (rewrite_mods(keycode, ALT_TO_SUPER)) return false; + } + break; + // Workspace overview (both modes): + // Super+Up -> Super+W (KDE overview, linux) + // Super+Up -> Ctrl+Up (Mission Control, mac) + case KC_UP: + if (record->event.pressed) { + if (mac_mode) { + if (rewrite_mods(keycode, SUPER_TO_CTRL)) return false; + } else { + if (rewrite_mods_and_key(SUPER_TO_SUPER, KC_W)) return false; + } + } + break; + } return true; } + +void matrix_init_user(void) { + // Force dynamic keymap reset so PROGMEM keymaps always take effect + dynamic_keymap_reset(); +} + diff --git a/keymaps/custom/mac_mode.h b/keymaps/custom/mac_mode.h new file mode 100644 index 0000000..148d337 --- /dev/null +++ b/keymaps/custom/mac_mode.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +extern bool mac_mode; +extern bool mac_mode_manual; diff --git a/keymaps/custom/os_detect.c b/keymaps/custom/os_detect.c new file mode 100644 index 0000000..d736d93 --- /dev/null +++ b/keymaps/custom/os_detect.c @@ -0,0 +1,31 @@ +#include "os_detection.h" +#include "timer.h" +#include "rgb_matrix.h" + +#include "mac_mode.h" + +bool mac_mode = false; +bool mac_mode_manual = false; +static bool os_detected = false; +static uint32_t flash_start = 0; + +// Poll OS detection after USB enumeration settles (~2s) +// Flash indicator color for 500ms then restore default animation +void matrix_scan_user(void) { + if (!os_detected && !mac_mode_manual && timer_elapsed32(0) > 2000) { + os_detected = true; + os_variant_t os = detected_host_os(); + mac_mode = (os == OS_MACOS || os == OS_IOS); + rgb_matrix_mode_noeeprom(RGB_MATRIX_SOLID_COLOR); + if (mac_mode) { + rgb_matrix_sethsv_noeeprom(HSV_GREEN); + } else { + rgb_matrix_sethsv_noeeprom(HSV_BLUE); + } + flash_start = timer_read32(); + } + if (flash_start && timer_elapsed32(flash_start) > 500) { + flash_start = 0; + rgb_matrix_reload_from_eeprom(); + } +} diff --git a/keymaps/custom/rules.mk b/keymaps/custom/rules.mk new file mode 100644 index 0000000..a5f0b67 --- /dev/null +++ b/keymaps/custom/rules.mk @@ -0,0 +1,2 @@ +OS_DETECTION_ENABLE = yes +SRC += os_detect.c diff --git a/qmk_firmware b/qmk_firmware new file mode 160000 index 0000000..473ce8e --- /dev/null +++ b/qmk_firmware @@ -0,0 +1 @@ +Subproject commit 473ce8e7b472e23dfab6f411d0792dbf4b990700 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..71a377d --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(builtins.getFlake (toString ./.)).devShells.${builtins.currentSystem}.default