First sketch of a macro which pastes one or more code or text snippets from Quiver.
(I notice that there is a LaunchBar action which does something similar, but it depends on the installation of Node.js and npm modules - this draft macro uses a JavaScript for Automation action, which has no external dependencies, other than a standard installation of Quiver itself).
Paste one or more Quiver snippets ver 2.kmmacros (47.0 KB)
ES 6 version of source
(Macro also provides an ES5 version for use with pre Sierra macOS)
(() => {
'use strict';
// Copyright (c) 2016 Robin Trew
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ( MAIN at end of this file )
// QUIVER LIBRARY TO USE
let strQVLibName = 'Default library',
strQVLibPath = '~/Library/Containers/com.happenapps.Quiver' +
'/Data/Library/Application\ Support/Quiver/Quiver.qvlibrary/';
let strMRUName = 'quiverMenuMemory';
// MENU LEVELS
// Sequence of two menu-path processing functions.
// (One function for each junction between successive menu levels).
// Each particular menu function is compiled from the specs here by
// the higher-order nestedMenu() function. (See below).
// The value of .menuItems should be a function from a [(k,v)] chain of
// given length to a list of contextual menu item pairs [(key,value)]
// (e.g. the books in a chosen library, or the notes in a chosen book)
// Surplus unconfirmed (k, v) pairs (remembered from previous sessions)
// are used as initial selections in the displayed menu.
let lstMenus = [{
title: 'Quiver notebooks',
msg: 'Choose a notebook:',
menuItems: lstPath => {
return resList(lstPath.length ? (
lstPath[0][1]
) : defaultLibPath())
.map(dct => [dct.name, dct.uuid]);
}
}, {
title: 'Quiver notes',
msg: 'Choose note(s):',
multiple: true,
menuItems: lstPath => {
return resList(
lstPath[0][1] + lstPath[1][1] + '.qvnotebook/',
'title'
)
.map(dct => [dct.title, dct.uuid]);
},
cancelName: 'Back'
}]
.map(nestedMenu);
// GENERIC FUNCTIONS FOR NESTED MENUS
// nestedMenu() generates a function from a key-value list
// (remembered choice sequence, retrieved from a JSON file)
// to a new key-value sequence (longer, shorter, or adjusted)
// wrapped with a confirmation status flag.
//
// Unconfirmed final key-value pairs are read as menu pre-selections
// confirmed final key-value pairs are used as menu output.
//
// The second argument to nesteMenu is a menu stage index,
// usually provided automatically by Array.map
// This is used to derive the length of choice paths that are
// handled by particular menu-stage functions.
//
// nestedMenu :: {title:String, msg:String, menuItems:([(k, v)] -> {
// path: [(k,v)],
// confirmed: Bool
// }),
// multiple:Bool} -> Int ->
// ( [(k, v)] -> { path: [(k,v)], confirmed: Bool} )
function nestedMenu(dctSpec, i) {
let nMenu = (i || 0) + 1;
return lstPath => {
// find :: (a -> Bool) -> [a] -> Maybe a
let find = (p, xs) => {
for (var i = 0, lng = xs.length; i < lng; i++) {
if (p(xs[i])) return xs[i];
}
return undefined;
};
let menuItems = dctSpec.menuItems,
lstOptions = menuItems instanceof Array ? (
menuItems
) : menuItems(lstPath),
lstMenuKeys = (lstOptions || [
['x', 0],
['y', 1],
['z', 2]
])
.map(x => x[0]);
let blnMult = (dctSpec
.multiple !== undefined ? dctSpec.multiple : false),
lstPathKeys = (lstPath || [])
.map(x => x[0]),
lngPathKeys = lstPathKeys.length;
let a = Application('System Events'),
sa = (a.includeStandardAdditions = true, a);
let varResult = lstMenuKeys.length ? sa
.activate() &&
sa.chooseFromList(lstMenuKeys, {
withTitle: (dctSpec.title || 'dctSpec.title'),
withPrompt: lstPathKeys.slice(0, nMenu)
.join(' > ') +
'\n\n' + (dctSpec.msg || 'dctSpec.msg'),
defaultItems: (lngPathKeys > nMenu ? (
lstPathKeys.slice(blnMult ? nMenu : -1)
) : lstMenuKeys[0]),
okButtonName: 'OK',
cancelButtonName: dctSpec.cancelName || 'Cancel',
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: true
}) : [];
let lstParentPath = lstPath.slice(0, nMenu);
return {
path: varResult !== false ? lstParentPath
.concat(
varResult
.map(k => find(x => x[0] === k, lstOptions))
) : lstParentPath,
confirmed: varResult !== false,
cancelled: (nMenu === 1) && (varResult === false)
};
};
}
// Index of appropriate subMenu, given current path length and status)
// menuIndexforPathLength :: MPath [(k, v)] -> Int
let menuIndexforPathLength = mPath => {
let intStages = mPath.path.length,
intMenus = lstMenus.length;
// Last menu (if path already full-length)
// or zero-index the length of the *confirmed* part of the path
let index = (intStages >= (intMenus + 1)) ?
(intMenus - 1) : (intStages - (mPath.confirmed ? 1 : 2));
return index < 0 ? 0 : index;
}
// QUIVER-SPECIFIC FUNCTIONS
let defaultLibPath = () => {
let strPath = strQVLibPath +
(strQVLibPath.charAt(-1) !== '/' ? '/' : '');
return (doesDirectoryExist(
strPath) ?
strPath : undefined);
},
// resList :: PathString -> maybe String -> [(String, UUID)]
resList = (strFolderPath, strField) => {
let strLabel = strField || 'name';
return strFolderPath ? listDirectory(strFolderPath)
.reduce((a, x) => {
let strMetaPath = strFolderPath + x + '/meta.json';
return (doesFileExist(strMetaPath)) ? (
a.concat(JSON.parse(readFile(strMetaPath)))
) : a;
}, [])
.sort((a, b) => {
let strA = a[strLabel].toLowerCase(),
strB = b[strLabel].toLowerCase();
return strA < strB ? -1 : (strA > strB ? 1 : 0);
}) : [];
},
// noteText :: libpath -> bookuuid -> noteuuid -> text
noteText = (strLibPath, strBookUUID, strNoteUUID) => {
let strNotePath = strLibPath + strBookUUID + '.qvnotebook/' +
strNoteUUID + '.qvnote/content.json',
dctNote = doesFileExist(strNotePath) ? (
JSON.parse(readFile(strNotePath))
) : undefined;
return dctNote ? (
dctNote.cells
.reduce(
(a, x) => ['code', 'text'].indexOf(x.type) !== -1 ? (
a + x.data + '\n\n'
) : a,
''
)
) : 'note not found';
};
// GENERIC FUNCTIONS
// show :: a -> String
let show = x => JSON.stringify(x, null, 2),
// until :: (a -> Bool) -> (a -> a) -> a -> a
until = (p, f, x) => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// Generic file-system functions
// doesDirectoryExist :: FilePath -> (FileManager|) -> IO Bool
let doesDirectoryExist = strPath =>
$.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(strPath)
.stringByStandardizingPath, Ref()
) && ref[0] === 1,
// doesFileExist :: String -> Bool
doesFileExist = strPath => {
let error = $();
return (
$.NSFileManager.defaultManager
.attributesOfItemAtPathError(
ObjC.unwrap($(strPath)
.stringByStandardizingPath),
error
),
error.code === undefined
);
},
// listDirectory :: FilePath -> [FilePath]
listDirectory = strPath => ObjC.unwrap(
$.NSFileManager.defaultManager
.contentsOfDirectoryAtPathError(
$(strPath)
.stringByStandardizingPath, null
)
)
.map(ObjC.unwrap),
// readFile :: FilePath -> maybe String
readFile = strPath => {
let error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return error.code ? error.localizedDescription : str;
},
// writeFile :: FilePath -> String -> IO ()
writeFile = (strPath, strText) =>
$.NSString.alloc.initWithUTF8String(strText)
.writeToFileAtomicallyEncodingError(
$(strPath)
.stringByStandardizingPath, false,
$.NSUTF8StringEncoding, null
),
scriptFolder = () => {
var a = Application.currentApplication();
let sa = (a.includeStandardAdditions = true, a);
return ObjC.unwrap(
$(sa.pathTo(this)
.toString())
.stringByDeletingLastPathComponent
);
};
// MAIN – Read, edit, save and use a key:value chain of menu choices.
// 1. INITIAL CHOICE PATH - retrieved from JSON MRU file, or
// from Keyboard Maestro MRU variable
// (defaulting otherwise to minimum (qvLibName, qvLibPath) pair
let strScriptFolder = scriptFolder(),
strMemoryFile = strScriptFolder + '/' + strMRUName + '.json',
blnKM = strScriptFolder.indexOf('/private/var') === 0;
if (blnKM) {
var kme = Application('Keyboard Maestro Engine'),
strJSON = kme.getvariable(strMRUName);
} else {
var strJSON = (doesFileExist(strMemoryFile) ? (
readFile(strMemoryFile)
) : '');
}
let lstMRUPath = strJSON.length > 0 ? JSON.parse(strJSON) : [
[strQVLibName, strQVLibPath]
]
// 2. CHOICE PATH EDITING by repeated application of (path-length-specific)
// menu functions, until the adjusted path is confirmed or dropped
let intMenus = lstMenus.length,
mChosenPath = until( // p, f, x – predicate, function, value
m => (m.cancelled ||
(m.confirmed && m.path.length > intMenus)),
m => lstMenus[menuIndexforPathLength(m)](m.path), {
path: lstMRUPath,
confirmed: false,
cancelled: false
}
);
// 3. SERIALISATION of any confirmed [(k, v)] chain to JSON file
// (in same folder as script, for use as MRU path in next session)
if (blnKM) {
kme.setvariable(strMRUName, {
to: show(mChosenPath.path)
})
} else writeFile(strMemoryFile, show(mChosenPath.path));
// 4. TRANSLATION & USE of any confirmed [(k, v)] chain.
// ( Here, returning concatenated text of selected Quiver snippets )
let kvs = mChosenPath.path;
// CONCATENATED SELECTION OF QUIVER SNIPPETS
return kvs.slice(intMenus) // Notes
.map(note => kvs.slice(0, intMenus) // Book paths
.concat([note]) // Individual note [kv] appended to book stem [kv]
.map(kv => kv[1])) // Values unzipped from key-value pairs
.map(xs => noteText.apply(null, xs)) // (Libpath, bookUUID, noteUUID)
.join('\n');
})();