Skip to content

Instantly share code, notes, and snippets.

@james2doyle
Created March 22, 2019 16:15
Show Gist options
  • Save james2doyle/4ba0815d991d182f1ef97bc9227c0e94 to your computer and use it in GitHub Desktop.
Save james2doyle/4ba0815d991d182f1ef97bc9227c0e94 to your computer and use it in GitHub Desktop.
A clipboard plugin for Hammerspoon that uses the chooser to show/select items in the clipboard
--[[
https://github.com/VFS/.hammerspoon
]]--
--[[
This is my attempt to implement a jumpcut replacement in Lua/Hammerspoon.
It monitors the clipboard/pasteboard for changes, and stores the strings you copy to the transfer area.
You can access this history on the menu (Unicode scissors icon).
Clicking on any item will add it to your transfer area.
If you open the menu while pressing option/alt, you will enter the Direct Paste Mode. This means that the selected item will be
"typed" instead of copied to the active clipboard.
The clipboard persists across launches.
-> Ng irc suggestion: hs.settings.set("jumpCutReplacementHistory", clipboard_history)
]]--
-- Feel free to change those settings
local frequency = 3 -- Speed in seconds to check for clipboard changes. If you check too frequently, you will loose performance, if you check sparsely you will loose copies
local hist_size = 9 -- How many items to keep on history
local label_length = 40 -- How wide (in characters) the dropdown menu should be. Copies larger than this will have their label truncated and end with "…" (unicode for elipsis ...)
local honor_clearcontent = false --asmagill request. If any application clears the pasteboard, we also remove it from the history https://groups.google.com/d/msg/hammerspoon/skEeypZHOmM/Tg8QnEj_N68J
local paste_on_select = false -- Auto-type on click
-- Don't change anything bellow this line
local jumpcut = hs.menubar.new()
jumpcut:setTooltip("Clipboard History")
local pasteboard = require("hs.pasteboard") -- http://www.hammerspoon.org/docs/hs.pasteboard.html
local settings = require("hs.settings") -- http://www.hammerspoon.org/docs/hs.settings.html
local last_change = pasteboard.changeCount() -- displays how many times the pasteboard owner has changed // Indicates a new copy has been made
--Array to store the clipboard history
local clipboard_history = settings.get("so.victor.hs.jumpcut") or {} --If no history is saved on the system, create an empty history
-- Append a history counter to the menu
function setTitle()
if (#clipboard_history == 0) then
jumpcut:setTitle("✂") -- Unicode magic
else
jumpcut:setTitle("✂ (" .. #clipboard_history .. ")") -- updates the menu counter
end
end
function putOnPaste(string,key)
if (paste_on_select) then
hs.eventtap.keyStrokes(string)
pasteboard.setContents(string)
last_change = pasteboard.changeCount()
else
if (key.alt == true) then -- If the option/alt key is active when clicking on the menu, perform a "direct paste", without changing the clipboard
hs.eventtap.keyStrokes(string) -- Defeating paste blocking http://www.hammerspoon.org/go/#pasteblock
else
pasteboard.setContents(string)
last_change = pasteboard.changeCount() -- Updates last_change to prevent item duplication when putting on paste
end
end
end
-- Clears the clipboard and history
function clearAll()
pasteboard.clearContents()
clipboard_history = {}
settings.set("so.victor.hs.jumpcut", clipboard_history)
now = pasteboard.changeCount()
setTitle()
end
-- Clears the last added to the history
function clearLastItem()
table.remove(clipboard_history, #clipboard_history)
settings.set("so.victor.hs.jumpcut", clipboard_history)
now = pasteboard.changeCount()
setTitle()
end
function pasteboardToClipboard(item)
-- Loop to enforce limit on qty of elements in history. Removes the oldest items
while (#clipboard_history >= hist_size) do
table.remove(clipboard_history,1)
end
table.insert(clipboard_history, item)
settings.set("so.victor.hs.jumpcut", clipboard_history) -- updates the saved history
setTitle() -- updates the menu counter
end
-- Dynamic menu by cmsj https://github.com/Hammerspoon/hammerspoon/issues/61#issuecomment-64826257
populateMenu = function(key)
setTitle() -- Update the counter every time the menu is refreshed
menu_data = {}
if (#clipboard_history == 0) then
table.insert(menu_data, {title = "None", disabled = true}) -- If the history is empty, display "None"
else
for k,v in pairs(clipboard_history) do
if (string.len(v) > label_length) then
table.insert(menu_data, 1, {title = string.sub(v, 0, label_length) .. "…", fn = function() putOnPaste(v, key) end }) -- Truncate long strings
else
table.insert(menu_data, 1, {title = v, fn = function() putOnPaste(v, key) end })
end -- end if else
end-- end for
end-- end if else
-- footer
table.insert(menu_data, {title = "-"})
table.insert(menu_data, {title = "Clear All", fn = function() clearAll() end })
if (key.alt == true or paste_on_select) then
table.insert(menu_data, {title = "Direct Paste Mode ✍", disabled = true})
end
return menu_data
end
-- If the pasteboard owner has changed, we add the current item to our history and update the counter.
function storeCopy()
now = pasteboard.changeCount()
if (now > last_change) then
current_clipboard = pasteboard.getContents()
-- asmagill requested this feature. It prevents the history from keeping items removed by password managers
if (current_clipboard == nil and honor_clearcontent) then
clearLastItem()
else
pasteboardToClipboard(current_clipboard)
end
last_change = now
end
end
--Checks for changes on the pasteboard. Is it possible to replace with eventtap?
timer = hs.timer.new(frequency, storeCopy)
timer:start()
setTitle() --Avoid wrong title if the user already has something on his saved history
jumpcut:setMenu(populateMenu)
function ReverseTable(t)
local reversed_table = {}
local item_count = #t
for k, v in ipairs(t) do
reversed_table[item_count + 1 - k] = v
end
return reversed_table
end
hs.hotkey.bind({"shift", "ctrl"}, 'V', function ()
-- grab the current application that is being used
local current = hs.application.frontmostApplication()
local chooser = hs.chooser.new(function(selection)
-- switch back to the application that was being used
current:activate()
if selection.text ~= nil then
pasteboard.setContents(selection.text)
-- hs.eventtap.keyStrokes(selection.text)
-- trigger a paste event
hs.eventtap.keyStroke({"cmd"}, "v")
last_change = pasteboard.changeCount()
end
end)
local choices = {}
for k,v in pairs(clipboard_history) do
table.insert(choices, {
["text"] = v,
})
end
chooser:choices(ReverseTable(choices))
chooser:show()
end)
@x-tropy
Copy link

x-tropy commented Feb 4, 2024

Cool work, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment