From 3e89d6b347f30a53fbdf75cfedc88ff9cecb48d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:11:31 +0200 Subject: [PATCH 1/3] feat(skills): add skills-status target - add skills-status.sh to show which skills are installed - add skills-status make target and run it before skills picker --- makefiles/scripts/skills-status.sh | 33 ++++++++++++++++++++++++++++++ makefiles/skills.mk | 7 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 makefiles/scripts/skills-status.sh diff --git a/makefiles/scripts/skills-status.sh b/makefiles/scripts/skills-status.sh new file mode 100755 index 00000000..ed3dea63 --- /dev/null +++ b/makefiles/scripts/skills-status.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# List every skill in REPO and mark the ones the `skills` CLI has installed. +# +# The CLI picker cannot show install state, so `make skills` prints this first. +set -euo pipefail + +REPO="$(cd "${1:?usage: skills-status.sh }" && pwd)" +LOCK="$HOME/.agents/.skill-lock.json" + +installed="" +if [[ -f "$LOCK" ]]; then + installed="$(jq -r --arg repo "$REPO" \ + '.skills // {} | to_entries[] | select(.value.sourceType == "local" and .value.source == $repo) | .key' \ + "$LOCK")" +fi + +have=0 +total=0 +for md in "$REPO"/skills/*/*/SKILL.md; do + [[ "$md" == "$REPO/skills/archived/"* ]] && continue + dir="$(dirname "$md")" + name="$(basename "$dir")" + bucket="$(basename "$(dirname "$dir")")" + total=$((total + 1)) + if grep -qx "$name" <<<"$installed"; then + printf ' \033[32m✓\033[0m %-24s %s\n' "$name" "$bucket" + have=$((have + 1)) + else + printf ' \033[2m·\033[0m %-24s %s\n' "$name" "$bucket" + fi +done + +echo "skills-status: $have of $total installed" diff --git a/makefiles/skills.mk b/makefiles/skills.mk index 6f8b17f3..b162a4e2 100644 --- a/makefiles/skills.mk +++ b/makefiles/skills.mk @@ -13,8 +13,10 @@ SKILLS_DIR ?= $(HOME)/codes/skills SKILLS_AGENTS := claude-code codex cursor SKILLS_CLI := npx -y skills@latest SKILLS_LINK := $(DOTFILES)/makefiles/scripts/skills-link.sh +SKILLS_STATUS := $(DOTFILES)/makefiles/scripts/skills-status.sh skills: skills-repo ## Pick skills from ~/codes/skills interactively and link them live + @$(SKILLS_STATUS) "$(SKILLS_DIR)" @$(SKILLS_CLI) add "$(SKILLS_DIR)" -g -a $(SKILLS_AGENTS) @$(SKILLS_LINK) "$(SKILLS_DIR)" @@ -29,6 +31,9 @@ skills-remove: ## Pick installed skills to remove skills-list: ## List globally installed skills @$(SKILLS_CLI) list -g +skills-status: ## Show which skills in ~/codes/skills are installed + @$(SKILLS_STATUS) "$(SKILLS_DIR)" + skills-repo: ## Clone ~/codes/skills and link its private config @[ -d "$(SKILLS_DIR)" ] || git clone "git@github.com:$(SKILLS_REPO).git" "$(SKILLS_DIR)" @if [ ! -d "$(SKILLS_DIR)/local" ]; then \ @@ -39,4 +44,4 @@ skills-repo: ## Clone ~/codes/skills and link its private config @mkdir -p "$(HOME)/.config" @ln -sfn "$(SKILLS_DIR)/local" "$(HOME)/.config/dgokcin-skills" -.PHONY: skills skills-all skills-remove skills-list skills-repo +.PHONY: skills skills-all skills-remove skills-list skills-status skills-repo From 0da21e7e41d063e3b3c76ab1d11c9d14e69931fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:11:31 +0200 Subject: [PATCH 2/3] feat(hammerspoon): add hammerspoon cask and config symlink - add hammerspoon cask to brewfile - add hammerspoon make target symlinking ~/.hammerspoon - add hammerspoon config directory --- Brewfile | 1 + makefiles/tools.mk | 5 +- other/hammerspoon/init.lua | 12 ++ other/hammerspoon/pip-dodge.lua | 217 ++++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 other/hammerspoon/init.lua create mode 100644 other/hammerspoon/pip-dodge.lua diff --git a/Brewfile b/Brewfile index 8983cd48..9bdd6639 100644 --- a/Brewfile +++ b/Brewfile @@ -84,6 +84,7 @@ cask "betterdisplay" cask "claude" cask "cursor" cask "docker-desktop" +cask "hammerspoon" cask "intellij-idea" cask "iterm2" cask "lookaway" diff --git a/makefiles/tools.mk b/makefiles/tools.mk index 22cc7882..950106da 100644 --- a/makefiles/tools.mk +++ b/makefiles/tools.mk @@ -136,6 +136,9 @@ tmux: ## Install tmux and symlink config (fixes TERM/escape-sequence bleed with $(call install_with_brew,tmux) $(call symlink,other/tmux/tmux.conf,${HOME}/.tmux.conf) +hammerspoon: ## Symlink the Hammerspoon config directory + $(call symlink,other/hammerspoon,${HOME}/.hammerspoon) + brew-trust: ## Trust the third-party taps the Brewfile declares @command -v brew >/dev/null || { echo "Homebrew is required"; exit 1; } @$(DOTFILES)/makefiles/scripts/brew-trust.sh @@ -149,4 +152,4 @@ brew-bundle-check: brew-trust ## Check packages and applications from Brewfile @brew bundle check --file="$(DOTFILES)/Brewfile" .PHONY: tools volta node npm-tools yarn pnpm bun uv aws-cli opencode-cli verify-tool-owners \ - lazygit yamllint yq k9s tmux brew-trust brew-bundle brew-bundle-check + lazygit yamllint yq k9s tmux hammerspoon brew-trust brew-bundle brew-bundle-check diff --git a/other/hammerspoon/init.lua b/other/hammerspoon/init.lua new file mode 100644 index 00000000..2a53c403 --- /dev/null +++ b/other/hammerspoon/init.lua @@ -0,0 +1,12 @@ +require("hs.ipc") +hs.window.animationDuration = 0 + +local pipDodge = require("pip-dodge") +pipDodge.start() + +local hyper = { "cmd", "alt", "ctrl" } +hs.hotkey.bind(hyper, "p", pipDodge.toggle) +hs.hotkey.bind({ "cmd", "alt", "ctrl", "shift" }, "p", pipDodge.dumpWindows) +hs.hotkey.bind(hyper, "r", hs.reload) + +hs.alert.show("Hammerspoon loaded", 0.6) diff --git a/other/hammerspoon/pip-dodge.lua b/other/hammerspoon/pip-dodge.lua new file mode 100644 index 00000000..347273ee --- /dev/null +++ b/other/hammerspoon/pip-dodge.lua @@ -0,0 +1,217 @@ +-- Moves Dia's Picture-in-Picture window out from under the mouse. +-- +-- Intent rules +-- Cursor sits in the PiP interior for a short dwell -> glide to a corner that clears it. +-- Cursor in the edge or corner band -> resize grab, hold still. +-- Mouse button held (dragging or resizing) -> hold still. +-- Hold modifier pressed (default alt) -> hold still. +-- Cursor follows the PiP to its new spot -> you want it, so it stays pinned +-- until the cursor leaves, plus a short grace period for re-entries. + +local M = {} + +M.config = { + bundleID = "company.thebrowser.dia", + subrolePattern = "SystemDialog|Floating", -- Dia's PiP reports AXSystemDialog + minSize = 150, -- ignores autofill dropdowns and helper windows + edgeBand = 28, -- px inside the frame that counts as a resize handle + dwell = 0.10, -- seconds the cursor must sit in the interior + cooldown = 0.40, -- seconds after a jump before the next one + chaseWindow = 2.5, -- re-entry this soon after a jump pins the PiP + pinGrace = 1.0, -- re-entry this soon after unpinning pins it again + margin = 20, -- gap from screen edges when parking + animation = 0.15, -- seconds the window takes to glide to its corner + pollInterval = 0.05, + windowCacheTTL = 1.0, + holdModifier = "alt", +} + +local state = { + enabled = true, + timer = nil, + interiorSince = nil, + wasInside = false, + pinned = false, + unpinnedAt = 0, + lastDodgeAt = 0, + cachedWindow = nil, + cachedAt = 0, +} + +local function now() return hs.timer.secondsSinceEpoch() end + +local function log(fmt, ...) + if M.verbose then print(("[pip-dodge] " .. fmt):format(...)) end +end + +-- Window lookup ------------------------------------------------------------- + +local function looksLikePip(w) + if not w:isVisible() or w:isMinimized() or w:isStandard() then return false end + + local f = w:frame() + if f.w < M.config.minSize or f.h < M.config.minSize then return false end + local sf = (w:screen() or hs.screen.mainScreen()):frame() + if f.w * f.h > 0.5 * sf.w * sf.h then return false end + + local subrole = w:subrole() or "" + for pat in M.config.subrolePattern:gmatch("[^|]+") do + if subrole:match(pat) then return true end + end + return false +end + +local function pipWindow() + local t = now() + local cached = state.cachedWindow + if cached and t - state.cachedAt < M.config.windowCacheTTL then + if cached:isVisible() then return cached end + state.cachedWindow = nil + end + + state.cachedAt = t + state.cachedWindow = nil + for _, app in ipairs(hs.application.applicationsForBundleID(M.config.bundleID)) do + for _, w in ipairs(app:allWindows()) do + if looksLikePip(w) then + state.cachedWindow = w + return w + end + end + end + return nil +end + +-- Geometry ------------------------------------------------------------------ + +local function pointIn(p, f) + return p.x >= f.x and p.x < f.x + f.w and p.y >= f.y and p.y < f.y + f.h +end + +local function inInterior(p, f) + local b = M.config.edgeBand + return pointIn(p, { x = f.x + b, y = f.y + b, w = f.w - 2 * b, h = f.h - 2 * b }) +end + +-- Corners of the PiP's screen, opposite corner first, then the two adjacent ones. +-- Returns the first frame that leaves the cursor outside it, or nil if none does. +local function escapeFrame(w, mouse) + local f = w:frame() + local s = w:screen():frame() + local m = M.config.margin + local left, right = s.x + m, s.x + s.w - f.w - m + local top, bottom = s.y + m, s.y + s.h - f.h - m + local onLeft = f.x + f.w / 2 < s.x + s.w / 2 + local onTop = f.y + f.h / 2 < s.y + s.h / 2 + + local candidates = { + { x = onLeft and right or left, y = onTop and bottom or top }, + { x = onLeft and right or left, y = onTop and top or bottom }, + { x = onLeft and left or right, y = onTop and bottom or top }, + } + for _, c in ipairs(candidates) do + local target = hs.geometry.rect(c.x, c.y, f.w, f.h) + if not pointIn(mouse, target) then return target end + end + return nil +end + +-- Decision ------------------------------------------------------------------ + +local function holdRequested() + if hs.eventtap.checkKeyboardModifiers()[M.config.holdModifier] then return true end + local buttons = hs.eventtap.checkMouseButtons() + return buttons.left or buttons.right +end + +local function tick() + if not state.enabled then return end + local t = now() + + local w = pipWindow() + if not w then + state.interiorSince, state.wasInside, state.pinned = nil, false, false + return + end + + local mouse = hs.mouse.absolutePosition() + local frame = w:frame() + local inside = pointIn(mouse, frame) + local entering = inside and not state.wasInside + state.wasInside = inside + + if state.pinned then + if inside then return end + state.pinned = false + state.unpinnedAt = t + log("unpinned") + return + end + + if not inside then state.interiorSince = nil; return end + + -- Coming back right after a jump, or shortly after unpinning, means you want it. + if entering and (t - state.lastDodgeAt < M.config.chaseWindow + or t - state.unpinnedAt < M.config.pinGrace) then + state.pinned = true + state.interiorSince = nil + log("pinned: cursor followed the window") + return + end + + if t - state.lastDodgeAt < M.config.cooldown then return end + if not inInterior(mouse, frame) or holdRequested() then + state.interiorSince = nil + return + end + + state.interiorSince = state.interiorSince or t + if t - state.interiorSince < M.config.dwell then return end + + local target = escapeFrame(w, mouse) + state.interiorSince = nil + if not target then + state.pinned = true + log("pinned: no corner clears the cursor") + return + end + + w:setFrame(target, M.config.animation) + state.lastDodgeAt = t + state.wasInside = false + log("dodged to %d,%d", target.x, target.y) +end + +-- Public API ---------------------------------------------------------------- + +function M.start() + if state.timer then return M end + state.timer = hs.timer.doEvery(M.config.pollInterval, tick) + return M +end + +function M.stop() + if state.timer then state.timer:stop(); state.timer = nil end + state.interiorSince, state.pinned = nil, false +end + +function M.toggle() + state.enabled = not state.enabled + state.interiorSince, state.pinned = nil, false + hs.alert.show(state.enabled and "PiP dodge on" or "PiP dodge off", 0.8) +end + +-- Prints every Dia window so subrolePattern and minSize can be tuned. +function M.dumpWindows() + for _, app in ipairs(hs.application.applicationsForBundleID(M.config.bundleID)) do + for _, w in ipairs(app:allWindows()) do + local f = w:frame() + print(("[pip-dodge] title=%q subrole=%s standard=%s frame=%dx%d@%d,%d pip=%s"):format( + w:title() or "", tostring(w:subrole()), tostring(w:isStandard()), + f.w, f.h, f.x, f.y, tostring(looksLikePip(w)))) + end + end + hs.alert.show("Dia windows dumped to console", 0.8) +end + +return M From 32a41f87840869e25cd90f3f9db4f1621ed7a176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:11:31 +0200 Subject: [PATCH 3/3] chore(claude): set opus model and force terminal images - set default model to opus - enable claude_code_force_terminal_images --- ai-stuff/claude/settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ai-stuff/claude/settings.json b/ai-stuff/claude/settings.json index 854a983a..17270f38 100644 --- a/ai-stuff/claude/settings.json +++ b/ai-stuff/claude/settings.json @@ -10,6 +10,7 @@ "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT": "1", "CLODEX_PLAN_USD": "10", "CLODEX_SPEND_WINDOW_DAYS": "30", + "CLAUDE_CODE_FORCE_TERMINAL_IMAGES": "1", "ENABLE_LSP_TOOL": "1", "ENABLE_TOOL_SEARCH": "auto:0.2" }, @@ -60,6 +61,7 @@ ], "defaultMode": "auto" }, + "model": "opus", "enableAllProjectMcpServers": false, "skillOverrides": { "buddy": "off",