From f6d6473465972a5c531b5a8cba3a60502d847429 Mon Sep 17 00:00:00 2001 From: Tim Deeb-Swihart Date: Sun, 20 Jan 2019 19:33:43 -0500 Subject: [PATCH] Add hammerspoon helpers for Keepit. Update installation scripts --- .gitmodules | 3 + bin/watchman-process-files.py | 24 ++++--- config.json | 13 +++- hammerspoon/init.lua | 6 ++ hammerspoon/keepit.lua | 104 ++++++++++++++++++++++++++++++ hammerspoon/mail.lua | 29 +++++++++ hammerspoon/scripts/ctrl-cmd-r.js | 29 +++++++++ install.py | 42 ++++++++---- setup.sh | 4 +- shell/funcs.sh | 5 ++ 10 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 .gitmodules create mode 100644 hammerspoon/init.lua create mode 100644 hammerspoon/keepit.lua create mode 100644 hammerspoon/mail.lua create mode 100644 hammerspoon/scripts/ctrl-cmd-r.js mode change 100644 => 100755 setup.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3045f9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hammerspoon/git/Lunette"] + path = hammerspoon/git/Lunette + url = https://github.com/scottwhudson/Lunette.git diff --git a/bin/watchman-process-files.py b/bin/watchman-process-files.py index fdf01e3..26c0228 100644 --- a/bin/watchman-process-files.py +++ b/bin/watchman-process-files.py @@ -63,9 +63,7 @@ def parse_rule(line: str): def expand_variables(variables, text): for variable, value in variables.items(): - key = f'{{{variable}}}' - if key in text: - text = text.replace(key, value) + text = text.replace(f'{{{variable}}}', value) return text @@ -74,15 +72,15 @@ def process_file(rules, variables, path: str): dirname = os.path.dirname(path) tags = [] name = basename - fldr = "inbox" folder = None for (pattern, name_tmpl, folder_tmpl, tag_tmpl) in rules: m = pattern.match(name) if not m: continue groups = m.groups() - group_vars = {f'{idx + 1}': group for idx, group in enumerate(m.groups())} - local_vars = {'{basename}': name, **group_vars, **variables} + numbered_groups = {f'{idx + 1}': group for idx, group in enumerate(m.groups())} # Numbered groups + named_groups = {f'{k}': v for k, v in m.groupdict().items()} # Named groups + local_vars = {'{basename}': name, **named_groups, **numbered_groups, **variables} if name_tmpl: name = expand_variables(local_vars, name_tmpl) if folder_tmpl: @@ -90,8 +88,8 @@ def process_file(rules, variables, path: str): if tag_tmpl: tags.extend(expand_variables(local_vars, tag_tmpl).split(',')) if folder: - fldr = " of ".join(f"folder \"{fl}\"" for fl in reversed(folder.split('/'))) + " of top level folder" - fldr = f"({fldr})" + folder = " of ".join(f"folder \"{fl}\"" for fl in reversed(folder.split('/'))) + " of top level folder" + folder = f"({fldr})" new_name = name if dirname: new_name = f'{dirname}/{name}' @@ -105,7 +103,12 @@ def process_file(rules, variables, path: str): if not os.path.exists('/usr/local/bin/tag'): raise RuntimeError("Please run `brew install tag'") check_output(['/usr/local/bin/tag', '--add', ','.join(tags), new_name]) - return add_tmpl.format(name=os.path.abspath(new_name), fldr=fldr, tags=', '.join(f'"{t}"' for t in tags)) + # TODO: find a better way to specify this... + if folder.startswith('!keepit'): + folder = folder.replace('!keepit', '').strip('/') or "inbox" + return add_tmpl.format(name=os.path.abspath(new_name), fldr=folder, tags=', '.join(f'"{t}"' for t in tags)) + else: + return None if __name__ == '__main__': confdir = os.path.expanduser('~/.config/watchman/') @@ -115,7 +118,8 @@ def process_file(rules, variables, path: str): with open(os.path.join(confdir, 'rules.conf'), 'r') as f: variables, rules = parse_config(f) if len(sys.argv) > 0: - items = [process_file(rules, variables, infile) for infile in sys.argv[1:]] + exclusions = re.compile(variables.get('exclusions', '^$')) + items = [process_file(rules, variables, infile) for infile in sys.argv[1:] if not exclusions.match(infile)] items = [i for i in items if i] if items: with NamedTemporaryFile(mode='w', delete=True) as tempf: diff --git a/config.json b/config.json index 0f6c762..869eb61 100644 --- a/config.json +++ b/config.json @@ -60,6 +60,7 @@ "tag", "tailor", "terminal-notifier", + "tesseract", "texinfo", "the_silver_searcher", "tmux", @@ -117,19 +118,25 @@ "1Password 7" ], "sources": { - "git@github.com:larkery/zsh-histdb.git": "~/.zsh-histdb/", - "git@github.com:tarjoilija/zgen": { + "https://github.com/larkery/zsh-histdb.git": "~/.zsh-histdb/", + "https://github.com/tarjoilija/zgen": { "symlinks": { "zgen.zsh": "~/.zgen.zsh", "_zgen": "~/.zsh/_zgen" } + }, + "https://github.com/scottwhudson/Lunette.git": { + "symlinks": { + "Lunette.spoon": "~/.hammerspoon/Spoons/" + } } }, "symlinks": { "config/.*": "~/", "shell/*": "~/.config/zsh/config.sh", "bin/*": "~/.local/bin/", - "launchagents/*":"~/Library/LaunchAgents/" + "launchagents/*":"~/Library/LaunchAgents/", + "hammerspoon/*": "~/.hammerspoon/" }, "after-scripts": [ "brew cleanup" diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua new file mode 100644 index 0000000..0670c0e --- /dev/null +++ b/hammerspoon/init.lua @@ -0,0 +1,6 @@ +-- ~/.hammerspoon/init.lua +hs.loadSpoon("Lunette") +spoon.Lunette:bindHotkeys() + +local keepit = require 'keepit' +local mail = require 'mail' diff --git a/hammerspoon/keepit.lua b/hammerspoon/keepit.lua new file mode 100644 index 0000000..b710aa0 --- /dev/null +++ b/hammerspoon/keepit.lua @@ -0,0 +1,104 @@ +-- keepit.lua +-- helpers for Keep It + +local module = {} +local db = hs.sqlite3.open(os.getenv("HOME") .. "/Library/Group Containers/D75L7R8266.com.reinvented.KeepIt/Keep It/Keep It Library.kptlib/KeepIt.sqlite") + +local IDENT_SEL_NOPAR = "select ZIDENTIFIER from ZITEM where ZFILENAME = ?" +local IDENT_SEL_ONEPAR = "select ZIDENTIFIER from ZITEM where ZFOLDER =(SELECT Z_PK from ZGROUP where ZNAME = ?) and ZFILENAME = ?" + +function runcmd(cmd) + local handle = assert(io.popen(cmd, "r")) + local res = handle:read("*all") + handle:close() + return res +end + +module.window_filter = hs.window.filter.new{'Keep It'} +module.keybinds = { + hs.hotkey.bind({"ctrl", "cmd"}, "r", function () + hs.osascript.javascriptFromFile("~/.hammerspoon/scripts/keepit-refile.js") + end), + hs.hotkey.bind({"ctrl", "cmd"}, "l", function () + -- insert Link + local files = runcmd("cd '" .. os.getenv("HOME") .. "/Library/Group Containers/D75L7R8266.com.reinvented.KeepIt/Keep It/Files' && " .. "/usr/local/bin/fd") + -- Build the list of emojis to be displayed. + local choices = {} + for file in string.gmatch(files, '(.-)[\n\r]') do + if file ~= nil then + local path, name = string.match(file, "(.-)/([^/]+)$") + if name ~= nil and path ~= nil then + table.insert(choices, + {text=name, + subText=path}) + end + end + end + + -- Focus the last used window. + local function focusLastFocused() + local wf = hs.window.filter + local lastFocused = wf.defaultCurrentSpace:getWindows(wf.sortByFocusedLast) + if #lastFocused > 0 then lastFocused[1]:focus() end + end + + -- Create the chooser. + -- On selection, copy the emoji and type it into the focused application. + local chooser = hs.chooser.new(function(choice) + if not choice then focusLastFocused(); return end + local path, folder = string.match(choice.subText, "(.-/)-([^/]+)") + local name = choice.text + local stmt = IDENT_SEL_NOPAR + if path ~= nil then + stmt = IDENT_SEL_ONEPAR + end + stmt = db:prepare(stmt) + if path ~= nil then + stmt:bind_values(path, name) + else + stmt:bind_values(name) + end + -- local ident = runcmd(IDENT_SEL_ONEPAR) + -- TODO: get identifier from DB + local ident = nil + for row in stmt:nrows() do + ident = row.ZIDENTIFIER + break + end + stmt:finalize() + focusLastFocused() + if ident ~= nil then + hs.eventtap.keyStrokes("[" .. name .. "](keepit://link?item=" .. ident .. ")") + end + end) + + chooser:rows(5) + -- chooser:bgDark(true) + + + chooser:searchSubText(true) + chooser:choices(choices) + chooser:show() + end) +} +for k, binding in pairs(module.keybinds) do + binding:disable() +end +local focus = function () + -- set up keybindings, etc + for k, binding in pairs(module.keybinds) do + binding:enable() + end +end + +local unfocus = function () + -- remove keybindings, etc + for k, binding in pairs(module.keybinds) do + binding:disable() + end +end + +module.window_filter:subscribe('windowFocused', focus) +module.window_filter:subscribe('windowUnfocused', unfocus) + +return module diff --git a/hammerspoon/mail.lua b/hammerspoon/mail.lua new file mode 100644 index 0000000..28ac284 --- /dev/null +++ b/hammerspoon/mail.lua @@ -0,0 +1,29 @@ +-- mail.lua +-- helpers for mail + +local module = {} +module.keybinds = { + hs.hotkey.bind({"ctrl", "cmd"}, "a", function () + hs.osascript.javascriptFromFile("~/.hammerspoon/scripts/mail-archive.js") +end)} + +for k, binding in pairs(module.keybinds) do + binding:disable() +end +module.window_filter = hs.window.filter.new{'Mail'} +local focus = function () + for k, binding in pairs(module.keybinds) do + binding:enable() + end +end + +local unfocus = function () + for k, binding in pairs(module.keybinds) do + binding:disable() + end +end + +module.window_filter:subscribe('windowFocused', focus) +module.window_filter:subscribe('windowUnfocused', unfocus) + +return module diff --git a/hammerspoon/scripts/ctrl-cmd-r.js b/hammerspoon/scripts/ctrl-cmd-r.js new file mode 100644 index 0000000..806929d --- /dev/null +++ b/hammerspoon/scripts/ctrl-cmd-r.js @@ -0,0 +1,29 @@ +function run(argv) {; + // Rename keepit files according to their modification date, then move them into Life/Year; + var keepit = Application("Keep It"); + var life = null;; + let folders = keepit.folders(); + for (let folder of folders) {; + if (folder.name() == "Life") {; + life = folder; + break; + }; + }; + var years = {};; + for (let folder of life.folders()) {; + years[folder.name()] = folder; + }; +; + keepit.selectedItems().forEach(function(item) {; + let ctime = JSON.stringify(item.created()).split('T')[0].replace('"', ''); + let year = ctime.split('-')[0]; + /*if (!item.name().startsWith(ctime)) { + item.name = ctime + " " + item.name() + }*/ + if (!years.hasOwnProperty(year)) {; + years[year] = new keepit.Folder({name: year, parentFolder: life}); + }; + console.log(item.name()); + item.move({to: years[year]}); + }); +}; diff --git a/install.py b/install.py index b89a549..948f618 100644 --- a/install.py +++ b/install.py @@ -9,7 +9,20 @@ def is_str(s): - return isinstance(s, (str, unicode)) + return isinstance(s, str) + +def getpath(source): + if '@' in source: + # SSH + chunks = source.split(':')[-1].split('/') + site = source.split('@')[-1].split(':')[0] + user = chunks[0] + repo = chunks[-1].replace('.git', '') + else: + url = source.split('://')[-1] + chunks = url.split('/') + site, user, repo = chunks[:3] + return site, user, repo @contextmanager def chdir(path): @@ -40,7 +53,7 @@ def install_sources(sources): mkdir(os.path.expanduser('~/.config/zsh/repos')) for source, config in sources.items(): print("Cloning {}".format(source)) - if isinstance(config, (str, unicode)): + if isinstance(config, str): # TODO: clone directly here config = os.path.expanduser(config) if not os.path.isdir(config): @@ -49,11 +62,8 @@ def install_sources(sources): with chdir(config): runcmd("git pull") else: - chunks = source.split(':')[-1].split('/') - user = chunks[0] - repo = chunks[-1].replace('.git', '') - repo_dir = os.path.expanduser("~/.config/zsh/repos/{}-{}".format(user, repo)) - mkdir(repo_dir) + site, user, repo = getpath(source) + repo_dir = os.path.expanduser("~/.config/zsh/repos/{}/{}-{}".format(site, user, repo)) if not os.path.isdir(repo_dir): runcmd("git clone {} {}".format(source, repo_dir)) else: @@ -73,20 +83,22 @@ def install_symlinks(config): # relative paths src = os.path.join(os.getcwd(), src) sources = sorted(glob(os.path.expanduser(src))) - + if not sources: + continue if dst.endswith('/'): # Its a directory. each file should be copied for path in sources: # print('Linking {} -> {}{}'.format(path, dst, os.path.basename(path))) - mkdir(dst) + mkdir(os.path.expanduser(dst)) ldst = os.path.expanduser('{}{}'.format(dst, os.path.basename(path))) if os.path.islink(ldst): os.unlink(ldst) elif os.path.exists(ldst): raise RuntimeError('{} already exists and is not controlled by us!'.format(ldst)) assert not os.path.islink(ldst) - os.symlink(os.path.expanduser(path), ldst) - else: + path = os.path.expanduser(path) + os.symlink(path, ldst, target_is_directory=os.path.isdir(path)) + elif os.path.isfile(sources[0]): # Combine/link into file # print('Combining {} into {}'.format(sources, dst)) dst = os.path.expanduser(dst) @@ -98,6 +110,14 @@ def install_symlinks(config): outf.write('## {}\n'.format(path)) outf.write(f.read()) outf.write('\n') + elif os.path.isdir(sources[0]): + ldst = os.path.expanduser(dst) + if os.path.islink(ldst): + os.unlink(ldst) + elif os.path.exists(ldst): + raise RuntimeError('{} already exists and is not controlled by us!'.format(ldst)) + assert not os.path.islink(ldst) + os.symlink(os.path.expanduser(path), ldst, target_is_directory=True) def install_taps(taps): for tap in taps: diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 index ad8e122..ed86635 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -python install.py config.json $* +python3 install.py config.json $* # cask requires passwords sometimes -xargs