mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-01-26 11:14:08 +00:00
feat: hammerspoon & karabiner-elements
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ config/zed/settings.json
|
||||
*.tmp.*
|
||||
config/op/plugins/gh.json
|
||||
config/fish/fish_variables.*
|
||||
config/karabiner/automatic_backups
|
||||
|
||||
15
base/hammerspoon/.luacheckrc
Normal file
15
base/hammerspoon/.luacheckrc
Normal file
@@ -0,0 +1,15 @@
|
||||
--
|
||||
-- These globals can be set and accessed:
|
||||
--
|
||||
globals = {
|
||||
"rawrequire",
|
||||
}
|
||||
|
||||
--
|
||||
-- These globals can only be accessed:
|
||||
--
|
||||
read_globals = {
|
||||
"hs",
|
||||
"ls",
|
||||
"spoon",
|
||||
}
|
||||
1010
base/hammerspoon/generate-hammerspoon-types.py
Executable file
1010
base/hammerspoon/generate-hammerspoon-types.py
Executable file
File diff suppressed because it is too large
Load Diff
17863
base/hammerspoon/hammerspoon.types.lua
Normal file
17863
base/hammerspoon/hammerspoon.types.lua
Normal file
File diff suppressed because it is too large
Load Diff
204
base/hammerspoon/init.lua
Normal file
204
base/hammerspoon/init.lua
Normal file
@@ -0,0 +1,204 @@
|
||||
-- ╭─────────────────────────────────────────────────────────╮
|
||||
-- │ Hammerspoon config file │
|
||||
-- ╰─────────────────────────────────────────────────────────╯
|
||||
-- init.lua — Pure Hammerspoon window controls
|
||||
-- Converted from skhdrc logic; expanded with per‑display grids,
|
||||
-- app rules with groups, wrap‑around focus, cross‑display moves,
|
||||
-- and overlay/notification toggles.
|
||||
-- Author: Ismo Vuorinen (ivuorinen)
|
||||
|
||||
--------------------------------------------------
|
||||
-- Caps Lock as Meh key (Shift+Control+Alt)
|
||||
--------------------------------------------------
|
||||
-- Prerequisites:
|
||||
-- 1. Go to System Settings → Keyboard → Keyboard Shortcuts → Modifier Keys
|
||||
-- 2. Set Caps Lock to "No Action" (you mentioned you already did this)
|
||||
-- 3. Install Karabiner-Elements: brew install --cask karabiner-elements
|
||||
-- 4. Open Karabiner-Elements, go to "Simple Modifications"
|
||||
-- 5. Add: caps_lock → f18
|
||||
--
|
||||
-- Then you can use F18 as your Meh key in Hammerspoon:
|
||||
local f18 = hs.hotkey.modal.new()
|
||||
|
||||
-- Capture F18 key press/release
|
||||
hs.hotkey.bind({}, 'F18', function()
|
||||
f18:enter()
|
||||
end, function()
|
||||
f18:exit()
|
||||
end)
|
||||
|
||||
-- Meh (F18/Caps Lock) key bindings for window management
|
||||
-- These provide quick access to common window operations
|
||||
|
||||
-- Helper function to get focused window
|
||||
local function W()
|
||||
return hs.window.focusedWindow()
|
||||
end
|
||||
|
||||
-- Window positioning: thirds (U/I/O)
|
||||
f18:bind({}, 'u', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit({ x = 0, y = 0, w = 1 / 3, h = 1 }, 0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'i', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit({ x = 1 / 3, y = 0, w = 1 / 3, h = 1 }, 0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'o', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit({ x = 2 / 3, y = 0, w = 1 / 3, h = 1 }, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Window positioning: half width, full height (Y)
|
||||
f18:bind({}, 'y', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit({ x = 0, y = 0, w = 0.5, h = 1 }, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Cycle through all windows (H/L)
|
||||
-- We need to maintain state to properly cycle through all windows
|
||||
local windowCycleIndex = 1
|
||||
local windowCycleList = {}
|
||||
local lastCycleTime = 0
|
||||
|
||||
local function getWindowCycleList()
|
||||
local currentTime = hs.timer.secondsSinceEpoch()
|
||||
-- Reset if more than 2 seconds have passed since last cycle
|
||||
if currentTime - lastCycleTime > 2 then
|
||||
windowCycleIndex = 1
|
||||
windowCycleList = hs.window.orderedWindows()
|
||||
end
|
||||
lastCycleTime = currentTime
|
||||
return windowCycleList
|
||||
end
|
||||
|
||||
f18:bind({}, 'h', function()
|
||||
local windows = getWindowCycleList()
|
||||
if #windows <= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Cycle backward
|
||||
windowCycleIndex = windowCycleIndex - 1
|
||||
if windowCycleIndex < 1 then
|
||||
windowCycleIndex = #windows
|
||||
end
|
||||
|
||||
windows[windowCycleIndex]:focus()
|
||||
end)
|
||||
|
||||
f18:bind({}, 'l', function()
|
||||
local windows = getWindowCycleList()
|
||||
if #windows <= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Cycle forward
|
||||
windowCycleIndex = windowCycleIndex + 1
|
||||
if windowCycleIndex > #windows then
|
||||
windowCycleIndex = 1
|
||||
end
|
||||
|
||||
windows[windowCycleIndex]:focus()
|
||||
end)
|
||||
|
||||
-- Window sizing: maximize (Up/J) and center (Down)
|
||||
f18:bind({}, 'up', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:maximize(0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'j', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:maximize(0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'down', function()
|
||||
local w = W()
|
||||
if not w then
|
||||
return
|
||||
end
|
||||
local f = w:frame()
|
||||
local sf = w:screen():frame()
|
||||
if f.w < sf.w * 0.95 then
|
||||
w:maximize(0)
|
||||
else
|
||||
local ww, hh = math.floor(sf.w * 0.5), math.floor(sf.h * 0.9)
|
||||
local xx = sf.x + math.floor((sf.w - ww) / 2)
|
||||
local yy = sf.y + math.floor((sf.h - hh) / 2)
|
||||
w:setFrame({ x = xx, y = yy, w = ww, h = hh }, 0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'k', function()
|
||||
local w = W()
|
||||
if w then
|
||||
local sf = w:screen():frame()
|
||||
local ww, hh = math.floor(sf.w * 0.9), math.floor(sf.h * 0.9)
|
||||
local xx = sf.x + math.floor((sf.w - ww) / 2)
|
||||
local yy = sf.y + math.floor((sf.h - hh) / 2)
|
||||
w:setFrame({ x = xx, y = yy, w = ww, h = hh }, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Move to next/previous screen (. and ,)
|
||||
f18:bind({}, '.', function()
|
||||
local w = W()
|
||||
if w then
|
||||
local s = w:screen()
|
||||
local ns = s:toEast() or s:toWest()
|
||||
if ns then
|
||||
w:moveToScreen(ns, true, true, 0)
|
||||
end
|
||||
end
|
||||
end)
|
||||
f18:bind({}, ',', function()
|
||||
local w = W()
|
||||
if w then
|
||||
local s = w:screen()
|
||||
local ps = s:toWest() or s:toEast()
|
||||
if ps then
|
||||
w:moveToScreen(ps, true, true, 0)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Window positioning: halves (Left/Right arrows)
|
||||
f18:bind({}, 'left', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit(hs.layout.left50, 0)
|
||||
end
|
||||
end)
|
||||
f18:bind({}, 'right', function()
|
||||
local w = W()
|
||||
if w then
|
||||
w:moveToUnit(hs.layout.right50, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Paste from clipboard with Meh + V
|
||||
f18:bind({}, 'v', function()
|
||||
hs.eventtap.keyStrokes(hs.pasteboard.getContents())
|
||||
end)
|
||||
|
||||
-- Paste 1Password secret with Meh + P
|
||||
f18:bind({}, 'p', function()
|
||||
local output, status = hs.execute('op read "op://Svea/3hzhctmvovbwlgulv7mgy25rf4/login-input"', true)
|
||||
if status then
|
||||
hs.eventtap.keyStrokes(output:gsub('%s+$', '')) -- trim trailing whitespace
|
||||
else
|
||||
hs.alert.show('1Password CLI error')
|
||||
end
|
||||
end)
|
||||
|
||||
-- require 'generate_emmylua'
|
||||
@@ -38,6 +38,8 @@ brew "openssl@3"
|
||||
brew "cryptography"
|
||||
# YAML Parser
|
||||
brew "libyaml"
|
||||
# Display directories as trees (with optional color/HTML output)
|
||||
brew "tree"
|
||||
# Automate deployment, configuration, and upgrading
|
||||
brew "ansible"
|
||||
# Checks ansible playbooks for practices and behaviour
|
||||
@@ -104,7 +106,7 @@ brew "glib"
|
||||
brew "cargo-binstall"
|
||||
# Multi-platform support library with a focus on asynchronous I/O
|
||||
brew "libuv"
|
||||
# Platform built on V8 to build network applications
|
||||
# Open-source, cross-platform JavaScript runtime environment
|
||||
brew "node", link: false
|
||||
# CLI tool for analyzing Claude Code usage from local JSONL files
|
||||
brew "ccusage"
|
||||
@@ -230,6 +232,8 @@ brew "luarocks"
|
||||
brew "lzip"
|
||||
# Swiss Army Knife for macOS
|
||||
brew "m-cli"
|
||||
# Cross platform, open source .NET development framework
|
||||
brew "mono"
|
||||
# Collection of tools that nobody wrote when UNIX was young
|
||||
brew "moreutils"
|
||||
# NCurses Disk Usage
|
||||
@@ -290,8 +294,8 @@ brew "tflint"
|
||||
brew "tfsec"
|
||||
# Terminal multiplexer
|
||||
brew "tmux"
|
||||
# Display directories as trees (with optional color/HTML output)
|
||||
brew "tree"
|
||||
# Extremely fast Python package installer and resolver, written in Rust
|
||||
brew "uv"
|
||||
# Tool for creating isolated virtual python environments
|
||||
brew "virtualenv"
|
||||
# Command-line interface to the WakaTime api
|
||||
@@ -349,13 +353,18 @@ cask "fantastical"
|
||||
cask "font-jetbrains-mono"
|
||||
cask "font-jetbrains-mono-nerd-font"
|
||||
cask "font-monaspace"
|
||||
cask "font-monaspace-nf"
|
||||
cask "font-open-sans"
|
||||
# GIT client
|
||||
cask "fork"
|
||||
# Desktop automation application
|
||||
cask "hammerspoon"
|
||||
# HTTP and GraphQL Client
|
||||
cask "insomnia"
|
||||
# JetBrains tools manager
|
||||
cask "jetbrains-toolbox"
|
||||
# Keyboard customiser
|
||||
cask "karabiner-elements"
|
||||
# End-to-end encryption software
|
||||
cask "keybase"
|
||||
# Kubernetes IDE
|
||||
|
||||
68
config/karabiner/karabiner.json
Normal file
68
config/karabiner/karabiner.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"complex_modifications": {
|
||||
"rules": [
|
||||
{
|
||||
"description": "Change right_command+hjkl to arrow keys",
|
||||
"manipulators": [
|
||||
{
|
||||
"from": {
|
||||
"key_code": "h",
|
||||
"modifiers": {
|
||||
"mandatory": ["right_command"],
|
||||
"optional": ["any"]
|
||||
}
|
||||
},
|
||||
"to": [{ "key_code": "left_arrow" }],
|
||||
"type": "basic"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"key_code": "j",
|
||||
"modifiers": {
|
||||
"mandatory": ["right_command"],
|
||||
"optional": ["any"]
|
||||
}
|
||||
},
|
||||
"to": [{ "key_code": "down_arrow" }],
|
||||
"type": "basic"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"key_code": "k",
|
||||
"modifiers": {
|
||||
"mandatory": ["right_command"],
|
||||
"optional": ["any"]
|
||||
}
|
||||
},
|
||||
"to": [{ "key_code": "up_arrow" }],
|
||||
"type": "basic"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"key_code": "l",
|
||||
"modifiers": {
|
||||
"mandatory": ["right_command"],
|
||||
"optional": ["any"]
|
||||
}
|
||||
},
|
||||
"to": [{ "key_code": "right_arrow" }],
|
||||
"type": "basic"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Default profile",
|
||||
"selected": true,
|
||||
"simple_modifications": [
|
||||
{
|
||||
"from": { "key_code": "caps_lock" },
|
||||
"to": [{ "key_code": "f18" }]
|
||||
}
|
||||
],
|
||||
"virtual_hid_keyboard": { "keyboard_type_v2": "iso" }
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user