Skip to content
Merged
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
1 change: 1 addition & 0 deletions Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ cask "betterdisplay"
cask "claude"
cask "cursor"
cask "docker-desktop"
cask "hammerspoon"
cask "intellij-idea"
cask "iterm2"
cask "lookaway"
Expand Down
2 changes: 2 additions & 0 deletions ai-stuff/claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -60,6 +61,7 @@
],
"defaultMode": "auto"
},
"model": "opus",
"enableAllProjectMcpServers": false,
"skillOverrides": {
"buddy": "off",
Expand Down
33 changes: 33 additions & 0 deletions makefiles/scripts/skills-status.sh
Original file line number Diff line number Diff line change
@@ -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 <skills-repo>}" && 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"
7 changes: 6 additions & 1 deletion makefiles/skills.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)"

Expand All @@ -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 \
Expand All @@ -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
5 changes: 4 additions & 1 deletion makefiles/tools.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
12 changes: 12 additions & 0 deletions other/hammerspoon/init.lua
Original file line number Diff line number Diff line change
@@ -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)
217 changes: 217 additions & 0 deletions other/hammerspoon/pip-dodge.lua
Original file line number Diff line number Diff line change
@@ -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
Loading