Как я настраивал хоткей чтобы убирать отступы при копировании из терминала
Работаю в Claude Code через Wave Terminal. Заметил неприятную мелочь: когда копирую ответ ассистента и вставляю куда-нибудь — в начале каждой строки прилетают два лишних пробела.
Сначала я думал, это сам ассистент пишет с какими-то trailing-пробелами. Попросил его не делать так. Не помогло. Полез разбираться – оказалось, что это рендер: терминал сдвигает текст ассистента на два пробела влево, чтобы визуально отделить от моего ввода. При копировании этот сдвиг честно уезжает в буфер обмена.
У iTerm2 для такого есть опция «Trim trailing whitespace when copying». У Wave – нет. У Claude Code в настройках тоже ничего похожего не нашлось.
Решил повесить на хоткей очистку буфера через Hammerspoon. Идея простая: жмёшь Cmd+Shift+V, Lua-скрипт берёт содержимое буфера, режет ведущие два пробела на каждой строке, кладёт обратно и эмулирует обычный Cmd+V.
Звучит на 10 строк кода. На деле я собрал коллекцию граблей.
Грабля 1. Hammerspoon hotkey API регистрирует комбинацию по символу. Когда я писал hs.hotkey.bind({"cmd","shift"}, "v", ...), он смотрел текущую раскладку. У меня в момент загрузки была русская – а на русской раскладке физическая клавиша V даёт букву "м". Хоткей зарегистрировался как ⌘⇧м и работал только в русской раскладке. Переключился на английскую – ничего не происходит.
Грабля 2. Попробовал передать чистый keycode числом – 9 это физическая клавиша V на любой раскладке. bindSpec принимает число, но молча его игнорирует и всё равно ищет строку. bind число принимает, но на физическое нажатие не реагировал – видимо что-то на уровне Carbon Events ломается с не-латинской раскладкой.
В итоге выкинул hotkey API целиком и переехал на hs.eventtap.new – это перехват на уровне CGEventTap, до приложений и до раскладки. Получаю чистый keyDown event, проверяю модификаторы и keycode 9 руками. Работает везде.
Грабля 3. Самая обидная. После всех правок Hammerspoon перезапускаю, конфиг загружается, hs.accessibilityState() возвращает true. А eventtap не получает ни одного события. Физически жму Cmd+Shift+V – в логах пусто. Программно через hs.eventtap.event.newKeyEvent(...):post() – срабатывает.
Полез в System Settings → Privacy & Security → Accessibility. Hammerspoon там есть, тумблер включён. Переключил OFF, подождал две секунды, обратно ON. Перезапустил Hammerspoon. Заработало.
Похоже, после pkill macOS закэшировал permission в состоянии «вроде дали, но реально не дали». Re-toggle перевыдаёт реальное разрешение на низкоуровневый event tap. accessibilityState() при этом всё время врал.
Итог: Cmd+Shift+V теперь чистит буфер перед вставкой. Обычный Cmd+V продолжает работать как раньше. Если когда-нибудь снова отвалится – знаю, что первым делом проверять.
Финальный ~/.hammerspoon/init.lua:
local function cleanText(s)
if s == nil or s == "" then return s end
s = s:gsub("^ ", "") -- ведущие 2 пробела в самом начале
s = s:gsub("\n ", "\n") -- ведущие 2 пробела после каждого \n
s = s:gsub("[ \t]+\n", "\n") -- висящие пробелы перед \n
s = s:gsub("[ \t]+{{content}}quot;, "") -- висящие пробелы в самом конце
return s
end
local tap = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
local flags = event:getFlags()
-- keycode 9 = физическая клавиша V на US ANSI, не зависит от раскладки
if flags.cmd and flags.shift and not flags.alt and not flags.ctrl
and event:getKeyCode() == 9 then
local raw = hs.pasteboard.getContents()
if raw == nil or raw == "" then return true end
hs.pasteboard.setContents(cleanText(raw))
-- посылаем синтетический Cmd+V в фокусное приложение
hs.timer.doAfter(0.02, function()
hs.eventtap.event.newKeyEvent({"cmd"}, 9, true):post()
hs.timer.usleep(20000)
hs.eventtap.event.newKeyEvent({"cmd"}, 9, false):post()
end)
-- восстанавливаем оригинал буфера после вставки
hs.timer.doAfter(0.6, function()
hs.pasteboard.setContents(raw)
end)
return true -- съедаем оригинальный Cmd+Shift+V, чтобы приложение его не видело
end
return false
end)
tap:start()
-- автоперезагрузка при изменении init.lua
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", function(files)
for _, f in ipairs(files) do
if f:match("%.lua{{content}}quot;) then hs.reload(); return end
end
end):start()
hs.alert.show("Hammerspoon: Cmd+Shift+V для чистой вставки")