Skip to content

Instantly share code, notes, and snippets.

@amanbolat
Last active March 2, 2025 14:54
Show Gist options
  • Save amanbolat/7f34ede37ed04223910b7b63c7d14ce0 to your computer and use it in GitHub Desktop.
Save amanbolat/7f34ede37ed04223910b7b63c7d14ce0 to your computer and use it in GitHub Desktop.
Hammperspoon Spoon to proofread the selected text
--- === LLMProofreader ===
---
--- Proofread clipboard contents using Claude
---
--- Place this file in the ~/.hammerspoon directory with the following structure
--- ── Spoons
--- └── LLMProofreader.spoon
--- └── init.lua
---
--- To use, add the following lines to ~/.hammerspoon/init.lua:
--- hs.loadSpoon('LLMProofreader')
--- spoon.LLMProofreader:bindHotkeys({
--- proofread = { {"cmd", "alt", "ctrl"}, "P" },
--- })
local obj = {}
obj.__index = obj
-- Metadata
obj.name = 'LLMProofreader'
obj.version = '1.1'
obj.author = 'Modified from ChatGPT spoon by JC Connell'
obj.license = 'MIT - https://opensource.org/licenses/MIT'
--- LLMProofreader.api_key
--- Variable
--- String API key for Claude/Anthropic
obj.api_key = 'YOUR-ANTHROPIC-API-KEY-HERE'
--- LLMProofreader.model
--- Variable
--- Claude model to use
obj.model = 'claude-3-7-sonnet-latest'
--- LLMProofreader.max_tokens
--- Variable
--- Integer indicating max length of the response
obj.max_tokens = 4000
--- LLMProofreader.logger
--- Variable
--- Logger object used within the Spoon
obj.logger = hs.logger.new('LLMProofreader')
-- getTextSelection()
-- Gets currently selected text using Cmd+C
-- Saves and restores the current pasteboard
-- Imperfect, perhaps. May not work well on large selections.
-- Taken from: https://github.com/Hammerspoon/hammerspoon/issues/634
local function getTextSelection() -- returns text or nil while leaving pasteboard undisturbed.
local oldText = hs.pasteboard.getContents()
hs.eventtap.keyStroke({"cmd"}, "c")
hs.timer.usleep(25000)
local text = hs.pasteboard.getContents() -- if nothing is selected this is unchanged
hs.pasteboard.setContents(oldText)
if text ~= oldText then
return text
else
return ""
end
end
local function readFileContents(filePath)
local file, err = io.open(filePath, "r")
if not file then
hs.alert.show("Error opening file: " .. tostring(err))
return nil
end
local content = file:read("*a")
file:close()
return content:match("^%s*(.-)%s*$") -- Trim whitespace
end
local pathToFile = "/Users/<your_user_name>/.secrets/claude_api_key"
--- LLMProofreader:proofreadWithConfig(max_tokens)
--- Method
--- Sends text to Claude for proofreading
---
--- Parameters:
--- * max_tokens - Integer
function obj:proofreadWithConfig(max_tokens)
max_tokens = max_tokens or obj.max_tokens
obj.api_key = readFileContents(pathToFile)
assert(obj.api_key and obj.api_key ~= '', "API key must be provided")
-- local user_text = hs.pasteboard.getContents()
local user_text = getTextSelection()
assert(user_text and user_text ~= '', "No text selected")
obj.logger.df("User text: %s", board)
obj.logger.df("Max tokens: %s", tostring(max_tokens))
hs.http.asyncPost(
'https://api.anthropic.com/v1/messages',
hs.json.encode({
model = obj.model,
max_tokens = max_tokens,
system = 'Proofread the text written by user. Paraphrase sentences or paragraphs if required. Make it look like it was written by a native English speaker. Keep the balance between colloquial and formal styles. Do not reply with anything, but a rewritten version of the text.',
messages = {
{
role = 'user',
content = user_text
}
}
}),
{
["Content-Type"] = "application/json",
["x-api-key"] = obj.api_key,
["anthropic-version"] = "2023-06-01"
},
function(http_code, response)
obj.logger.ef("HTTP Code: %s", tostring(http_code))
obj.logger.ef("Response: %s", response)
if http_code == 200 then
local decoded_data = hs.json.decode(response)
-- Update to use the correct response structure
local proofread_text = decoded_data.content[1].text or "No content received"
hs.pasteboard.setContents(proofread_text)
hs.timer.usleep(25000)
hs.eventtap.keyStroke({"cmd"}, "v")
hs.notify.new({title = 'Proofreading Successful. The response was added to the pasteboard.'}):send()
obj.logger.df("Claude response: %s", proofread_text)
else
local decoded_data = hs.json.decode(response)
local error_message = decoded_data.error and decoded_data.error.message or "Unknown error"
hs.notify.new({title = 'Proofreading Failed!', informativeText = error_message}):send()
obj.logger.ef("Claude response: %s", response)
end
end
)
end
--- LLMProofreader:proofread()
--- Method
--- Sends text to Claude for proofreading with default settings
function obj:proofread()
obj:proofreadWithConfig(4000)
end
--- LLMProofreader:bindHotkeys(mapping)
--- Method
--- Binds hotkeys for LLMProofreader
---
--- Parameters:
--- * mapping - A table containing hotkey modifier/key details for:
--- * proofread - proofread text with Claude
function obj:bindHotkeys(mapping)
local def = {}
for action, key in pairs(mapping) do
if action == "proofread" then
def.proofread = hs.fnutils.partial(self.proofread, self)
else
self.logger.ef("Invalid hotkey action '%s'", action)
end
end
hs.spoons.bindHotkeysToSpec(def, mapping)
obj.mapping = mapping
end
return obj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment