Vibe Writing

Minyoung Jeong @kkung@hackers.pub
Recently, I've had many occasions to send messages in English, and I've been getting a lot of help from AI for English writing. However, it was quite annoying to copy and paste every time. Then I remembered that Hammerspoon allows easy access to Mac's Accessibility API, so I created a simple solution. I tried to do most of the development using Vibe coding, but it kept trying to use non-existent APIs, so I ended up just using it as a reference and building it myself. ((The path of a Vibe coder remains long and arduous today...))
With the script below, you can select text and press Cmd+Shift+K to perform Korean<->English translation while maintaining the selected area.
local config = {
-- OpenAI API Key
openai_api_key = "sk-proj--",
-- I'll use Response API
openai_api_url = "https://api.openai.com/v1/responses",
-- Model name
openai_model = "gpt-4o",
}
local function callOpenAI(text, callback)
if not config.openai_api_key then
hs.alert.show("Config error: missing openai_api_key")
return
end
local insturction = "입력된 문장이 영어일 경우 한국어로, 한국어일 경우 영어로 변환해줘. 의미를 모국어 사용자가 자연스럽게 받아들일 수 있게 정확하고 유창하게 전달하고, 불필요한 문장을 생략하여 명료하게 작성해. 번역어 외의 다른 문장을 추가하지 말고 번역 그 자체만 반환해."
local request = hs.json.encode({
model = config.openai_model,
instructions = insturction,
input = text,
max_output_tokens = 5000,
})
print(request)
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. config.openai_api_key
}
hs.http.asyncPost(config.openai_api_url, request, headers, function(status, response, _)
if status == 200 then
local success, data = pcall(hs.json.decode, response)
if success and data.status == "completed" then
local translated = data.output[1].content[1].text
print("TR: " .. translated)
callback(translated)
else
hs.alert.show("Call failed " .. data)
callback(nil)
end
else
print(status, response)
hs.alert.show("Call failed " .. status)
callback(nil)
end
end)
end
local function getSelectedTextCB()
local orig_cb = hs.pasteboard.getContents()
print("Original Clipboard " .. orig_cb)
hs.eventtap.keyStroke({"cmd"}, "c")
local sel = hs.pasteboard.getContents()
print("Selected Text " .. orig_cb)
hs.timer.doAfter(0.1, function()
if orig_cb then
hs.pasteboard.setContents(orig_cb)
end
end)
return sel
end
local function getFocusedElement()
local ax = hs.axuielement
local sys = ax.systemWideElement()
local focused = sys:attributeValue("AXFocusedUIElement")
return focused
end
local function getSelectedTextAX()
local focused = getFocusedElement()
if not focused then
hs.alert.show("Could not found focused element")
return nil
end
local selected_text = focused:attributeValue("AXSelectedText")
if selected_text and selected_text ~= "" then
return selected_text
end
end
local function getSelectedText()
local sel = getSelectedTextAX()
if sel == nil then
sel = getSelectedTextCB()
end
return sel
end
local function replaceSelectedTextCB(new_text)
local orig_cb = hs.pasteboard.getContents()
hs.pasteboard.setContents(new_text)
hs.eventtap.keyStroke({"cmd"}, "v")
hs.timer.doAfter(0.1, function()
if orig_cb then
hs.pasteboard.setContents(orig_cb)
end
end)
end
local function replaceSelectedTextAX(new_text)
local focused = getFocusedElement()
local ran = focused:attributeValue("AXSelectedTextRange")
if ran then
local val = focused:attributeValue("AXValue")
local start_pos = utf8.offset(val, ran.location + 1)
local end_pos = utf8.offset(val, ran.location + ran.length + 1)
print(hs.inspect(val) .. "start_pos=" .. start_pos .. ", end_pos=" .. end_pos)
local new_val = string.sub(val, 1, start_pos - 1) ..
new_text ..
string.sub(val, end_pos, -1)
print(hs.inspect(new_text) .. ", " .. new_val)
focused:setAttributeValue("AXValue", new_val)
hs.timer.doAfter(0.1, function()
-- adjust cursor position
local new_ran = {location = ran.location, length = utf8.len(new_text)}
focused:setAttributeValue("AXSelectedTextRange", new_ran)
end)
return true
else
return nil
end
end
local function replaceSelectedText(new_text)
if replaceSelectedTextAX(new_text) == nil then
replaceSelectedTextCB(new_text)
end
end
hs.hotkey.bind({"cmd", "shift"}, "k", function()
local sel = getSelectedText()
if sel and sel ~= "" then
callOpenAI(sel, function(translated)
replaceSelectedText(translated)
end)
end
end)