diff --git a/macroAndSubMacroList.kmmacros b/macroAndSubMacroList.kmmacros new file mode 100644 index 0000000..323b9f3 --- /dev/null +++ b/macroAndSubMacroList.kmmacros @@ -0,0 +1,13124 @@ + + + + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997196 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_Window Title + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + // MD link to file selected in Obsidian v. 0.10.1 + + // Rob Trew @2020, @2024 + // Ver 0.5 + + // main :: IO () + const main = () => + either( + alert("Copy as MD Link") + )( + mdLink => mdLink + )( + bindLR( + openObsidianVaultIdPathsLR() + )( + mdLinkFromIdPathsLR( + kmvar.local_Window_Title + ) + ) + ); + + + // openObsidianVaultIdPathsLR :: () -> IO Either String FilePath + const openObsidianVaultIdPathsLR = () => + bindLR( + bindLR( + readFileLR( + [ + "~/Library/Application Support", + "/Obsidian/obsidian.json" + ] + .join("") + ) + )( + jsonParseLR + ) + )( + dict => "vaults" in dict + ? Right( + Object.entries(dict.vaults) + .flatMap( + ([k, v]) => v.open + ? [Tuple(k)(v.path)] + : [] + ) + ) + : Left("No 'vaults' key found in Obsidian.json") + + ); + + // mdLinkFromIdPathsLR :: + // String -> [(id, FilePath)] -> Either String String + const mdLinkFromIdPathsLR = windowTitle => + idPaths => { + const + noteAndVault = windowTitle + .split(" - ") + .slice(0, -1) + .join(" - "), + iVault = idPaths.findIndex( + idFp => noteAndVault.endsWith( + takeFileName(idFp[1]) + ) + ); + + return fmapLR(idPath => { + const + vaultName = takeFileName(idPath[1]), + noteName = noteAndVault.slice( + 0, + (noteAndVault.length - vaultName.length) - 3 + ); + + return `[${noteName}](` + ( + `obsidian://open?vault=${idPath[0]}&` + ( + `file=${encodeURIComponent(noteName)})` + ) + ); + })( + -1 !== iVault + ? Right(idPaths[iVault]) + : Left( + "Window title doesn't match open vault name." + ) + ); + }; + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + // readFileLR :: FilePath -> Either String IO String + const readFileLR = fp => { + // Either a message or the contents of any + // text file at the given filepath. + const + uw = ObjC.unwrap, + e = $(), + ns = $.NSString + .stringWithContentsOfFileEncodingError( + $(fp).stringByStandardizingPath, + $.NSUTF8StringEncoding, + e + ); + + return ns.isNil() + ? Left(uw(e.localizedDescription)) + : Right(uw(ns)); + }; + + + // --------------------- GENERIC --------------------- + // https: //github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2 + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = lr => + // Bind operator for the Either option type. + // If lr has a Left value then lr unchanged, + // otherwise the function mf applied to the + // Right value in lr. + mf => "Left" in lr + ? lr + : mf(lr.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c + const fmapLR = f => + // Either f mapped into the contents of any Right + // value in e, or e unchanged if is a Left value. + e => "Left" in e + ? e + : Right(f(e.Right)); + + + // jsonParseLR :: String -> Either String a + const jsonParseLR = s => { + // Either a message, or a JS value obtained + // from a successful parse of s. + try { + return Right(JSON.parse(s)); + } catch (e) { + return Left( + `${e.message} (line:${e.line} col:${e.column})` + ); + } + }; + + // takeFileName :: FilePath -> FilePath + const takeFileName = fp => + // The file name component of a filepath. + 0 < fp.length + ? "/" !== fp[fp.length - 1] + ? fp.split("/").slice(-1)[0] + : "" + : ""; + + + // MAIN -- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 740856569.43946898 + ModificationDate + 740988293.47620702 + Name + Obsidian link from window title + Triggers + + + MacroTriggerType + Subroutine + Parameters + + local_Window Title + + ReturnsValue + + + + UID + 3447E395-DDE1-4BBF-8CAE-AC5AEF4CC125 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997043 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + ActionName + From front application and selection to mdLink string value + ActionUID + 15997044 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // Rob Trew @ 2020 + + // Copy Markdown Link to front document, URL, or resource. + // Ver 0.34 + + // Switched to running app-specific macros by UUID + // fetched from a JSON dictionary stored in a + // uuidsForMDLink KM variable. + + // If this variable is not found, or a UUID retrieved + // from it is not found, then the dictionary is regenerated. + + // The regeneration, which will happen on the first + // run, but should only be needed thereafter when + // new sub-macros are added, will activate Keyboard Maestro.app + + // Normally use of the macro will, however, normally + // bypass Keyboard Maestro.app and run through + // Keyboard Maestro Engine instead. + + ObjC.import("AppKit"); + + const kmGroupName = "MD link tools"; + + // ---------------------- MAIN ----------------------- + // main :: IO () + // eslint-disable-next-line max-lines-per-function + const main = () => { + const bundleID = frontAppBundleId(); + + return either( + msg => ( + alert("Copy as Markdown link")(msg), + msg + ) + )( + mdLink => mdLink + )( + bindLR( + void 0 !== bundleID + ? Right(bundleID) + : Left( + "No active application detected" + ) + )(linkForBundleLR) + ); + }; + + // linkForBundleLR :: String -> Either String String + const linkForBundleLR = bundleID => + // ------------ BROWSER ? ------------ + + [ + "com.apple.Safari", + "com.google.Chrome", + "com.microsoft.edgemac", + "com.vivaldi.Vivaldi", + "com.kagi.kagimacOS", + "com.operasoftware.Opera", + "company.thebrowser.Browser" + ].includes(bundleID) + ? browserLinkLR(bundleID) + : (() => { + // ---- APP-SPECIFIC MACRO ? ----- + const + kme = Application("Keyboard Maestro Engine"), + dctUUID = either( + msg => ( + // eslint-disable-next-line no-console + console.log( + "BundleID map had to be regenerated", + msg + ), + // Regenerated UUID dictionary + updatedUUIDMap() + ) + )( + // UUID dictionary from existing + // KM Variable + dct => dct + )( + jsonParseLR( + kme.getvariable("uuidsForMDLink") + ) + ); + + return linkFromUUID(kme)(bundleID)( + dctUUID[bundleID] + ); + })(); + + // linkFromUUID :: Application -> + // String -> String -> String + const linkFromUUID = kme => + bundleID => maybeUUID => Boolean(maybeUUID) + ? either( + // If the UUID wasn"t found, + // then run a new one from an + // updated dictionary. + () => ( + bindLR( + doScriptLR(kme)( + updatedUUIDMap()[bundleID] + ) + )( + // Link after use of alternate UUID + () => Right( + kme.getvariable("mdLink") + ) + ) + ) + )( + // Link read after with UUID + () => Right(kme.getvariable("mdLink")) + )( + // Run macro with this UUID if possible. + doScriptLR(kme)(maybeUUID) + ) + : appFrontWindowMDLinkLR(bundleID); + + + // doScriptLR :: UUID -> Either String String + const doScriptLR = kme => + uuid => { + try { + return ( + kme.doScript(uuid), + Right(uuid) + ); + } catch (e) { + return Left( + `Macro UUID :: ${uuid}\n\n${e.message}` + ); + } + }; + + // -------------- BUNDLEID -> UUID MAP --------------- + + // updatedUUIDMap :: IO () -> { bundleID :: UUID } + const updatedUUIDMap = () => { + const + macroGroupName = "MD Link tools", + mdLinkToolsGroups = Application( + "Keyboard Maestro" + ).macroGroups.where({ + name: macroGroupName + }); + + return either( + alert("Copy as MD Link - Map bundle to UUID") + )( + dictUUIDs => ( + Application("Keyboard Maestro Engine") + .setvariable("uuidsForMDLink", { + to: JSON.stringify( + dictUUIDs, null, 2 + ) + }), + dictUUIDs + ) + )( + 0 < mdLinkToolsGroups.length + ? (() => { + const + instances = mdLinkToolsGroups.at(0) + .macros() + .flatMap(macro => { + const k = macro.name(); + + return k.includes(".") + ? [[k, macro.id()]] + : []; + }); + + return Right( + instances.reduce( + (a, [bundle, uuid]) => Object.assign( + a, { + [bundle]: uuid + } + ), {} + ) + ); + })() + : Left( + `Macro group not found:\n\n\t${macroGroupName}` + ) + ); + }; + + + // --------------------- BROWSERS ---------------------- + + // browserLinkLR :: String -> Either String IO String + const browserLinkLR = bundleID => { + const + w = Application(bundleID).windows.at(0), + anySelectedText = copiedText(), + anyHighlight = 0 < anySelectedText.length + ? `#:~:text=${encodeURIComponent(anySelectedText)}` + : ""; + + return "company.thebrowser.Browser" !== bundleID + ? w.exists() + ? w.tabs.at(0).exists() + ? (() => { + const + tab = w[ + [ + "com.apple.Safari", + "com.kagi.kagimacOS" + ] + .includes(bundleID) + ? "currentTab" + : "activeTab" + ](); + + return Right( + `[${tab.name()}](${tab.url()}${anyHighlight})` + ); + })() + : Left( + `No open tabs in front window of ${bundleID}` + ) + : Left(`No windows open in ${bundleID}`) + : Right( + (() => { + const tab = w.activeTab; + + return `[${tab.title()}](${tab.url()}${anyHighlight})`; + })() + ); + }; + + + const copiedText = () => { + const + sa = Object.assign( + Application.currentApplication(), + { includeStandardAdditions: true } + ); + + return ( + sa.setTheClipboardTo(""), + Application("Keyboard Maestro Engine") + .doScript(`<dict> + <key>Action</key> + <string>Copy</string> + <key>IsDisclosed</key> + <false/> + <key>MacroActionType</key> + <string>CutCopyPaste</string> + <key>NotifyOnTimeOut</key> + <false/> + <key>TimeOutAbortsMacro</key> + <true/> + <key>TimeOutPeriod</key> + <real>1</real> + </dict>`), + sa.theClipboard() + ); + }; + + + // ----------------------- JXA ----------------------- + + + // frontAppBundleId :: () -> String + const frontAppBundleId = () => { + const uw = ObjC.unwrap; + + return uw(uw( + $.NSWorkspace.sharedWorkspace.activeApplication + ).NSApplicationBundleIdentifier); + }; + + + + + // ------- DEFAULT - DOCUMENT OF FRONT WINDOW -------- + + // appFrontWindowMDLinkLR :: String -> Either String String + const appFrontWindowMDLinkLR = bundleID => { + const + procs = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }) + .applicationProcesses.where({ + bundleIdentifier: bundleID + }); + + return bindLR( + bindLR( + procs.length > 0 + ? Right(procs.at(0).windows) + : Left(`Application not found: ${bundleID}`) + )(ws => ws.length > 0 + ? Right(ws.at(0)) + : Left(`No windows found for ${bundleID}`)) + )(w => { + const + uw = ObjC.unwrap, + [winTitle, maybeDocURL] = [ + "AXTitle", "AXDocument" + ] + .map(appID => uw( + w.attributes.byName(appID).value() + )); + + return Boolean(maybeDocURL) + ? Right(`[${winTitle}](${maybeDocURL})`) + : Left( + [ + `Window "${winTitle}" of:\n\n\t${bundleID}`, + "\nmay not be a document window.", + `\nConsider adding a macro named "${bundleID}"`, + `to the KM Group "${kmGroupName}".`, + "\n(Or request such a macro, which should", + "save a [label](url) string) in the", + "KM variable \"mdLink\")", + "on the Keyboard Maestro forum)." + ].join("\n") + ); + }); + }; + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left + ? m + : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // jsonParseLR :: String -> Either String a + const jsonParseLR = s => { + // Either a message, or a JS value obtained + // from a successful parse of s. + try { + return Right(JSON.parse(s)); + } catch (e) { + return Left( + `${e.message} (line:${e.line} col:${e.column})` + ); + } + }; + + // showLog :: a -> IO () + const showLog = (...args) => + // eslint-disable-next-line no-console + console.log( + args + .map(JSON.stringify) + .join(" -> ") + ); + + // MAIN --- + return main(); +})(); + + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + ActionName + Copy to clipboard both as MD links and as RTF labelled hyperlinks + ActionUID + 15997045 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // Rob Trew @2021, @2022 + + // Value of KM mdLink variable placed in clipboard, + // both as plain text, + // and also as a labelled (and styled) RTF hyperlink. + + // Ver 0.2 added RTF link style options. + // Ver 0.3 updated link style font, size, color. + // Ver 0.4 added a pasteboard item for Bike Outliner + + ObjC.import("AppKit"); + + // main :: IO () + const main = () => { + const linkStyle = { + "color": "#ACACAC", + "font-family": "Helvetica Neue, sans-serif", + "font-size": "13px" + }; + + const + md = Application("Keyboard Maestro Engine") + .getvariable("mdLink"), + labelLinks = mdLinkPartPairs(md); + + return ( + copyTypedString(true)( + "public.utf8-plain-text" + )(md), + copyTypedString(false)( + "com.hogbaysoftware.bike.xml" + )( + bikeLinksXML(labelLinks) + ), + either( + alert("Copy as RTF Link") + )( + copyTypedString(false)("public.rtf") + )( + rtfFromHTML( + styledLinks(linkStyle)( + labelLinks + ) + ) + ), + md + ); + }; + + + // ---------------------- LINKS ---------------------- + + const bikeLinksXML = labelLinks => { + const + ps = labelLinks.map( + ([k, url]) => + `<li><p><a href="${url}">${k}</a></p></li>` + ).join("\n"); + + return `<html><body><ul>${ps}</ul></body></html>`; + }; + + // copyTypedString :: Bool -> String -> String -> IO () + const copyTypedString = blnClear => + // public.html, public.rtf, public.utf8-plain-text + pbType => s => { + const pb = $.NSPasteboard.generalPasteboard; + + return ( + blnClear && pb.clearContents, + pb.setStringForType( + $(s), + $(pbType) + ) + ); + }; + + // cssTag :: Dict {String :: String} -> String + const cssTag = settings => { + const + kvs = Object.entries(settings) + .map(([k, v]) => `${k}: ${v};`) + .join(" "), + css = `p { ${kvs} }`; + + return `<style type="text/css">${css}</style>`; + }; + + // htmlEncoded :: String -> String + const htmlEncoded = s => { + const rgx = /[\w\s]/u; + + return [...s].map( + c => rgx.test(c) ? ( + c + ) : `&#${c.codePointAt(0)};` + ).join(""); + }; + + + // mdLinkPartPairs :: String -> [(String, String)] + const mdLinkPartPairs = s => + lines(s).map(x => { + const ab = x.trim().split("]("); + + return 2 !== ab.length ? ( + Tuple(s)("") + ) : Tuple(ab[0].slice(1))( + ab[1].slice(0, -1) + ); + }); + + + // rtfFromHTML :: String -> Either String String + const rtfFromHTML = strHTML => { + const + as = $.NSAttributedString.alloc + .initWithHTMLDocumentAttributes($(strHTML) + .dataUsingEncoding($.NSUTF8StringEncoding), + 0 + ); + + return bindLR( + "function" !== typeof as + .dataFromRangeDocumentAttributesError ? ( + Left("String could not be parsed as HTML") + ) : Right(as) + )( + // Function bound if Right value obtained above: + htmlAS => { + const + error = $(), + rtfData = htmlAS + .dataFromRangeDocumentAttributesError({ + "location": 0, + "length": htmlAS.length + }, { + DocumentType: "NSRTF" + }, + error + ); + + return Boolean( + ObjC.unwrap(rtfData) && !error.code + ) ? Right( + ObjC.unwrap($.NSString.alloc + .initWithDataEncoding( + rtfData, + $.NSUTF8StringEncoding + )) + ) : Left(ObjC.unwrap( + error.localizedDescription + )); + } + ); + }; + + // styledLinks :: Dict -> [(String, String)] -> String + const styledLinks = styleDict => + // One or more <a href> lines, wrapped in <p>...</p> + // and preceded by a <style> tag based on styleDict. + kvs => { + const + css = cssTag(styleDict), + linkTags = kvs.map(kv => { + const [label, url] = biList(kv).map( + htmlEncoded + ), + labelOrFullLink = Boolean(url) ? ( + `<a href="${url}">${label}</a>` + ) : label; + + return `${labelOrFullLink}`; + }) + .join("<br>"); + + return `${css}\n<p>${linkTags}</p>`; + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + // A pair of values, possibly of + // different types. + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2, + *[Symbol.iterator]() { + for (const k in this) { + if (!isNaN(k)) { + yield this[k]; + } + } + } + }); + + // biList :: (a, a) -> [a] + const biList = ab => + // A list of two items derived from a tuple. + Array.from(ab); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e ? ( + fl(e.Left) + ) : fr(e.Right); + + // lines :: String -> [String] + const lines = s => + // A list of strings derived from a single + // string delimited by newline and or CR. + 0 < s.length ? ( + s.split(/[\r\n]+/u) + ) : []; + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + ActionUID + 295 + IsActive + + JustDisplay + + MacroActionType + SetClipboardToText + Text + %Variable%mdLink% + + + ActionName + Notification + ActionUID + 15997046 + Conditions + + ConditionList + + + ConditionType + Variable + Variable + mdLink + VariableConditionType + IsNot + VariableValue + + + + ConditionListMatch + All + + ElseActions + + + ActionUID + 15997048 + MacroActionType + Notification + SoundName + Glass + Subtitle + Nothing copied + Text + %Application%1% + Title + %ExecutingMacro% and RTF labelled hyperlink + + + MacroActionType + IfThenElse + ThenActions + + + ActionUID + 15997047 + MacroActionType + Notification + SoundName + Glass + Subtitle + + Text + %Variable%mdLink% + Title + %ExecutingMacro% and RTF labelled hyperlink + + + ActionUID + 15487694 + MacroActionType + SetVariableToText + Text + %Delete% + Variable + mdLink + + + TimeOutAbortsMacro + + + + CreationDate + 591010979.372293 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 744905251.65038097 + Name + Copy as Markdown link + Triggers + + + FireType + Pressed + KeyCode + 46 + MacroTriggerType + HotKey + Modifiers + 2304 + + + UID + 6ABBE175-6098-4799-9C62-418126866B96 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997197 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either(alert('Copy Markdown link from MarginNote 3'))(x => x)( + bindLR( + menuItemClickedLR('MarginNote 3')([ + 'Edit', 'Copy note URL' + ]) + )(_ => { + const strAddr = ( + delay(0.2), + clipboardText() + ); + return bindLR( + strAddr.startsWith('marginnote') ? ( + Right(strAddr) + ) : Left('No address copied') + )(address => bindLR( + menuItemClickedLR('MarginNote 3')([ + 'Edit', 'Copy' + ]) + )(_ => { + const strName = ( + delay(0.5), // ADJUST HERE ... + clipboardText() + ); + return !strName.startsWith('marginnote') ? ( + Right('[' + + strName.split(/\n/)[0] + + '](' + address + ')') + ) : Left('No Name copied - try increasing delay ...') + })) + }) + ); + + // alert :: String -> String -> IO String + const alert = title => s => { + const + sa = Object.assign(Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + + // clipboardText :: IO () -> String + const clipboardText = () => + // Any plain text in the clipboard. + ObjC.unwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType($.NSPasteboardTypeString), + $.NSUTF8StringEncoding + ) + ); + + // menuItemClickedLR :: String -> [String] -> Either String IO String + const menuItemClickedLR = strAppName => menuParts => { + const intMenuPath = menuParts.length; + return 1 < intMenuPath ? (() => { + const + appProcs = Application('System Events') + .processes.where({ + name: strAppName + }); + return 0 < appProcs.length ? (() => { + Application(strAppName).activate(); + delay(0.1); + return bindLR( + menuParts.slice(1, -1) + .reduce( + (lra, x) => bindLR(lra)(a => { + const menuItem = a.menuItems[x]; + return menuItem.exists() ? ( + Right(menuItem.menus[x]) + ) : Left('Menu item not found: ' + x); + })(), + (() => { + const + k = menuParts[0], + menu = appProcs[0].menuBars[0] + .menus.byName(k); + return menu.exists() ? ( + Right(menu) + ) : Left('Menu not found: ' + k); + })() + ) + )(xs => { + const + k = menuParts[intMenuPath - 1], + items = xs.menuItems, + strPath = [strAppName] + .concat(menuParts).join(' > '); + return bindLR( + items[k].exists() ? ( + Right(items[k]) + ) : Left('Menu item not found: ' + k) + )(x => x.enabled() ? ( + x.click(), + Right('Clicked: ' + strPath) + ) : Left( + 'Menu item disabled : ' + strPath + )) + }) + })() : Left(strAppName + ' not running.'); + })() : Left( + 'MenuItemClickedLR needs a menu path of 2+ items.' + ); + }; + + + // GENERIC FUNCTIONS ---------------------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b + const bindLR = m => mf => + undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => fr => e => + 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 593718996.65122902 + ModificationDate + 634818696.82369196 + Name + QReader.MarginStudyMac + Triggers + + UID + 22FB516D-D15A-420C-99B0-191EE1138BA1 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997163 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + org.mozilla.firefox + Name + Firefox + NewFile + /Applications/Firefox.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997164 + IsDisclosed + + KeyCode + 37 + MacroActionType + SimulateKeystroke + Modifiers + 256 + ReleaseAll + + TargetApplication + + TargetingType + Front + + + Action + Copy + ActionUID + 15997165 + IsDisclosed + + MacroActionType + CutCopyPaste + TimeOutAbortsMacro + + + + ActionUID + 15997166 + MacroActionType + SetVariableToText + Text + [%FrontWindowName%](%SystemClipboard%) + Variable + mdLink + + + CreationDate + 620235747.35165203 + ModificationDate + 634818691.54600894 + Name + org.mozilla.firefox + Triggers + + UID + 196FB146-050B-474E-B28A-2C8CB03F1D42 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997191 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997192 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + net.shinyfrog.bear + Name + Bear + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997193 + MacroActionType + SelectMenuItem + Menu + + Note + Copy Link To Note + + TargetApplication + + BundleIdentifier + net.shinyfrog.bear + Name + Bear + + TargetingType + Specific + + + ActionUID + 15997194 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997195 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Rob Trew @2020 + // Copy Bear selection as MD Link. + + const main = () => + either( + alert('Copy as MD Link') + )( + copyText + )( + mdLinkFromClipTypes( + 'net.shinyfrog.bear.url-name' + )( + 'public.utf8-plain-text' + ) + ); + + // ---------- LINK PARTS FROM CLIPBOARD TYPES ---------- + + // mdLinkFromClipTypes UTI String -> + // UTI String -> Either String MDLink + const mdLinkFromClipTypes = nameUTI => + urlUTI => bindLR( + clipOfTypeLR(nameUTI) + )(name => bindLR( + clipOfTypeLR(urlUTI) + )(url => Right(`[${name}](${url})`))); + + + // ------------- JAVASCRIPT FOR AUTOMATION ------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + // ObjC.import('AppKit'); + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // copyText :: String -> IO String + const copyText = s => { + // ObjC.import('AppKit'); + const pb = $.NSPasteboard.generalPasteboard; + return ( + pb.clearContents, + pb.setStringForType( + $(s), + $.NSPasteboardTypeString + ), + s + ); + }; + + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 622068180.07188797 + ModificationDate + 678465748.77456903 + Name + net.shinyfrog.bear + Triggers + + UID + EF7011E5-1758-42DC-B377-865EB185D842 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997020 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + Tested for Soulver 3 + + + ActionUID + 15997021 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997022 + MacroActionType + SelectMenuItem + Menu + + Sheet + Copy Link to Sheet + + TargetApplication + + BundleIdentifier + app.soulver.appstore.mac + Name + Soulver 3 + NewFile + /Applications/Soulver 3.app + + TargetingType + Specific + + + ActionName + Pause until text clipboard not empty + ActionUID + 15997023 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997024 + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + local_URL + + + ActionUID + 15997025 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_URL + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + const + bundleID = 'app.soulver.mac', + se = ( + Application(bundleID).activate(), + Application('System Events') + ), + procs = se.applicationProcesses.where({ + name: 'Soulver 3' + }), + sheetName = 0 < procs.length ? ( + procs.at(0).windows.at(0).name() + ) : 'Soulver 3 sheet name not found.'; + +return `[${sheetName}](${kmvar.local_URL})`; + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 740416244.844733 + ModificationDate + 740416261.00207806 + Name + app.soulver.mac + Triggers + + UID + 482DAAA1-E342-40D1-822D-988BB9AA8B34 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997077 + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC78BQAAKwAAAAEA + AAD0BQAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYyNTEz + Clxjb2NvYXRleHRzY2FsaW5nMFxj + b2NvYXBsYXRmb3JtMHtcZm9udHRi + bFxmMFxmc3dpc3NcZmNoYXJzZXQw + IEhlbHZldGljYTtcZjFcZm5pbFxm + Y2hhcnNldDAgTWVubG8tUmVndWxh + cjtcZjJcZnN3aXNzXGZjaGFyc2V0 + MCBIZWx2ZXRpY2EtT2JsaXF1ZTsK + fQp7XGNvbG9ydGJsO1xyZWQyNTVc + Z3JlZW4yNTVcYmx1ZTI1NTtccmVk + MFxncmVlbjBcYmx1ZTA7fQp7XCpc + ZXhwYW5kZWRjb2xvcnRibDs7XGNz + c3JnYlxjMFxjMFxjMFxjODQ3MDZc + Y25hbWUgY29udHJvbFRleHRDb2xv + cjt9ClxwYXJkXHR4NTYwXHR4MTEy + MFx0eDE2ODBcdHgyMjQwXHR4Mjgw + MFx0eDMzNjBcdHgzOTIwXHR4NDQ4 + MFx0eDUwNDBcdHg1NjAwXHR4NjE2 + MFx0eDY3MjBccGFyZGlybmF0dXJh + bFxwYXJ0aWdodGVuZmFjdG9yMAoK + XGYwXGZzMzBcZnNtaWxsaTE1MzY0 + IFxjZjIgRm9yIGEgbWVudSBvZiBt + YXJrZG93biBsaW5rcyBpbiB0aGUg + ZnJvbnQgZG9jdW1lbnQgaW4gVGFz + a1BhcGVyIDMsIGVkaXQgOlwKXHBh + cmRcdHg1NjBcdHgxMTIwXHR4MTY4 + MFx0eDIyNDBcdHgyODAwXHR4MzM2 + MFx0eDM5MjBcdHg0NDgwXHR4NTA0 + MFx0eDU2MDBcdHg2MTYwXHR4Njcy + MFxwYXJkaXJuYXR1cmFsXHBhcnRp + Z2h0ZW5mYWN0b3IwCgpcZjFcZnMy + NCBcY2YwIFwKICAgIGNvbnN0IHNo + b3dMaW5rc0luVGFza1BhcGVyRnJv + bnREb2MgPSBmYWxzZTtcClwKXHBh + cmRcdHg1NjBcdHgxMTIwXHR4MTY4 + MFx0eDIyNDBcdHgyODAwXHR4MzM2 + MFx0eDM5MjBcdHg0NDgwXHR4NTA0 + MFx0eDU2MDBcdHg2MTYwXHR4Njcy + MFxwYXJkaXJuYXR1cmFsXHBhcnRp + Z2h0ZW5mYWN0b3IwCgpcZjBcZnMz + MFxmc21pbGxpMTUzNjQgXGNmMiBu + ZWFyIHRoZSB0b3Agb2YgdGhlIEph + dmFTY3JpcHQgZm9yIEF1dG9tYXRp + b24gc2NyaXB0IGJlbG93LCB0bzpc + ClwKCQpcZjFcZnMyNCBcY2YwIGNv + bnN0IHNob3dMaW5rc0luVGFza1Bh + cGVyRnJvbnREb2MgPSB0cnVlO1wK + ClxmMFxmczMwXGZzbWlsbGkxNTM2 + NCBcY2YyIFwKT3IsIG9yIGEgbWVu + dSBvZiBsaW5rcyBrZXB0IGluIGEg + cGFydGljdWxhciB0ZXh0IGZpbGUs + IGluIFttYXJrZG93biBsaW5rXSh1 + cmwpIGZvcm1hdDpcClwKXHBhcmRc + dHg1NjBcdHgxMTIwXHR4MTY4MFx0 + eDIyNDBcdHgyODAwXHR4MzM2MFx0 + eDM5MjBcdHg0NDgwXHR4NTA0MFx0 + eDU2MDBcdHg2MTYwXHR4NjcyMFxw + YXJkaXJuYXR1cmFsXHBhcnRpZ2h0 + ZW5mYWN0b3IwCgpcZjFcZnMyNCBc + Y2YwIC0KXGYwXGZzMzBcZnNtaWxs + aTE1MzY0IFxjZjIgIGVkaXQgdGhl + IHZhbHVlIG9mIApcZjFcZnMyNCBc + Y2YwIHNob3dMaW5rc0luVGFza1Bh + cGVyRnJvbnREb2MgClxmMFxmczMw + XGZzbWlsbGkxNTM2NCBcY2YyIHRv + IApcZjFcZnMyNCBcY2YwIGZhbHNl + LCBcCi0gClxmMFxmczMwXGZzbWls + bGkxNTM2NCBcY2YyIHNwZWNpZnkg + YSBmaWxlIHBhdGggYXMgdGhlIHZh + bHVlIG9mIHRoZSAKXGYyXGkgbGlu + a0xpc3RGaWxlUGF0aCAKXGYwXGkw + ICB2YXJpYWJsZSBiZWxvdy59AQAA + ACMAAAABAAAABwAAAFRYVC5ydGYQ + AAAA+ZNGX7YBAAAAAAAAAAAAAA== + + Title + A menu of links EITHER in a specified text file, OR in the front document in TaskPaper + + + ActionName + SPECIFY HERE THE FILE PATH A TEXT FILE containing MD links of the pattern [label](url) + ActionUID + 15997078 + MacroActionType + SetVariableToText + Text + ~/projects/activeProjects.taskpaper + Variable + linkListFilePath + + + ActionUID + 15997079 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + // Menu of the labels of any links + // (assumed to be in MD format) in the front + // TaskPaper document, or in a file at the filePath + // specified by a Keyboard Maestro variable. + + // If one or more labels are chosen in the menu, + // the corresponding links are opened. + + + // --------------------- SETTINGS ---------------------- + + // EITHER a menu of links in the TP3 front document, + const showLinksInTaskPaperFrontDoc = true; + + // OR a menu of links in a file at a given path, + // specified by a Keyboard Maestro variable. + + // e.g: '~/projects/activeProjects.taskpaper' + const filePathKMVarName = 'linkListFilePath'; + + + // --------------------- MENU CODE --------------------- + // main :: IO () + const main = () => { + return either( + msg => msg.startsWith('User cancelled') ? ( + msg + ) : alert('Link menu')(msg) + )(openLinks)( + bindLR( + showLinksInTaskPaperFrontDoc ? ( + taskPaperFrontDocFilePathLR() + ) : filePathFromKMVariableLR( + filePathKMVarName + ) + )(fp => { + const menuKVs = mdLinkValuesInFile(fp); + return 0 < menuKVs.length ? ( + bindLR( + showMenuLR(true)('Links')( + menuKVs.map(x => x.label) + ) + )(compose( + Right, + menuChoiceValues( + menuKVs + )('label')('link') + )) + ) : Left('No links found in document.'); + }) + ); + }; + + + // ------------ FRONT DOCUMENT IN TASKPAPER ------------ + + // taskPaperFrontDocFilePathLR :: Either String FilePath + const taskPaperFrontDocFilePathLR = () => { + const + tp = Application('TaskPaper'), + ds = tp.documents; + return 0 < ds.length ? ( + Right(ds.at(0).file().toString()) + ) : Left('No document found in TaskPaper'); + }; + + + // ---- FILEPATH GIVEN IN KEYBOARD MAESTRO VARIABLE ---- + const filePathFromKMVariableLR = kmVarName => { + const + fp = Application('Keyboard Maestro Engine') + .getvariable(kmVarName); + return Boolean(fp) ? (() => { + const fpPath = filePath(fp); + return doesFileExist(fpPath) ? ( + Right(fpPath) + ) : Left('No file found at: ' + fpPath); + })() : Left( + 'No value found for KM variable: "' + ( + kmVarName + '"' + ) + ); + }; + + + // ----------- CHOICE OF LINKS IN GIVEN FILE ----------- + + // mdLinkValuesInFile :: FilePath -> + // [{label :: String, link :: String }] + const mdLinkValuesInFile = fp => + sortBy(comparing(x => x.label))( + lines(readFile(fp)).flatMap( + x => x.includes('](') ? ( + parse(mdLinkParse())(strip(x)) + ) : [] + ).map(fst) + ); + + // openLinks :: [URL String] -> IO [URL String] + const openLinks = urls => { + const + sa = Object.assign( + Application.currentApplication(), { + includeStandardAdditions: true + }); + return urls.map(x => ( + sa.openLocation(x), + x + )); + }; + + // menuChoiceValues :: [Dict a] -> + // String -> String -> [String] -> [a] + const menuChoiceValues = menuKVs => + // A map from a list of keys to a list of values, + // given a list of dictionaries, + // with their label and value keys, + // and some subset of label keys. + labelKey => valueKey => ks => { + const + dct = menuKVs.reduce( + (a, x) => Object.assign( + a, { + [x[labelKey]]: x[valueKey] + } + ), {} + ); + return ks.flatMap(k => { + const v = dct[k]; + return void 0 !== v ? ( + [v] + ) : []; + }); + }; + + // ------------------- PARSING LINKS ------------------- + + // mdLinkParse :: () -> + // Parser {title :: String, link :: String} + const mdLinkParse = () => + bindP( + char('[') + )(_ => bindP( + many(noneOf(']')) + )(title => bindP( + string('](') + )(_ => bindP( + many(noneOf(')')) + )(link => pureP({ + label: title.join(''), + link: link.join('') + }))))); + + // ------------ GENERIC PARSER COMBINATORS ------------- + + // Parser :: String -> [(a, String)] -> Parser a + const Parser = f => + // A function lifted into a Parser object. + ({ + type: 'Parser', + parser: f + }); + + + // altP (<|>) :: Parser a -> Parser a -> Parser a + const altP = p => + // p, or q if p doesn't match. + q => Parser(s => { + const xs = parse(p)(s); + return 0 < xs.length ? ( + xs + ) : parse(q)(s); + }); + + + // apP <*> :: Parser (a -> b) -> Parser a -> Parser b + const apP = pf => + // A new parser obtained by the application + // of a Parser-wrapped function, + // to a Parser-wrapped value. + p => Parser( + s => parse(pf)(s).flatMap( + vr => parse( + fmapP(vr[0])(p) + )(vr[1]) + ) + ); + + + // bindP (>>=) :: Parser a -> + // (a -> Parser b) -> Parser b + const bindP = p => + // A new parser obtained by the application of + // a function to a Parser-wrapped value. + // The function must enrich its output, lifting it + // into a new Parser. + // Allows for the nesting of parsers. + f => Parser( + s => parse(p)(s).flatMap( + tpl => parse(f(tpl[0]))(tpl[1]) + ) + ); + + + // char :: Char -> Parser Char + const char = x => + // A particular single character. + satisfy(c => x == c); + + + // fmapP :: (a -> b) -> Parser a -> Parser b + const fmapP = f => + // A new parser derived by the structure-preserving + // application of f to the value in p. + p => Parser( + s => parse(p)(s).flatMap( + vr => Tuple(f(vr[0]))(vr[1]) + ) + ); + + + // liftA2P :: (a -> b -> c) -> + // Parser a -> Parser b -> Parser c + const liftA2P = op => + // The binary function op, lifted + // to a function over two parsers. + p => apP(fmapP(op)(p)); + + + // many :: Parser a -> Parser [a] + const many = p => { + // Zero or more instances of p. + // Lifts a parser for a simple type of value + // to a parser for a list of such values. + const some_p = p => + liftA2P( + x => xs => [x].concat(xs) + )(p)(many(p)); + return Parser( + s => parse( + 0 < s.length ? ( + altP(some_p(p))(pureP([])) + ) : pureP([]) + )(s) + ); + }; + + // noneOf :: String -> Parser Char + const noneOf = s => + // Any character not found in the + // exclusion string. + satisfy(c => !s.includes(c)); + + + // parse :: Parser a -> String -> [(a, String)] + const parse = p => + // The result of parsing s with p. + s => { + // showLog('s', s) + return p.parser([...s]); + }; + + + // pureP :: a -> Parser a + const pureP = x => + // The value x lifted, unchanged, + // into the Parser monad. + Parser(s => [Tuple(x)(s)]); + + + // satisfy :: (Char -> Bool) -> Parser Char + const satisfy = test => + // Any character for which the + // given predicate returns true. + Parser( + s => 0 < s.length ? ( + test(s[0]) ? [ + Tuple(s[0])(s.slice(1)) + ] : [] + ) : [] + ); + + + // sequenceP :: [Parser a] -> Parser [a] + const sequenceP = ps => + // A single parser for a list of values, derived + // from a list of parsers for single values. + Parser( + s => ps.reduce( + (a, q) => a.flatMap( + vr => parse(q)(snd(vr)).flatMap( + first(xs => fst(vr).concat(xs)) + ) + ), + [Tuple([])(s)] + ) + ); + + + // string :: String -> Parser String + const string = s => + // A particular string. + fmapP(cs => cs.join(''))( + sequenceP([...s].map(char)) + ); + + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK', + withIcon: sa.pathToResource('TaskPaper.icns', { + inBundle: 'Applications/TaskPaper.app' + }) + }), + s + ); + }; + + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: 'Tuple', + '0': a, + '1': b, + length: 2 + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // comparing :: (a -> b) -> (a -> a -> Ordering) + const comparing = f => + x => y => { + const + a = f(x), + b = f(y); + return a < b ? -1 : (a > b ? 1 : 0); + }; + + // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c + const compose = (...fs) => + // A function defined by the right-to-left + // composition of all the functions in fs. + fs.reduce( + (f, g) => x => f(g(x)), + x => x + ); + + // doesFileExist :: FilePath -> IO Bool + const doesFileExist = fp => { + const ref = Ref(); + return $.NSFileManager.defaultManager + .fileExistsAtPathIsDirectory( + $(fp) + .stringByStandardizingPath, ref + ) && 1 !== ref[0]; + }; + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + + // filePath :: String -> FilePath + const filePath = s => + // The given file path with any tilde expanded + // to the full user directory path. + ObjC.unwrap(ObjC.wrap(s) + .stringByStandardizingPath); + + + // first :: (a -> b) -> ((a, c) -> (b, c)) + const first = f => + // A simple function lifted to one which applies + // to a tuple, transforming only its first item. + xy => Tuple(f(xy[0]))( + xy[1] + ); + + + // fst :: (a, b) -> a + const fst = tpl => + // First member of a pair. + tpl[0]; + + + // lines :: String -> [String] + const lines = s => + // A list of strings derived from a single + // newline-delimited string. + 0 < s.length ? ( + s.split(/[\r\n]/) + ) : []; + + + // list :: StringOrArrayLike b => b -> [a] + const list = xs => + // xs itself, if it is an Array, + // or an Array derived from xs. + Array.isArray(xs) ? ( + xs + ) : Array.from(xs || []); + + + // readFile :: FilePath -> IO String + const readFile = fp => { + // The contents of a text file at the + // path file fp. + const + e = $(), + ns = $.NSString + .stringWithContentsOfFileEncodingError( + $(fp).stringByStandardizingPath, + $.NSUTF8StringEncoding, + e + ); + return ObjC.unwrap( + ns.isNil() ? ( + e.localizedDescription + ) : ns + ); + }; + + // showLog :: a -> IO () + const showLog = (...args) => + console.log( + args + .map(JSON.stringify) + .join(' -> ') + ); + + + // showMenuLR :: Bool -> String -> [String] -> + // Either String [String] + const showMenuLR = blnMult => + title => xs => 0 < xs.length ? (() => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + sa.activate(); + const v = sa.chooseFromList(xs, { + withTitle: title, + withPrompt: 'Select' + ( + blnMult ? ( + ' one or more of ' + + xs.length.toString() + ) : ':' + ), + defaultItems: xs[0], + okButtonName: 'OK', + cancelButtonName: 'Cancel', + multipleSelectionsAllowed: blnMult, + emptySelectionAllowed: false + }); + return Array.isArray(v) ? ( + Right(v) + ) : Left('User cancelled ' + title + ' menu.'); + })() : Left(title + ': No items to choose from.'); + + + // snd :: (a, b) -> b + const snd = tpl => + // Second member of a pair. + tpl[1]; + + + // sortBy :: (a -> a -> Ordering) -> [a] -> [a] + const sortBy = f => + xs => list(xs).slice() + .sort((a, b) => f(a)(b)); + + + // strip :: String -> String + const strip = s => + s.trim(); + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + ActionName + Clear the linkListFilePath variable, in case similar macros use a different file. + ActionUID + 15997080 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + linkListFilePath + + + CreationDate + 620152106.71959996 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 659824624.20129597 + Name + Menu of MD links in File specified by path + Triggers + + UID + 7E7A7A04-BC05-48B9-9B29-FB0403C0BB42 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997086 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + md.obsidian + Name + Obsidian + NewFile + /Applications/Obsidian.app + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997087 + Conditions + + ConditionList + + + Application + + BundleIdentifier + md.obsidian + Name + Obsidian + NewFile + /Applications/Obsidian.app + + ConditionType + FrontWindow + FrontWindowConditionType + Exists + IsFrontApplication + + + + ConditionListMatch + All + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997088 + MacroActionType + ExecuteSubroutine + MacroUID + 3447E395-DDE1-4BBF-8CAE-AC5AEF4CC125 + Parameters + + %FrontWindowName% + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 630955437.86829698 + ModificationDate + 740984527.64106596 + Name + md.obsidian + Triggers + + UID + 58101A65-1DFB-4B4A-9AC6-9D25B494E33A + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997198 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_BundleId + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + // Either an alert message or MD links(s) for any + // OmniFocus 4 selections + // Placed in a Keyboard Maestro variable + // with the name 'mdLink' + const main = () => { + const + focus = Application(kmvar.local_BundleId), + doc = focus.documents.at(0); + + return either( + msg => ( + alert("MD Link")(msg), + "" + ) + )( + mdLink => mdLink + )( + bindLR( + doc.exists() + ? Right(doc.documentWindows.at(0)) + : Left("No OmniFocus documents found.") + )( + win => fmapLR( + selns => selns.map(markdownLink) + .join("\n") + )( + win.exists() + ? focusSelnsLR(win) + : Left("No document window found.") + ) + ) + ); + }; + + + // markdownLink :: Omnifocus Tree -> String + const markdownLink = ofTree => { + const + type = "folder" !== ( + ofTree.value().properties().pcls + ) + ? "task" + : "folder"; + + return `[${ofTree.name()}]` + ( + `(omnifocus:///${type}/${ofTree.id()})` + ); + }; + + + // focusSelnsLR :: Window -> Either String OFTrees + const focusSelnsLR = win => { + const + contentSelns = win.content.selectedTrees, + panel = 0 < contentSelns.length + ? contentSelns + : win.sidebar.selectedTrees; + + return 0 < panel.length + ? Right(panel()) + : Left("Nothing selected in OmniFocus."); + }; + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = lr => + // Bind operator for the Either option type. + // If lr has a Left value then lr unchanged, + // otherwise the function mf applied to the + // Right value in lr. + mf => "Left" in lr + ? lr + : mf(lr.Right); + + + // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c + const fmapLR = f => + // Either f mapped into the contents of any Right + // value in e, or e unchanged if is a Left value. + e => "Left" in e + ? e + : Right(f(e.Right)); + + // MAIN -- + return main(); +})(); + + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 740492054.76215601 + ModificationDate + 740492531.369488 + Name + OmniFocus variant + Triggers + + + MacroTriggerType + Subroutine + Parameters + + local_BundleId + + ReturnsValue + + + + UID + 9BB504DE-9FA1-423A-9B08-8F6075FA3C73 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997156 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + ActionName + From front application and selection to mdLink string value + ActionUID + 15997157 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // Rob Trew @ 2020 + + // Copy Markdown Link to front document, URL, or resource. + // Ver 0.30 + + // Switched to running app-specific macros by UUID + // fetched from a JSON dictionary stored in a + // uuidsForMDLink KM variable. + + // If this variable is not found, or a UUID retrieved + // from it is not found, then the dictionary is regenerated. + + // The regeneration, which will happen on the first + // run, but should only be needed thereafter when + // new sub-macros are added, will activate Keyboard Maestro.app + + // Normally use of the macro will, however, normally + // bypass Keyboard Maestro.app and run through + // Keyboard Maestro Engine instead. + + ObjC.import("AppKit"); + + const kmGroupName = "MD link tools"; + + // ---------------------- MAIN ----------------------- + // main :: IO () + // eslint-disable-next-line max-lines-per-function + const main = () => { + const bundleID = frontAppBundleId(); + + return either( + msg => ( + alert("Copy as Markdown link")(msg), + msg + ) + )( + mdLink => mdLink + )( + bindLR( + void 0 !== bundleID ? ( + Right(bundleID) + ) : Left( + "No active application detected" + ) + )(linkForBundleLR) + ); + }; + + // linkForBundleLR :: String -> Either String String + const linkForBundleLR = bundleID => + // ------------ BROWSER ? ------------ + + [ + "com.apple.Safari", + "com.google.Chrome", + "com.microsoft.edgemac", + "com.vivaldi.Vivaldi" + ].includes(bundleID) ? ( + browserLinkLR(bundleID) + ) : (() => { + // ---- APP-SPECIFIC MACRO ? ----- + const + kme = Application("Keyboard Maestro Engine"), + dctUUID = either( + msg => ( + // eslint-disable-next-line no-console + console.log( + "BundleID map had to be regenerated", + msg + ), + // Regenerated UUID dictionary + updatedUUIDMap() + ) + )( + // UUID dictionary from existing + // KM Variable + dct => dct + )( + jsonParseLR( + kme.getvariable("uuidsForMDLink") + ) + ); + + return linkFromUUID(kme)(bundleID)( + dctUUID[bundleID] + ); + })(); + + // linkFromUUID :: Application -> + // String -> String -> String + const linkFromUUID = kme => + bundleID => maybeUUID => Boolean(maybeUUID) ? ( + either( + // If the UUID wasn"t found, + // then run a new one from an + // updated dictionary. + () => ( + bindLR( + doScriptLR(kme)( + updatedUUIDMap()[bundleID] + ) + )( + // Link after use of alternate UUID + () => Right( + kme.getvariable("mdLink") + ) + ) + ) + )( + // Link read after with UUID + () => Right(kme.getvariable("mdLink")) + )( + // Run macro with this UUID if possible. + doScriptLR(kme)(maybeUUID) + ) + ) : appFrontWindowMDLinkLR(bundleID); + + + // doScriptLR :: UUID -> Either String String + const doScriptLR = kme => + uuid => { + try { + return ( + kme.doScript(uuid), + Right(uuid) + ); + } catch (e) { + return Left( + `Macro UUID :: ${uuid}\n\n${e.message}` + ); + } + }; + + // -------------- BUNDLEID -> UUID MAP --------------- + + // updatedUUIDMap :: IO () -> { bundleID :: UUID } + const updatedUUIDMap = () => { + const + macroGroupName = "MD Link tools", + mdLinkToolsGroups = Application( + "Keyboard Maestro" + ).macroGroups.where({ + name: macroGroupName + }); + + return either( + alert("Copy as MD Link - Map bundle to UUID") + )( + dictUUIDs => ( + Application("Keyboard Maestro Engine") + .setvariable("uuidsForMDLink", { + to: JSON.stringify( + dictUUIDs, null, 2 + ) + }), + dictUUIDs + ) + )( + 0 < mdLinkToolsGroups.length ? (() => { + const + instances = mdLinkToolsGroups.at(0) + .macros() + .flatMap(macro => { + const k = macro.name(); + + return k.includes(".") ? ( + [ + [k, macro.id()] + ] + ) : []; + }); + + return Right( + instances.reduce( + (a, [bundle, uuid]) => Object.assign( + a, { + [bundle]: uuid + } + ), {} + ) + ); + })() : Left( + `Macro group not found:\n\n\t${macroGroupName}` + ) + ); + }; + + + // --------------------- BROWSERS ---------------------- + + // browserLinkLR :: String -> Either String IO String + const browserLinkLR = bundleID => { + const + app = Application(bundleID), + ws = app.windows; + + return bindLR( + 0 < ws.length ? ( + Right(ws.at(0)) + ) : Left(`No windows open in ${bundleID}`) + )( + w => { + const tabs = w.tabs; + + return 0 < tabs.length ? (() => { + const + tab = w[ + "com.apple.Safari" === bundleID ? ( + "currentTab" + ) : "activeTab" + ](); + + return Right( + `[${tab.name()}](${tab.url()})` + ); + })() : Left( + `No open tabs in front window of ${bundleID}` + ); + } + ); + }; + + // ----------------------- JXA ----------------------- + + // frontAppBundleId :: () -> String + const frontAppBundleId = () => { + const uw = ObjC.unwrap; + + return uw(uw( + $.NSWorkspace.sharedWorkspace.activeApplication + ).NSApplicationBundleIdentifier); + }; + + // ------- DEFAULT - DOCUMENT OF FRONT WINDOW -------- + + // appFrontWindowMDLinkLR :: String -> Either String String + const appFrontWindowMDLinkLR = bundleID => { + const + procs = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }) + .applicationProcesses.where({ + bundleIdentifier: bundleID + }); + + return bindLR( + bindLR( + procs.length > 0 ? ( + Right(procs.at(0).windows) + ) : Left(`Application not found: ${bundleID}`) + )(ws => ws.length > 0 ? ( + Right(ws.at(0)) + ) : Left(`No windows found for ${bundleID}`)) + )(w => { + const + uw = ObjC.unwrap, + [winTitle, maybeDocURL] = [ + "AXTitle", "AXDocument" + ] + .map(appID => uw( + w.attributes.byName(appID).value() + )); + + return Boolean(maybeDocURL) ? ( + Right(`[${winTitle}](${maybeDocURL})`) + ) : Left( + [ + `Window "${winTitle}" of:\n\n\t${bundleID}`, + "\nmay not be a document window.", + `\nConsider adding a macro named "${bundleID}"`, + `to the KM Group "${kmGroupName}".`, + "\n(Or request such a macro, which should", + "save a [label](url) string) in the", + "KM variable \"mdLink\")", + "on the Keyboard Maestro forum)." + ].join("\n") + ); + }); + }; + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Either" === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + + // jsonParseLR :: String -> Either String a + const jsonParseLR = s => { + // Either a message, or a JS value obtained + // from a successful parse of s. + try { + return Right(JSON.parse(s)); + } catch (e) { + return Left( + `${e.message} (line:${e.line} col:${e.column})` + ); + } + }; + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + ActionName + Copy to clipboard as OrgMode link + ActionUID + 15997158 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // Rob Trew @2021 + // Ver 0.2 + + // One or more lines of Orgmode [[link][label]] + // derived from KM mdLink variable + // and placed in clipboard. + + ObjC.import("AppKit"); + + // main :: IO () + const main = () => { + const + kme = Application("Keyboard Maestro Engine"), + kvs = mdLinkPartPairs( + kme.getvariable("mdLink") + ), + orgLinks = kvs.map( + kv => `[[${kv[1]}][${kv[0]}]]` + ) + .join("\n"); + + return ( + kme.setvariable("orgLinks", { + to: orgLinks + }), + copyTypedString(true)( + "public.utf8-plain-text" + )( + orgLinks + ) + ); + }; + + + // ---------------------- LINKS ---------------------- + + // copyTypedString :: Bool -> String -> String -> IO () + const copyTypedString = blnClear => + // public.html, public.rtf, public.utf8-plain-text + pbType => s => { + const pb = $.NSPasteboard.generalPasteboard; + + return ( + blnClear && pb.clearContents, + pb.setStringForType( + $(s), + $(pbType) + ), + s + ); + }; + + + // mdLinkPartPairs :: String -> [(String, String)] + const mdLinkPartPairs = s => + lines(s).map(x => { + const ab = x.trim().split("]("); + + return 2 !== ab.length ? ( + Tuple(s)("") + ) : Tuple(ab[0].slice(1))( + ab[1].slice(0, -1) + ); + }); + + // --------------------- GENERIC --------------------- + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2 + }); + + + // lines :: String -> [String] + const lines = s => + // A list of strings derived from a single + // string delimited by newline and or CR. + 0 < s.length ? ( + s.split(/[\r\n]+/u) + ) : []; + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + ActionName + Notification + ActionUID + 15997159 + Conditions + + ConditionList + + + ConditionType + Variable + Variable + orgLink + VariableConditionType + IsNot + VariableValue + + + + ConditionListMatch + All + + ElseActions + + + ActionUID + 15997161 + MacroActionType + Notification + SoundName + Glass + Subtitle + Nothing copied + Text + %Application%1% + Title + %ExecutingMacro% + + + MacroActionType + IfThenElse + ThenActions + + + ActionUID + 15997160 + MacroActionType + Notification + SoundName + Glass + Subtitle + + Text + %Variable%orgLinks% + Title + %ExecutingMacro% + + + TimeOutAbortsMacro + + + + CreationDate + 654681523.18336499 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 738248394.17644501 + Name + Copy as OrgMode link + Triggers + + UID + EA0FF29C-B381-4DF3-A254-7A10B7767067 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997091 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997092 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + Action + Copy + ActionUID + 15997093 + IsDisclosed + + MacroActionType + CutCopyPaste + TimeOutAbortsMacro + + + + ActionUID + 15997094 + DisplayKind + Window + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Copy selected Ulysses item as MD Link + + // main :: IO () + const main = () => { + const + kme = Application('Keyboard Maestro Engine'), + itemName = ( + kme.doScript(windowNamePlist), + kme.getvariable('windowName') + ); + + return either( + alert('Copy as MD Link') + )( + s => ( + kme.setvariable('mdLink', { + to: s + }), + s + ) + )( + bindLR( + clipTextLR() + )( + url => Right(`[${itemName}](${url})`) + ) + ); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + // ---------------- KEYBOARD MAESTRO ----------------- + + // windowNamePlist :: XML String + const windowNamePlist = `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>MacroActionType</key> + <string>SetVariableToText</string> + <key>Text</key> + <string>%FrontWindowName%</string> + <key>Variable</key> + <string>windowName</string> + </dict> + </array> + </plist>` + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 631051801.47009397 + ModificationDate + 740397212.23399603 + Name + com.ulyssesapp.mac + Triggers + + UID + 0AF05301-0AA9-486B-8668-CE93CBE4C88E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997062 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // com.panic.Transmit + // vs App Store build: + // com.panic.transmit.mas + + // main :: IO () + const main = () => { + const + transmit = Application('Transmit'), + ds = transmit.documents; + return either( + // Notification + msg => msg + )( + link => ( + Application('Keyboard Maestro Engine') + .setvariable('mdLink', { + to: link + }), + link + ) + )(bindLR( + 0 < ds.length ? ( + Right(ds.at(0)) + ) : Left('No documents open in Transmit') + )(doc => { + const tabs = doc.tabs; + return bindLR( + 0 < tabs.length ? ( + Right(doc.currentTab()) + ) : Left( + 'No tabs found for document :: doc.name()' + ) + )(tab => { + const fbs = tab.fileBrowsers; + return bindLR( + 0 < fbs.length ? ( + Right(fbs.at(0)) + ) : Left( + 'No file browsers found for current tab.' + ) + )(fb => { + const selns = fb.selectedBrowserItems; + return bindLR( + 0 < selns.length ? ( + Right(selns.at(0)) + ) : Left('Nothing selected in front tab') + )(x => Right( + `[${x.name()}]` + ( + `(${encodeURI('file://' + x.path())})` + ) + )); + }); + }); + })); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 628543401.21383905 + ModificationDate + 740902490.45909297 + Name + com.panic.Transmit + Triggers + + UID + 3E5A8E68-8572-4C16-B8B6-E1CC2AC1B434 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997162 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + const + track = Application('com.spotify.client') + .currentTrack(), + idTrack = track.id().split(':')[2]; + + return `[${track.name()}](spotify://track/${idTrack})`; +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620241764.56565905 + ModificationDate + 677774302.29004395 + Name + com.spotify.client + Triggers + + UID + 9FCDF844-4057-473F-A611-862549F5BA8E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997212 + MacroActionType + ExecuteSubroutine + MacroUID + F18CB180-7FD8-4256-81B1-47F55B93A93E + Parameters + + com.sonnysoftware.bookends2 + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 654686941.21547604 + ModificationDate + 740903620.91090202 + Name + com.sonnysoftware.bookends2 + Triggers + + UID + 7A1AAF8A-2071-4968-B02D-C74B39F6BBCB + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997184 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997185 + MacroActionType + SelectMenuItem + Menu + + Edit + Copy Day as Text + + TargetApplication + + BundleIdentifier + com.flexibits.fantastical2.mac + Name + Fantastical + + TargetingType + Specific + + + ActionUID + 15997186 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either( + alert('MD links from Fantastical') + )( + x => x + )( + bindLR( + clipOfTypeLR('public.utf8-plain-text') + )( + clip => { + const + dateString = clip.split('\n')[0], + iso8601 = taskPaperDateString( + new Date(dateString) + ).slice(0, 10), + pfx = 'x-fantastical3://show/calendar/'; + return Right( + `[${dateString}](${pfx + iso8601})` + ); + } + ) + ); + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // ---------------------- GENERAL ---------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // iso8601Local :: Date -> String + const iso8601Local = dte => + new Date(dte - (6E4 * dte.getTimezoneOffset())) + .toISOString(); + + // taskPaperDateString :: Date -> String + const taskPaperDateString = dte => { + const [d, t] = iso8601Local(new Date()).split('T'); + return [d, t.slice(0, 5)].join(' '); + }; + + // main + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620254453.41475594 + ModificationDate + 655300969.18022597 + Name + com.flexibits.fantastical2.mac + Triggers + + UID + EE56E9F3-24F7-4AC1-A7E1-72A36D88833F + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997095 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.toketaware.ithoughtsx + Name + iThoughtsX + NewFile + /Applications/iThoughtsX.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionName + Link to any selected node or otherwise to document + ActionUID + 15997096 + Actions + + + ActionUID + 15997097 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + ActionUID + 15997098 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997099 + Conditions + + ConditionList + + + ConditionType + Menu + MenuConditionSelectionType + Path + MenuConditionType + IsEnabled + MenuModifiers + 256 + MenuShortcut + C + MenuTitle + Edit > Copy as > Link + + + ConditionListMatch + All + + ElseActions + + + ActionName + Link to document + ActionUID + 15997111 + Actions + + + ActionName + MD Link for iThoughtsX document, with no particular selection + ActionUID + 15997112 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + IsDisclosed + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + // MD Link for iThoughtsX Document with + // no specific selection. + + const main = () => ( + Application('iThoughtsX').activate(), + either( + alert('MD Link') + )( + md => ( + Application('Keyboard Maestro Engine') + .setvariable('mdLink', { + to: md + }), + md + ) + )( + appFrontWindowMDLinkLR( + 'com.toketaware.ithoughtsx' + ) + ) + ); + + // ---------------------- LINKS ---------------------- + + // appFrontWindowMDLinkLR :: String -> Either String String + const appFrontWindowMDLinkLR = bundleID => { + const + procs = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }) + .applicationProcesses.where({ + bundleIdentifier: bundleID + }); + return bindLR( + bindLR( + procs.length > 0 ? ( + Right(procs.at(0).windows) + ) : Left('Application not found: ' + bundleID) + )(ws => ws.length > 0 ? ( + Right(ws.at(0)) + ) : Left(`No windows found for ${bundleID}`)) + )(w => { + const + uw = ObjC.unwrap, + [winTitle, maybeDocURL] = map( + k => uw(w.attributes.byName(k).value()) + )(['AXTitle', 'AXDocument']) + return Boolean(maybeDocURL) ? ( + Right(`[${winTitle}](${maybeDocURL})`) + ) : Left( + `Window '${winTitle}' of:\n\n\t${bundleID}` + [ + '\n\nmay not be a document window.', + `\nConsider adding a macro named '${bundleID}'`, + `to the KM Group '${kmGroupName}'.`, + '\n(Or request such a macro, which should', + 'save a [label](url) string) in the', + 'KM variable "mdLink")', + `on the Keyboard Maestro forum).` + ].join('\n') + ) + }); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // map :: (a -> b) -> [a] -> [b] + const map = f => + // The list obtained by applying f + // to each element of xs. + // (The image of xs under f). + xs => [...xs].map(f); + + return main() +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + IsDisclosed + + MacroActionType + Group + TimeOutAbortsMacro + + + + MacroActionType + IfThenElse + ThenActions + + + ActionName + Link to selection + ActionUID + 15997100 + Actions + + + ActionUID + 15997101 + MacroActionType + SelectMenuItem + Menu + + Edit + Copy as + Link + + TargetApplication + + BundleIdentifier + com.toketaware.ithoughtsx + Name + iThoughtsX + NewFile + /Applications/iThoughtsX.app + + TargetingType + Specific + + + ActionUID + 15997102 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997103 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + mdURL + + + ActionUID + 15997104 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997105 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Edit + Copy + + TargetApplication + + BundleIdentifier + com.toketaware.ithoughtsx + Name + iThoughtsX + NewFile + /Applications/iThoughtsX.app + + TargetingType + Specific + + + ActionUID + 15997106 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionName + Execute JavaScript For Automation :: Name of selected node. + ActionUID + 15997107 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + IsDisclosed + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either( + msg => msg + )( + x => { + const xs = lines(x); + return 0 < xs.length ? ( + xs[0] + ) : x; + } + )(clipTextLR()) + + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // lines :: String -> [String] + const lines = s => + // A list of strings derived from a single + // string delimited by newline and or CR. + 0 < s.length ? ( + s.split(/[\r\n]+/) + ) : []; + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdName + + + ActionUID + 15997108 + MacroActionType + SetVariableToText + Text + [%Variable%mdName%](%Variable%mdURL%) + Variable + mdLink + + + ActionUID + 15997109 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdName + + + ActionUID + 15997110 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdURL + + + IsDisclosed + + MacroActionType + Group + TimeOutAbortsMacro + + + + TimeOutAbortsMacro + + + + MacroActionType + Group + TimeOutAbortsMacro + + + + CreationDate + 628791216.25947201 + ModificationDate + 634818668.12104201 + Name + com.toketaware.ithoughtsx + Triggers + + UID + 3B594ECB-BBFE-401D-82C1-EDBE702EA5C9 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997129 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997130 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Note + Copy Note Link + + TargetApplication + + BundleIdentifier + com.happenapps.Quiver + Name + Quiver + NewFile + /Applications/Quiver.app + + TargetingType + Specific + + + ActionUID + 15997131 + Conditions + + ConditionList + + + ClipboardConditionType + HasText + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997132 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit') + + // main :: IO () + const main = () => + either(alert('Quiver'))(x => x)( + bindLR( + clipOfTypeLR('public.html') + )(s => Right( + `[${s.slice(63, -4)}](${s.slice(9, 61)})` + )) + ); + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // ---------------------- GENERAL ---------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620228833.87620103 + ModificationDate + 634818474.07095397 + Name + com.happenapps.Quiver + Triggers + + UID + 4B0EB65D-144D-4268-A580-637E644E7CFF + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997071 + MacroActionType + ExecuteSubroutine + MacroUID + F18CB180-7FD8-4256-81B1-47F55B93A93E + Parameters + + com.sonnysoftware.bookends + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 622163020.078107 + ModificationDate + 740903637.12486506 + Name + com.sonnysoftware.bookends + Triggers + + UID + A803ADEF-527C-4D43-A1E5-07941E2736BC + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997137 + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC4ZAgAAKwAAAAEA + AAARAgAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNjcx + XGNvY29hc3VicnRmNjAwCntcZm9u + dHRibFxmMFxmc3dpc3NcZmNoYXJz + ZXQwIEhlbHZldGljYTt9CntcY29s + b3J0Ymw7XHJlZDI1NVxncmVlbjI1 + NVxibHVlMjU1O1xyZWQwXGdyZWVu + MFxibHVlMDt9CntcKlxleHBhbmRl + ZGNvbG9ydGJsOztcY3NzcmdiXGMw + XGMwXGMwXGM4NDcwNlxjbmFtZSBj + b250cm9sVGV4dENvbG9yO30KXHBh + cmRcdHg1NjBcdHgxMTIwXHR4MTY4 + MFx0eDIyNDBcdHgyODAwXHR4MzM2 + MFx0eDM5MjBcdHg0NDgwXHR4NTA0 + MFx0eDU2MDBcdHg2MTYwXHR4Njcy + MFxwYXJkaXJuYXR1cmFsXHBhcnRp + Z2h0ZW5mYWN0b3IwCgpcZjBcZnMz + MFxmc21pbGxpMTUzNjQgXGNmMiBW + ZXIgMC4wMSBSb2IgVHJldyAyMDIw + XApcCklmIHRoZSBpdGVtIGlzIGN1 + cnJlbnRseSBiZWluZyBlZGl0ZWQs + IGZvY3VzIGJyaWVmbHkgc3dpdGNo + ZXMgdG8gdGhlIGVuY2xvc2luZyBn + cm91cCAodG8gZW5hYmxlIGxpbmsg + Y29weWluZykgYW5kIGlzIHRoZW4g + cmVzdG9yZWQufQEAAAAjAAAAAQAA + AAcAAABUWFQucnRmEAAAACAWRF62 + AQAAAAAAAAAAAAA= + + Title + Copy MD [Name](URL) link to current item. + + + ActionUID + 15997138 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.literatureandlatte.scrivener3 + Name + Scrivener + NewFile + /Applications/Scrivener.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997139 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + ENCLOSING GROUP + + + ActionName + Temporarily get enclosing group context, if necessary. + ActionUID + 15997140 + Conditions + + ConditionList + + + ConditionType + Menu + MenuConditionSelectionType + Path + MenuConditionType + IsEnabled + MenuModifiers + 256 + MenuShortcut + C + MenuTitle + Navigate > Go To > Enclosing Group + + + ConditionListMatch + All + + ElseActions + + + ActionUID + 15997143 + IsDisclosed + + MacroActionType + SetVariableToText + Text + false + Variable + scrivenerContextChanged + + + IsDisclosed + + MacroActionType + IfThenElse + ThenActions + + + ActionUID + 15997141 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Navigate + Go To + Enclosing Group + + TargetApplication + + BundleIdentifier + com.literatureandlatte.scrivener3 + Name + Scrivener + NewFile + /Applications/Scrivener.app + + TargetingType + Specific + + + ActionUID + 15997142 + IsDisclosed + + MacroActionType + SetVariableToText + Text + true + Variable + scrivenerContextChanged + + + TimeOutAbortsMacro + + + + ActionUID + 15997144 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + Copy NAME + + + Action + Copy + ActionUID + 15997145 + IsDisclosed + + MacroActionType + CutCopyPaste + TimeOutAbortsMacro + + + + ActionUID + 15997146 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + scrivenerItemName + + + ActionUID + 15997147 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + Copy URL + + + ActionUID + 15997148 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Edit + Copy Special + Copy Document as External Link + + TargetApplication + + BundleIdentifier + com.literatureandlatte.scrivener3 + Name + Scrivener + NewFile + /Applications/Scrivener.app + + TargetingType + Specific + + + ActionName + Pause until Clipboard contains "x-scrivener-item:" + ActionUID + 15997149 + Conditions + + ConditionList + + + ClipboardConditionType + Contains + ClipboardText + x-scrivener-item: + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997150 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + MARKDOWN link copied to mdLink variable, context restored, user notified + + + ActionUID + 15997151 + Conditions + + ConditionList + + + ConditionType + Variable + Variable + scrivenerContextChanged + VariableConditionType + Is + VariableValue + true + + + ConditionListMatch + All + + ElseActions + + IsDisclosed + + MacroActionType + IfThenElse + ThenActions + + + ActionUID + 15997152 + MacroActionType + SelectMenuItem + Menu + + Navigate + Editor + Backward in Document History + + TargetApplication + + BundleIdentifier + com.literatureandlatte.scrivener3 + Name + Scrivener + NewFile + /Applications/Scrivener.app + + TargetingType + Specific + + + TimeOutAbortsMacro + + + + ActionUID + 15997153 + IsDisclosed + + MacroActionType + SetVariableToText + Text + [%Variable%scrivenerItemName%](%SystemClipboard%) + Variable + mdLink + + + CreationDate + 631093054.48296905 + ModificationDate + 634818482.71016705 + Name + com.literatureandlatte.scrivener3 + Triggers + + UID + 31A5563F-D22F-4D23-BB02-1280047D673E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997167 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997168 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + Action + Copy + ActionUID + 15997169 + IsDisclosed + + MacroActionType + CutCopyPaste + TimeOutAbortsMacro + + + + ActionUID + 15997170 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Copy selected Ulysses item as MD Link + + // main :: IO () + const main = () => { + const + kme = Application('Keyboard Maestro Engine'), + itemName = ( + kme.doScript(windowNamePlist), + kme.getvariable('windowName') + ); + + return either( + alert('Copy as MD Link') + )( + s => ( + kme.setvariable('mdLink', { + to: s + }), + s + ) + )( + bindLR( + clipTextLR() + )( + url => Right(`[${itemName}](${url})`) + ) + ); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + // ---------------- KEYBOARD MAESTRO ----------------- + + // windowNamePlist :: XML String + const windowNamePlist = `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>MacroActionType</key> + <string>SetVariableToText</string> + <key>Text</key> + <string>%FrontWindowName%</string> + <key>Variable</key> + <string>windowName</string> + </dict> + </array> + </plist>` + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 631055636.30216706 + ModificationDate + 634818658.02646804 + Name + com.soulmen.ulysses-setapp + Triggers + + UID + CEFD7F24-51D6-4324-BD91-E6D0CF33E3AE + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997051 + IsDisclosed + + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC5uAAAAKwAAAAEA + AABmAAAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYxNDA0 + XGNvY29hc3VicnRmNDcwCntcZm9u + dHRibH0Ke1xjb2xvcnRibDtccmVk + MjU1XGdyZWVuMjU1XGJsdWUyNTU7 + fQp9AQAAACMAAAABAAAABwAAAFRY + VC5ydGYQAAAAd3KjV7YBAAAAAAAA + AAAAAA== + + Title + Tested for Soulver 3 + + + ActionUID + 15997052 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997053 + MacroActionType + SelectMenuItem + Menu + + Sheet + Copy Link to Sheet + + TargetApplication + + BundleIdentifier + app.soulver.appstore.mac + Name + Soulver 3 + NewFile + /Applications/Soulver 3.app + + TargetingType + Specific + + + ActionName + Pause until text clipboard not empty + ActionUID + 15997054 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997055 + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + local_URL + + + ActionUID + 15997056 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_URL + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + const + bundleID = 'app.soulver.appstore.mac', + se = ( + Application(bundleID).activate(), + Application('System Events') + ), + procs = se.applicationProcesses.where({ + name: 'Soulver 3' + }), + sheetName = 0 < procs.length ? ( + procs.at(0).windows.at(0).name() + ) : 'Soulver 3 sheet name not found.'; + +return `[${sheetName}](${kmvar.local_URL})`; + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 631732621.41446495 + ModificationDate + 740416198.28848004 + Name + app.soulver.appstore.mac + Triggers + + UID + 46153BBD-185B-43DE-A121-9ADB2E0124DD + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997059 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + const + kme = Application('Keyboard Maestro'), + selnMacros = kme.selectedMacros(); + + return ( + 0 < selnMacros.length ? ( + selnMacros + ) : kme.selectedMacroGroups() + ).map( + x => `[${x.name()}]` + ( + `(keyboardmaestro://m=${x.id()})` + ) + ).join('\n'); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 628990819.70212102 + ModificationDate + 634818665.15527105 + Name + com.stairways.keyboardmaestro.editor + Triggers + + UID + DEAB4CB2-E0A5-435A-A6A9-9466FDF48DBE + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997154 + MacroActionType + SelectMenuItem + Menu + + Editor + Show Path Bar + + NotifyOnFailure + + StopOnFailure + + TargetApplication + + BundleIdentifier + com.panic.Nova + Name + Nova + NewFile + /Applications/Nova.app + + TargetingType + Specific + + + ActionUID + 15997155 + DisplayKind + Briefly + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(d){return d.raw=d};$jscomp.createTemplateTagFirstArgWithRaw=function(d,e){d.raw=e;return d}; +(function(){var d=function(a){return function(b){var c=Object.assign(Application("System Events"),{includeStandardAdditions:!0});return c.activate(),c.displayDialog(b,{withTitle:a,buttons:["OK"],defaultButton:"OK"}),b}},e=function(a){return{type:"Either",Left:a}},g=function(a){return{type:"Either",Right:a}},h=function(a){return function(b){return void 0!==a.Left?a:b(a.Right)}},k=function(a){return function(b){return function(c){return"Either"===c.type?void 0!==c.Left?a(c.Left):b(c.Right):void 0}}}, +l=function(a){a=Array.isArray(a)?a:Array.from(a||[]);return 0<a.length?a.slice(0,-1):void 0};return function(){var a=Object.assign(Application("System Events"),{includeStandardAdditions:!0}).applicationProcesses.where({name:"Nova"});return k(d("Copy as MD link"))(function(b){return Application("Keyboard Maestro Engine").setvariable("mdLink",{to:b}),b})(h(0<a.length?g(a.at(0).windows):e("Nova not running."))(function(b){return h(0<b.length?g(b.at(0).splitterGroups.at(0).splitterGroups.at(0).splitterGroups.at(0).groups.at(0)): +e("No windows open in Nova."))(function(c){var m=c.lists;return 0<c.lists.length?h(g(m.at(0)))(function(f){f=l(f.staticTexts().map(function(n){return n.name()}));var p=encodeURI("file:///Users/"+f.join("/"));return g("["+(0<f.length?f.slice(-1)[0]:void 0)+"]("+p+")")}):e("Editor > Show Path Bar not chosen.")})}))}()})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 633698454.90628695 + ModificationDate + 633700281.50065506 + Name + com.panic.Nova + Triggers + + UID + DB2BE1D0-7FBD-40F3-9984-2FA4ACBDB60D + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997076 + MacroActionType + ExecuteSubroutine + MacroUID + 9815DA78-703E-424C-9025-2D182032A61F + Parameters + + com.omnigroup.OmniPlan3 + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 629071910.84008896 + ModificationDate + 740911553.70155895 + Name + com.omnigroup.OmniPlan3 + Triggers + + UID + 8DCFA088-7CEE-4D21-8CA1-55A41D558EBE + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997026 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // com.panic.Transmit + // vs App Store build: + // com.panic.transmit.mas + + // main :: IO () + const main = () => { + const + transmit = Application('Transmit'), + ds = transmit.documents; + return either( + // Notification + msg => msg + )( + link => ( + Application('Keyboard Maestro Engine') + .setvariable('mdLink', { + to: link + }), + link + ) + )(bindLR( + 0 < ds.length ? ( + Right(ds.at(0)) + ) : Left('No documents open in Transmit') + )(doc => { + const tabs = doc.tabs; + return bindLR( + 0 < tabs.length ? ( + Right(doc.currentTab()) + ) : Left( + 'No tabs found for document :: doc.name()' + ) + )(tab => { + const fbs = tab.fileBrowsers; + return bindLR( + 0 < fbs.length ? ( + Right(fbs.at(0)) + ) : Left( + 'No file browsers found for current tab.' + ) + )(fb => { + const selns = fb.selectedBrowserItems; + return bindLR( + 0 < selns.length ? ( + Right(selns.at(0)) + ) : Left('Nothing selected in front tab') + )(x => Right( + `[${x.name()}]` + ( + `(${encodeURI('file://' + x.path())})` + ) + )); + }); + }); + })); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 628771538.92972803 + ModificationDate + 634818528.60760605 + Name + com.panic.transmit.mas + Triggers + + UID + 5B08951F-F2AC-44C2-8EF8-1E15A4C760E3 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997113 + MacroActionType + ExecuteSubroutine + MacroUID + 9815DA78-703E-424C-9025-2D182032A61F + Parameters + + com.omnigroup.OmniPlan4 + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 629071556.148844 + ModificationDate + 740911536.30203795 + Name + com.omnigroup.OmniPlan4 + Triggers + + UID + 9509A648-A07B-40F2-8525-4DB6E2BACBDF + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997115 + MacroActionType + ExecuteSubroutine + MacroUID + 9BB504DE-9FA1-423A-9B08-8F6075FA3C73 + Parameters + + com.omnigroup.OmniFocus3.MacAppStore + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 628975106.58552098 + ModificationDate + 740492460.38368797 + Name + com.omnigroup.OmniFocus3.MacAppStore + Triggers + + UID + F874CA21-D9FA-4569-B36E-CB91E57C4021 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997060 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => { + const + documents = Application( + 'com.multimarkdown.nvUltra' + ).documents, + kme = Application('Keyboard Maestro Engine'), + setMDLink = s => ( + kme.setvariable('mdLink', { + to: s + }), + s + ); + return ( + // In Keyboard Maestro, then: + setMDLink(''), + either( + // in a user dialog, + alert('Copy as Markdown Link') + )( + // or in Keyboard Maestro. + setMDLink + )( + bindLR( + 0 < documents.length ? ( + Right(documents.at(0)) + ) : Left( + 'No front document found in nvUltra.' + ) + )(doc => { + const fp = str(Path(doc.note())); + return fp.endsWith('null') ? ( + Left(`Selection is not a text file.`) + ) : Right( + `[${takeBaseName(fp)}]` + ( + `(${doc.noteLink()})` + ) + ); + }) + ) + ); + }; + + + // ---------------------- MACOS ---------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + + // str :: a -> String + const str = x => + Array.isArray(x) && x.every( + v => ('string' === typeof v) && (1 === v.length) + ) ? ( + x.join('') + ) : x.toString(); + + + // takeBaseName :: FilePath -> String + const takeBaseName = strPath => + ('' !== strPath) ? ( + ('/' !== strPath[strPath.length - 1]) ? (() => { + const fn = strPath.split('/').slice(-1)[0]; + return fn.includes('.') ? ( + fn.split('.').slice(0, -1).join('.') + ) : fn; + })() : '' + ) : ''; + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 631585981.14060795 + ModificationDate + 634818492.34164703 + Name + com.multimarkdown.nvUltra + Triggers + + UID + CBF5AB2E-6817-4556-8658-C5B206F6132B + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997014 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997015 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.OakTree.Accordance + Name + Accordance + NewFile + /Applications/Accordance.app + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997016 + MacroActionType + SelectMenuItem + Menu + + Edit + Copy As + Location URL + + TargetApplication + + BundleIdentifier + com.OakTree.Accordance + Name + Accordance + NewFile + /Applications/Accordance.app + + TargetingType + Specific + + + ActionUID + 15997017 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997018 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Rob Trew @2020 + + // main :: IO () + const main = () => + either( + msg => msg + )( + x => x + )( + bindLR( + clipTextLR() + )(s => { + const xs = parse(accordLinkP())(s); + return 0 < xs.length ? ( + Right(xs[0][0]) + ) : Left( + '[No Accordance link found in clipboard.]()' + ); + }) + ); + + // ---------------------- PARSING ---------------------- + + // accordLinkP :: Parser String + const accordLinkP = () => + // Internal link for Accordance.app parsed + // from Edit > Copy As > Location URL + // clipboard string. + thenBindP( + manyTill( + anyChar() + )( + string('/read/') + ) + )( + takeWhileP(ne('#')) + )(book => thenBindP(char('#'))( + fmapP(concat)(some(anyChar())) + )(ref => pureP( + `[${book} ${ref.replace(/_/, ' ')}]` + ( + `(accord://read/${book}#${ref})` + ) + ))); + + // ---------------- PARSER COMBINATORS ----------------- + + // Parser :: String -> [(a, String)] -> Parser a + const Parser = f => + // A function lifted into a Parser object. + ({ + type: 'Parser', + parser: f + }); + + + // altP (<|>) :: Parser a -> Parser a -> Parser a + const altP = p => + // p, or q if p doesn't match. + q => Parser(s => { + const xs = parse(p)(s); + return 0 < xs.length ? ( + xs + ) : parse(q)(s); + }); + + + // anyChar :: () -> Parser Char + const anyChar = () => + // A single character. + Parser( + s => 0 < s.length ? [ + Tuple(s[0])( + s.slice(1) + ) + ] : [] + ); + + + // apP <*> :: Parser (a -> b) -> Parser a -> Parser b + const apP = pf => + // A new parser obtained by the application + // of a Parser-wrapped function, + // to a Parser-wrapped value. + p => Parser( + s => parse(pf)(s).flatMap( + vr => parse( + fmapP(vr[0])(p) + )(vr[1]) + ) + ); + + + // bindP (>>=) :: Parser a -> + // (a -> Parser b) -> Parser b + const bindP = p => + // A new parser obtained by the application of + // a function to a Parser-wrapped value. + // The function must enrich its output, lifting it + // into a new Parser. + // Allows for the nesting of parsers. + f => Parser( + s => parse(p)(s).flatMap( + tpl => parse(f(tpl[0]))(tpl[1]) + ) + ); + + + // char :: Char -> Parser Char + const char = x => + // A particular single character. + satisfy(c => x == c); + + + // fmapP :: (a -> b) -> Parser a -> Parser b + const fmapP = f => + // A new parser derived by the structure-preserving + // application of f to the value in p. + p => Parser( + s => parse(p)(s).flatMap( + first(f) + ) + ); + + + // liftA2P :: (a -> b -> c) -> + // Parser a -> Parser b -> Parser c + const liftA2P = op => + // The binary function op, lifted + // to a function over two parsers. + p => apP(fmapP(op)(p)); + + + // manyTill :: Parser a -> Parser e -> Parser [a] + const manyTill = p => + // All of the matches for p before e matches. + // Wrapping e in lookAhead can preserve any + // string which matches e, if it is needed. + e => { + const + scan = () => altP( + thenP(e)(pureP([])) + )( + bindP( + p + )(x => bindP( + go + )(xs => pureP( + [x].concat(xs) + ))) + ), + go = scan(); + return go; + }; + + + // parse :: Parser a -> String -> [(a, String)] + const parse = p => + // The result of parsing s with p. + s => { + // showLog('s', s) + return p.parser([...s]); + }; + + + // pureP :: a -> Parser a + const pureP = x => + // The value x lifted, unchanged, + // into the Parser monad. + Parser(s => [Tuple(x)(s)]); + + + // satisfy :: (Char -> Bool) -> Parser Char + const satisfy = test => + // Any character for which the + // given predicate returns true. + Parser( + s => 0 < s.length ? ( + test(s[0]) ? [ + Tuple(s[0])(s.slice(1)) + ] : [] + ) : [] + ); + + + // sequenceP :: [Parser a] -> Parser [a] + const sequenceP = ps => + // A single parser for a list of values, derived + // from a list of parsers for single values. + Parser( + s => ps.reduce( + (a, q) => a.flatMap( + vr => parse(q)(snd(vr)).flatMap( + first(xs => fst(vr).concat(xs)) + ) + ), + [Tuple([])(s)] + ) + ); + + + // some :: Parser a -> Parser [a] + const some = p => { + // One or more instances of p. + // Lifts a parser for a simple type of value + // to a parser for a list of such values. + const many_p = p => + altP(some(p))(pureP([])); + return Parser( + s => parse( + liftA2P( + x => xs => [x].concat(xs) + )(p)(many_p(p)) + )(s) + ); + }; + + + // string :: String -> Parser String + const string = s => + // A particular string. + fmapP(cs => cs.join(''))( + sequenceP([...s].map(char)) + ); + + + // takeWhileP :: (Char -> Bool) -> Parser String + const takeWhileP = p => + // The largest prefix in which p is + // true over all the characters. + Parser( + compose( + pureList, + first(concat), + span(p) + ) + ); + + + // thenBindP :: Parser a -> Parser b -> + // (b -> Parser c) Parser c + const thenBindP = o => + // A combination of thenP and bindP in which a + // preliminary parser consumes text and discards + // its output, before any output of a subsequent + // parser is bound. + p => f => Parser( + s => parse(o)(s).flatMap( + vr => parse(p)(vr[1]).flatMap( + tpl => parse(f(tpl[0]))(tpl[1]) + ) + ) + ); + + + // thenP (>>) :: Parser a -> Parser b -> Parser b + const thenP = o => + // A composite parser in which o just consumes text + // and then p consumes more and returns a value. + p => Parser( + s => parse(o)(s).flatMap( + vr => parse(p)(vr[1]) + ) + ); + + // ------------------------ JXA ------------------------ + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + // ObjC.import('AppKit') + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + // copyText :: String -> IO String + const copyText = s => { + const pb = $.NSPasteboard.generalPasteboard; + return ( + pb.clearContents, + pb.setStringForType( + $(s), + $.NSPasteboardTypeString + ), + s + ); + }; + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: 'Tuple', + '0': a, + '1': b, + length: 2 + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c + const compose = (...fs) => + // A function defined by the right-to-left + // composition of all the functions in fs. + fs.reduce( + (f, g) => x => f(g(x)), + x => x + ); + + + // concat :: [[a]] -> [a] + // concat :: [String] -> String + const concat = xs => ( + ys => 0 < ys.length ? ( + ys.every(Array.isArray) ? ( + [] + ) : '' + ).concat(...ys) : ys + )(list(xs)); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + + // first :: (a -> b) -> ((a, c) -> (b, c)) + const first = f => + // A simple function lifted to one which applies + // to a tuple, transforming only its first item. + xy => Tuple(f(xy[0]))( + xy[1] + ); + + + // fst :: (a, b) -> a + const fst = tpl => + // First member of a pair. + tpl[0]; + + + // list :: StringOrArrayLike b => b -> [a] + const list = xs => + // xs itself, if it is an Array, + // or an Array derived from xs. + Array.isArray(xs) ? ( + xs + ) : Array.from(xs || []); + + + // ne :: a -> a -> Bool + const ne = a => + b => a !== b; + + + // pureList :: a -> [a] + const pureList = x => [x]; + + + // snd :: (a, b) -> b + const snd = tpl => + // Second member of a pair. + tpl[1]; + + + // sj :: a -> String + function sj() { + const args = Array.from(arguments); + return JSON.stringify.apply( + null, + 1 < args.length && !isNaN(args[0]) ? [ + args[1], null, args[0] + ] : [args[0], null, 2] + ); + } + + + // span :: (a -> Bool) -> [a] -> ([a], [a]) + const span = p => + // Longest prefix of xs consisting of elements which + // all satisfy p, tupled with the remainder of xs. + xs => { + const + ys = 'string' !== typeof xs ? ( + list(xs) + ) : xs, + iLast = ys.length - 1; + return splitAt( + until( + i => iLast < i || !p(ys[i]) + )(i => 1 + i)(0) + )(ys); + }; + + + // splitAt :: Int -> [a] -> ([a], [a]) + const splitAt = n => + xs => Tuple(xs.slice(0, n))( + xs.slice(n) + ); + + + // until :: (a -> Bool) -> (a -> a) -> a -> a + const until = p => f => x => { + let v = x; + while (!p(v)) v = f(v); + return v; + }; + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620257297.67379797 + ModificationDate + 754775805.87437201 + Name + com.OakTree.Accordance + Triggers + + UID + 07E17E89-9FF5-4441-833C-895DCDEE93EC + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997072 + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997073 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Share + Copy Link + + TargetApplication + + BundleIdentifier + com.reederapp.macOS + Name + Reeder + NewFile + /Applications/Reeder.app + + TargetingType + Specific + + + ActionName + Pause until clipboard has text + ActionUID + 15997074 + Conditions + + ConditionList + + + ClipboardConditionType + HasText + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + NotifyOnTimeOut + + TimeOutAbortsMacro + + TimeOutPeriod + 5 + + + ActionUID + 15997075 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either(alert('Reeder link'))(x => x)( + bindLR( + clipOfTypeLR('public.utf8-plain-text') + )(link => Right(`[](${link})`)) + ); + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // ----------------- GENERAL FUNCTIONS ----------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620248960.36932695 + ModificationDate + 665188318.83569002 + Name + com.reederapp.macOS + Triggers + + UID + 8098CD1B-0DE2-48FF-A65B-DCBB5F6875CA + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997058 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_BundleId + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + // Copy selected items from Bookends as MD links. + // Rob Trew @2020 @2024 + + // Ver 0.2 + + // const kmvar = {"local_BundleId": "com.sonnysoftware.bookends2"}; + + // main :: IO () + const main = () => { + const + url = "bookends://sonnysoftware.com/", + bookends = Application(kmvar.local_BundleId), + ws = bookends.libraryWindows; + + return ( + bookends.activate(), + either( + alert("Copy as MD link") + )( + mdLink => mdLink + )( + bindLR( + 0 < ws.length + ? Right(ws.at(0).selectedPublicationItems()) + : Left( + "No library windows open in Bookends." + ) + )( + items => Right( + items.map( + x => `[${bookends.format(x).trim()}]` + ( + `(${url}${x.id()})` + ) + ) + .join("\n") + ) + ) + ) + ); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = lr => + // Bind operator for the Either option type. + // If lr has a Left value then lr unchanged, + // otherwise the function mf applied to the + // Right value in lr. + mf => "Left" in lr + ? lr + : mf(lr.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 740902987.94469094 + ModificationDate + 740903580.281057 + Name + Bookends variant + Triggers + + + MacroTriggerType + Subroutine + Parameters + + local_BundleId + + ReturnsValue + + + + UID + F18CB180-7FD8-4256-81B1-47F55B93A93E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997081 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997082 + MacroActionType + SelectMenuItem + Menu + + Share + Copy Link + + TargetApplication + + BundleIdentifier + com.reederapp.5.macOS + Name + Reeder + NewFile + /Applications/Reeder.app + + TargetingType + Specific + + + ActionName + Pause until clipboard has text + ActionUID + 15997083 + Conditions + + ConditionList + + + ClipboardConditionType + HasText + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + NotifyOnTimeOut + + TimeOutAbortsMacro + + TimeOutPeriod + 5 + + + ActionUID + 15997084 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either(alert('Reeder link'))(x => x)( + bindLR( + clipOfTypeLR('public.utf8-plain-text') + )(link => Right(`[](${link})`)) + ); + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // ----------------- GENERAL FUNCTIONS ----------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 626022406.92979598 + ModificationDate + 634818531.90881097 + Name + com.reederapp.5.macOS + Triggers + + UID + C4C84F6D-6F93-4DBC-A584-D0154E8B28D3 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997118 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.ngocluu.goodlinks + Name + GoodLinks + NewFile + /Applications/GoodLinks.app + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997119 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997120 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Article + Copy + Article Link + + TargetApplication + + BundleIdentifier + com.ngocluu.goodlinks + Name + GoodLinks + NewFile + /Applications/GoodLinks.app + + TargetingType + Specific + + + ActionUID + 15997121 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + TimeOutPeriod + 5 + + + ActionUID + 15997122 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + local_URL + + + ActionUID + 15997123 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997124 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Article + Copy + Plain Text + + TargetApplication + + BundleIdentifier + com.ngocluu.goodlinks + Name + GoodLinks + NewFile + /Applications/GoodLinks.app + + TargetingType + Specific + + + ActionUID + 15997125 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + TimeOutPeriod + 5 + + + ActionUID + 15997126 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + local_Text + + + ActionUID + 15997127 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %Variable%local_Text[1]\n% + Variable + local_Label + + + ActionUID + 15997128 + MacroActionType + SetVariableToText + Text + [%Variable%local_Label%](%Variable%local_URL%) + Variable + mdLink + + + CreationDate + 740905921.42545903 + ModificationDate + 740906887.92760706 + Name + com.ngocluu.goodlinks + Triggers + + UID + BBD88F1F-F377-411D-98EC-4DFAAEF7AAAF + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997019 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + ObjC.import("AppKit"); + + // main :: IO () + const main = () => { + const + macroGroupName = "MD Link tools", + mdLinkToolsGroups = Application( + "Keyboard Maestro" + ) + .macroGroups.where({ + name: macroGroupName + }); + + return either( + alert("Copy as MD Link - Map bundle to UUID") + )( + uuidsShownAndUpdatedIO + )( + 0 < mdLinkToolsGroups.length ? (() => { + const + instances = mdLinkToolsGroups.at(0) + .macros() + .flatMap(macro => { + const k = macro.name(); + + return k.includes(".") ? ([ + [k, macro.id()] + ]) : []; + }); + + return Right( + Tuple( + JSON.stringify( + instances.reduce( + (a, [bundle, uuid]) => Object.assign( + a, { + [bundle]: uuid + } + ), {} + ), + null, 2 + ) + )(instances.length) + ); + })() : Left( + `Macro group not found:\n\n\t${macroGroupName}` + ) + ); + }; + + // uuidsShownAndUpdatedIO :: (Dict, Int) -> IO() + const uuidsShownAndUpdatedIO = tpl => { + const [dictJSON, n] = Array.from(tpl); + + return ( + copyText(dictJSON), + Application("Keyboard Maestro Engine") + .setvariable( + "uuidsForMDLink", { + to: dictJSON + } + ), + alert( + `Copy as MD Link :: ${n} specialised instances.` + )(dictJSON) + ); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // copyText :: String -> IO String + const copyText = s => { + const pb = $.NSPasteboard.generalPasteboard; + + return ( + pb.clearContents, + pb.setStringForType( + $(s), + $.NSPasteboardTypeString + ), + s + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2 + }); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => e.Left ? ( + fl(e.Left) + ) : fr(e.Right); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 634657573.72913599 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 659824634.96878505 + Name + Update map from bundleIDs to KM UUIDs (after new sub-macro added) + Triggers + + UID + 62E7B57C-C52F-4613-B5B7-06353E3646C0 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997180 + MacroActionType + Comment + StyledText + + cnRmZAAAAAADAAAAAgAAAAcAAABU + WFQucnRmAQAAAC7zBQAAKwAAAAEA + AADrBQAAe1xydGYxXGFuc2lcYW5z + aWNwZzEyNTJcY29jb2FydGYyNTEz + Clxjb2NvYXRleHRzY2FsaW5nMFxj + b2NvYXBsYXRmb3JtMHtcZm9udHRi + bFxmMFxmc3dpc3NcZmNoYXJzZXQw + IEhlbHZldGljYTtcZjFcZm5pbFxm + Y2hhcnNldDAgTWVubG8tUmVndWxh + cjtcZjJcZnN3aXNzXGZjaGFyc2V0 + MCBIZWx2ZXRpY2EtT2JsaXF1ZTsK + fQp7XGNvbG9ydGJsO1xyZWQyNTVc + Z3JlZW4yNTVcYmx1ZTI1NTtccmVk + MFxncmVlbjBcYmx1ZTA7fQp7XCpc + ZXhwYW5kZWRjb2xvcnRibDs7XGNz + c3JnYlxjMFxjMFxjMFxjODQ3MDZc + Y25hbWUgY29udHJvbFRleHRDb2xv + cjt9ClxwYXJkXHR4NTYwXHR4MTEy + MFx0eDE2ODBcdHgyMjQwXHR4Mjgw + MFx0eDMzNjBcdHgzOTIwXHR4NDQ4 + MFx0eDUwNDBcdHg1NjAwXHR4NjE2 + MFx0eDY3MjBccGFyZGlybmF0dXJh + bFxwYXJ0aWdodGVuZmFjdG9yMAoK + XGYwXGZzMzBcZnNtaWxsaTE1MzY0 + IFxjZjIgRm9yIGEgbWVudSBvZiBs + aW5rcyBpbiB0aGUgZnJvbnQgZG9j + dW1lbnQgaW4gVGFza1BhcGVyIDMs + IGVkaXQgOlwKXHBhcmRcdHg1NjBc + dHgxMTIwXHR4MTY4MFx0eDIyNDBc + dHgyODAwXHR4MzM2MFx0eDM5MjBc + dHg0NDgwXHR4NTA0MFx0eDU2MDBc + dHg2MTYwXHR4NjcyMFxwYXJkaXJu + YXR1cmFsXHBhcnRpZ2h0ZW5mYWN0 + b3IwCgpcZjFcZnMyNCBcY2YwIFwK + ICAgIGNvbnN0IHNob3dMaW5rc0lu + VGFza1BhcGVyRnJvbnREb2MgPSBm + YWxzZTtcClwKXHBhcmRcdHg1NjBc + dHgxMTIwXHR4MTY4MFx0eDIyNDBc + dHgyODAwXHR4MzM2MFx0eDM5MjBc + dHg0NDgwXHR4NTA0MFx0eDU2MDBc + dHg2MTYwXHR4NjcyMFxwYXJkaXJu + YXR1cmFsXHBhcnRpZ2h0ZW5mYWN0 + b3IwCgpcZjBcZnMzMFxmc21pbGxp + MTUzNjQgXGNmMiBuZWFyIHRoZSB0 + b3Agb2YgdGhlIEphdmFTY3JpcHQg + Zm9yIEF1dG9tYXRpb24gc2NyaXB0 + IGJlbG93LCB0bzpcClwKCQpcZjFc + ZnMyNCBcY2YwIGNvbnN0IHNob3dM + aW5rc0luVGFza1BhcGVyRnJvbnRE + b2MgPSB0cnVlO1wKClxmMFxmczMw + XGZzbWlsbGkxNTM2NCBcY2YyIFwK + T3IsIG9yIGEgbWVudSBvZiBsaW5r + cyBrZXB0IGluIGEgcGFydGljdWxh + ciB0ZXh0IGZpbGUsIGluIFttYXJr + ZG93biBsaW5rXSh1cmwpIGZvcm1h + dDpcClwKXHBhcmRcdHg1NjBcdHgx + MTIwXHR4MTY4MFx0eDIyNDBcdHgy + ODAwXHR4MzM2MFx0eDM5MjBcdHg0 + NDgwXHR4NTA0MFx0eDU2MDBcdHg2 + MTYwXHR4NjcyMFxwYXJkaXJuYXR1 + cmFsXHBhcnRpZ2h0ZW5mYWN0b3Iw + CgpcZjFcZnMyNCBcY2YwIC0KXGYw + XGZzMzBcZnNtaWxsaTE1MzY0IFxj + ZjIgIGVkaXQgdGhlIHZhbHVlIG9m + IApcZjFcZnMyNCBcY2YwIHNob3dM + aW5rc0luVGFza1BhcGVyRnJvbnRE + b2MgClxmMFxmczMwXGZzbWlsbGkx + NTM2NCBcY2YyIHRvIApcZjFcZnMy + NCBcY2YwIGZhbHNlLCBcCi0gClxm + MFxmczMwXGZzbWlsbGkxNTM2NCBc + Y2YyIHNwZWNpZnkgYSBmaWxlIHBh + dGggYXMgdGhlIHZhbHVlIG9mIHRo + ZSAKXGYyXGkgbGlua0xpc3RGaWxl + UGF0aCAKXGYwXGkwICB2YXJpYWJs + ZSBiZWxvdy59AQAAACMAAAABAAAA + BwAAAFRYVC5ydGYQAAAA65JGX7YB + AAAAAAAAAAAAAA== + + Title + A menu of links EITHER in a specified text file, OR in the front document in TaskPaper + + + ActionName + SPECIFY HERE THE FILE PATH A TEXT FILE containing MD links of the pattern [label](url) + ActionUID + 15997181 + MacroActionType + SetVariableToText + Text + ~/projects/activeProjects.taskpaper + Variable + linkListFilePath + + + ActionUID + 15997182 + DisplayKind + Briefly + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // Rob Trew @2020, @2021 + + // ver 0.03 + + // Menu of the labels of any links + // (assumed to be in MD format) in the front + // TaskPaper document, or in a file at the filePath + // specified by a Keyboard Maestro variable. + + // If one or more labels are chosen in the menu, + // the corresponding links are opened. + + + // -------------------- SETTINGS --------------------- + + // EITHER a menu of links in the TP3 front document, + const showLinksInTaskPaperFrontDoc = true; + + // OR a menu of links in a file at a given path, + // specified by a Keyboard Maestro variable. + + // e.g: '~/projects/activeProjects.taskpaper' + const filePathKMVarName = "linkListFilePath"; + + // If this is false, links with text or hyphens + // before the opening [label] are ignored + const blnIncludePrefixed = true; + + + // -------------------- MENU CODE -------------------- + // main :: IO () + const main = () => + either( + msg => msg.startsWith("User cancelled") ? ( + msg + ) : alert("Link menu")(msg) + )(openLinks)( + bindLR( + showLinksInTaskPaperFrontDoc ? ( + taskPaperFrontDocFilePathLR() + ) : filePathFromKMVariableLR( + filePathKMVarName + ) + )(fp => { + showLog("fp", fp); + + const + menuKVs = mdLinkValuesInFile( + blnIncludePrefixed + )(fp); + showLog("menuKVs", menuKVs); + + return 0 < menuKVs.length ? ( + bindLR( + showMenuLR(true)("Links")( + "Choose link(s):" + )(menuKVs[0].title)( + menuKVs.map(x => x.title) + ) + )( + compose( + Right, + menuChoiceValues( + menuKVs + )("title")("link") + ) + ) + ) : Left("No links found in document."); + }) + ); + + + // ----------- FRONT DOCUMENT IN TASKPAPER ----------- + + // taskPaperFrontDocFilePathLR :: Either String FilePath + const taskPaperFrontDocFilePathLR = () => { + const + tp = Application("TaskPaper"), + ds = tp.documents; + + return 0 < ds.length ? ( + Right(`${ds.at(0).file()}`) + ) : Left("No document found in TaskPaper"); + }; + + + // --- FILEPATH GIVEN IN KEYBOARD MAESTRO VARIABLE --- + const filePathFromKMVariableLR = kmVarName => { + const + fp = Application("Keyboard Maestro Engine") + .getvariable(kmVarName); + + return Boolean(fp) ? (() => { + const fpPath = filePath(fp); + + return doesFileExist(fpPath) ? ( + Right(fpPath) + ) : Left(`No file found at: ${fpPath}`); + })() : Left( + `No value found for KM variable: "${kmVarName}"` + ); + }; + + + // ---------- CHOICE OF LINKS IN GIVEN FILE ---------- + + // mdLinkValuesInFile :: Bool -> FilePath -> + // [{label :: String, link :: String }] + const mdLinkValuesInFile = stripPrefix => + fp => sortBy(comparing(x => x.label))( + lines(readFile(fp)).flatMap( + x => x.includes("](") ? ( + parse(mdLinkP())( + ( + stripPrefix ? s => { + const mbi = s.indexOf("["); + + return s.slice( + 0 <= mbi ? ( + mbi + ) : 0 + ); + } : strip + )(x) + ) + ) : [] + ) + .map(fst) + ); + + // openLinks :: [URL String] -> IO [URL String] + const openLinks = urls => { + const + sa = Object.assign( + Application.currentApplication(), { + includeStandardAdditions: true + }); + + return urls.map(x => ( + sa.openLocation(x), + x + )); + }; + + // menuChoiceValues :: [Dict a] -> + // String -> String -> [String] -> [a] + const menuChoiceValues = menuKVs => + // A map from a list of keys to a list of values, + // given a list of dictionaries, + // with their label and value keys, + // and some subset of label keys. + labelKey => valueKey => ks => { + const + dct = menuKVs.reduce( + (a, x) => Object.assign( + a, { + [x[labelKey]]: x[valueKey] + } + ), {} + ); + + return ks.flatMap(k => { + const v = dct[k]; + + return void 0 !== v ? ( + [v] + ) : []; + }); + }; + + // ------------------ PARSING LINKS ------------------ + + // mdLinkP :: () -> Parser Dict + const mdLinkP = () => + thenBindP( + char("[") + )( + takeWhileP(ne("]")) + )(title => thenBindP( + string("](") + )( + takeWhileP(ne(")")) + )(link => thenP( + char(")") + )( + pureP({ + title, + link + }) + ))); + + // ----------- GENERIC PARSER COMBINATORS ------------ + + // Parser :: String -> [(a, String)] -> Parser a + const Parser = f => + // A function lifted into a Parser object. + ({ + type: "Parser", + parser: f + }); + + + // altP (<|>) :: Parser a -> Parser a -> Parser a + const altP = p => + // p, or q if p doesn't match. + q => Parser(s => { + const xs = parse(p)(s); + + return 0 < xs.length ? ( + xs + ) : parse(q)(s); + }); + + + // apP <*> :: Parser (a -> b) -> Parser a -> Parser b + const apP = pf => + // A new parser obtained by the application + // of a Parser-wrapped function, + // to a Parser-wrapped value. + p => Parser( + s => parse(pf)(s).flatMap( + vr => parse( + fmapP(vr[0])(p) + )(vr[1]) + ) + ); + + + // char :: Char -> Parser Char + const char = x => + // A particular single character. + satisfy(c => x === c); + + + // fmapP :: (a -> b) -> Parser a -> Parser b + const fmapP = f => + // A new parser derived by the structure-preserving + // application of f to the value in p. + p => Parser( + s => parse(p)(s).flatMap( + first(f) + ) + ); + + + // liftA2P :: (a -> b -> c) -> + // Parser a -> Parser b -> Parser c + const liftA2P = op => + // The binary function op, lifted + // to a function over two parsers. + p => apP(fmapP(op)(p)); + + + // many :: Parser a -> Parser [a] + const many = p => { + // Zero or more instances of p. + // Lifts a parser for a simple type of value + // to a parser for a list of such values. + const someP = q => + liftA2P( + x => xs => [x].concat(xs) + )(q)(many(q)); + + return Parser( + s => parse( + 0 < s.length ? ( + altP(someP(p))(pureP([])) + ) : pureP([]) + )(s) + ); + }; + + + // parse :: Parser a -> String -> [(a, String)] + const parse = p => + // The result of parsing s with p. + s => p.parser([...s]); + + + // pureP :: a -> Parser a + const pureP = x => + // The value x lifted, unchanged, + // into the Parser monad. + Parser(s => [Tuple(x)(s)]); + + + // satisfy :: (Char -> Bool) -> Parser Char + const satisfy = test => + // Any character for which the + // given predicate returns true. + Parser( + s => 0 < s.length ? ( + test(s[0]) ? [ + Tuple(s[0])(s.slice(1)) + ] : [] + ) : [] + ); + + + // sequenceP :: [Parser a] -> Parser [a] + const sequenceP = ps => + // A single parser for a list of values, derived + // from a list of parsers for single values. + Parser( + s => ps.reduce( + (a, q) => a.flatMap( + vr => parse(q)(snd(vr)).flatMap( + first(xs => fst(vr).concat(xs)) + ) + ), + [Tuple([])(s)] + ) + ); + + + // string :: String -> Parser String + const string = s => + // A particular string. + fmapP(cs => cs.join(""))( + sequenceP([...s].map(char)) + ); + + + // takeWhileP :: (Char -> Bool) -> Parser String + const takeWhileP = p => + // The largest prefix in which p is + // true over all the characters. + Parser( + compose( + pureList, + first(concat), + span(p) + ) + ); + + + // thenBindP :: Parser a -> Parser b -> + // (b -> Parser c) Parser c + const thenBindP = o => + // A combination of thenP and bindP in which a + // preliminary parser consumes text and discards + // its output, before any output of a subsequent + // parser is bound. + p => f => Parser( + s => parse(o)(s).flatMap( + vr => parse(p)(vr[1]).flatMap( + tpl => parse(f(tpl[0]))(tpl[1]) + ) + ) + ); + + + // thenP (>>) :: Parser a -> Parser b -> Parser b + const thenP = o => + // A composite parser in which o just consumes text + // and then p consumes more and returns a value. + p => Parser( + s => parse(o)(s).flatMap( + vr => parse(p)(vr[1]) + ) + ); + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // doesFileExist :: FilePath -> IO Bool + const doesFileExist = fp => { + const ref = Ref(); + + return $.NSFileManager.defaultManager + .fileExistsAtPathIsDirectory( + $(fp) + .stringByStandardizingPath, ref + ) && 1 !== ref[0]; + }; + + + // filePath :: String -> FilePath + const filePath = s => + // The given file path with any tilde expanded + // to the full user directory path. + ObjC.unwrap(ObjC.wrap(s) + .stringByStandardizingPath); + + + // readFile :: FilePath -> IO String + const readFile = fp => { + // The contents of a text file at the + // filepath fp. + const + e = $(), + ns = $.NSString + .stringWithContentsOfFileEncodingError( + $(fp).stringByStandardizingPath, + $.NSUTF8StringEncoding, + e + ); + + return ObjC.unwrap( + ns.isNil() ? ( + e.localizedDescription + ) : ns + ); + }; + + + // showMenuLR :: Bool -> String -> String -> + // [String] -> String -> Either String [String] + const showMenuLR = blnMult => + // An optionally multi-choice menu, with + // a given title and prompt string. + // Listing the strings in xs, with + // the string `selected` pre-selected + // if found in xs. + menuTitle => prompt => selected => xs => + 0 < xs.length ? (() => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + sa.activate(); + + const v = sa.chooseFromList(xs, { + withTitle: menuTitle, + withPrompt: prompt, + defaultItems: xs.includes(selected) ? ( + [selected] + ) : [xs[0]], + okButtonName: "OK", + cancelButtonName: "Cancel", + multipleSelectionsAllowed: blnMult, + emptySelectionAllowed: false + }); + + return Array.isArray(v) ? ( + Right(v) + ) : Left(`User cancelled ${menuTitle} menu.`); + })() : Left(`${menuTitle}: No items to choose from.`); + + + // ---------------- GENERIC FUNCTIONS ---------------- + // https://github.com/RobTrew/prelude-jxa + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2 + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // comparing :: (a -> b) -> (a -> a -> Ordering) + const comparing = f => + x => y => { + const + a = f(x), + b = f(y); + + return a < b ? -1 : (a > b ? 1 : 0); + }; + + + // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c + const compose = (...fs) => + // A function defined by the right-to-left + // composition of all the functions in fs. + fs.reduce( + (f, g) => x => f(g(x)), + x => x + ); + + + // concat :: [[a]] -> [a] + // concat :: [String] -> String + const concat = xs => + 0 < xs.length ? ( + ( + xs.every(x => "string" === typeof x) ? ( + "" + ) : [] + ).concat(...xs) + ) : xs; + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e ? ( + fl(e.Left) + ) : fr(e.Right); + + + // first :: (a -> b) -> ((a, c) -> (b, c)) + const first = f => + // A simple function lifted to one which applies + // to a tuple, transforming only its first item. + xy => Tuple(f(xy[0]))( + xy[1] + ); + + + // fst :: (a, b) -> a + const fst = tpl => + // First member of a pair. + tpl[0]; + + + // identity :: a -> a + const identity = x => + // The identity function. + x; + + + // lines :: String -> [String] + const lines = s => + // A list of strings derived from a single + // newline-delimited string. + 0 < s.length ? ( + s.split(/[\r\n]/u) + ) : []; + + + // list :: StringOrArrayLike b => b -> [a] + const list = xs => + // xs itself, if it is an Array, + // or an Array derived from xs. + Array.isArray(xs) ? ( + xs + ) : Array.from(xs || []); + + + // ne :: a -> a -> Bool + const ne = a => + b => a !== b; + + + // pureList :: a -> [a] + const pureList = x => [x]; + + + // showLog :: a -> IO () + const showLog = (...args) => + // eslint-disable-next-line no-console + console.log( + args + .map(JSON.stringify) + .join(" -> ") + ); + + + // snd :: (a, b) -> b + const snd = tpl => + // Second member of a pair. + tpl[1]; + + + // sortBy :: (a -> a -> Ordering) -> [a] -> [a] + const sortBy = f => + xs => list(xs).slice() + .sort((a, b) => f(a)(b)); + + + // span :: (a -> Bool) -> [a] -> ([a], [a]) + const span = p => + // Longest prefix of xs consisting of elements which + // all satisfy p, tupled with the remainder of xs. + xs => { + const i = xs.findIndex(x => !p(x)); + + return -1 !== i ? ( + Tuple(xs.slice(0, i))( + xs.slice(i) + ) + ) : Tuple(xs)([]); + }; + + + // strip :: String -> String + const strip = s => + s.trim(); + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + ActionName + Clear the linkListFilePath variable, in case similar macros use a different file. + ActionUID + 15997183 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + linkListFilePath + + + CreationDate + 620152106.71959996 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 659824629.65695703 + Name + Menu of MD links in TaskPaper front document + Triggers + + UID + 9537F29C-AF38-44BB-8D6B-CD68F046F181 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997171 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + ObjC.import("AppKit"); + + // main :: IO () + const main = () => { + const + macroGroupName = "MD Link tools", + mdLinkToolsGroups = Application( + "Keyboard Maestro" + ) + .macroGroups.where({ + name: macroGroupName + }); + + return either( + alert("Copy as MD Link - instances") + )(tpl => { + const [listing, count] = Array.from(tpl); + + return ( + copyText(listing), + alert( + `Copy as MD Link :: ${count} specialised instances.` + )(listing) + ); + })( + 0 < mdLinkToolsGroups.length ? (() => { + const + instances = mdLinkToolsGroups.at(0) + .macros() + .flatMap(macro => { + const k = macro.name(); + + return k.includes(".") ? ( + [k] + ) : []; + }); + + return Right( + Tuple( + sortBy( + comparing(toLower) + )( + instances.map(x => `- ${x}`) + ) + .join("\n") + )(instances.length) + ); + })() : Left( + `Macro group not found:\n\n\t${macroGroupName}` + ) + ); + }; + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // copyText :: String -> IO String + const copyText = s => { + const pb = $.NSPasteboard.generalPasteboard; + + return ( + pb.clearContents, + pb.setStringForType( + $(s), + $.NSPasteboardTypeString + ), + s + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2 + }); + + + // comparing :: (a -> b) -> (a -> a -> Ordering) + const comparing = f => + x => y => { + const + a = f(x), + b = f(y); + + return a < b ? -1 : (a > b ? 1 : 0); + }; + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Either" === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // list :: StringOrArrayLike b => b -> [a] + const list = xs => + // xs itself, if it is an Array, + // or an Array derived from xs. + Array.isArray(xs) ? ( + xs + ) : Array.from(xs || []); + + + // sortBy :: (a -> a -> Ordering) -> [a] -> [a] + const sortBy = f => + xs => list(xs).slice() + .sort((a, b) => f(a)(b)); + + + // toLower :: String -> String + const toLower = s => + // Lower-case version of string. + s.toLocaleLowerCase(); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + CreationDate + 631058844.49377406 + CustomIconData + KMEC=Rounded=KMCOLOR:239,239,239,255=↓=86=2=0=0=KMCOLOR:255,0,0,255 + ModificationDate + 659824618.40139794 + Name + Copy list of specialised instances + Triggers + + UID + 88780E4E-8C09-4445-954A-F837D66D8859 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997116 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.hogbaysoftware.Bike + Name + Bike + NewFile + /Applications/Bike.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997117 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + const doc = Application("Bike").documents.at(0); + + return doc.exists() ? (() => { + const + rows = doc.rows.where({ + _and: [ + {selected: true}, + {_not: [{ + name: "" + }]} + ] + }); + + return 0 < rows.length ? (() => { + const + row = rows.at(0), + label = doc.selectedText() || ( + row.name() + ); + + return `[${label}](${row.url()})`; + })() : "No rows selected in Bike."; + })() : "No documents open in Bike."; +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 665188126.03055596 + ModificationDate + 685963836.36670601 + Name + com.hogbaysoftware.Bike + Triggers + + UID + 12FD40DC-15BB-41DA-B11F-FDEA59B33065 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997188 + MacroActionType + Notification + SoundName + Submarine + Subtitle + Cycling Kindle Views + Text + Please wait ... + Title + Copy As MD Link + + + ActionUID + 15997189 + DisplayKind + None + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Copy as MD Link for macOS Kindle + + // Rob Trew @ 2020 + + // Read available fields + // (for md link :: Title + Author, Location, ASIN) + // from active Kindle view <- {'page', 'icon', 'list'} + // and continue to 'next' view, + // until 'home' and 'dry'. + + // main :: IO () + const main = () => { + const kme = Application('Keyboard Maestro Engine'); + return either( + alert('Copy as MD Link') + )(x => ( + kme.setvariable('mdLink', { + to: x + }), + x + ))( + bindLR( + kindleProcLR() + )(proc => bindLR( + until( + homeAndDryOrSunk + )( + harvestNextViewLR(kme)(proc) + )( + viewHarvestFromProcessLR(kme)(proc)({ + viewPath: [] + }) + ) + )(linkFromDictLR)) + ); + }; + + + // harvestNextViewLR :: Application -> a + // Dict -> Either String Dict + const harvestNextViewLR = kme => + proc => lrDict => { + const + dict = lrDict.Right, + path = dict.viewPath, + currentView = last(path); + return ( + // In Kindle: + !isComplete(dict) ? ( + 'list' !== currentView ? ( + kme.doScript( + 'page' !== currentView ? ( + hitReturnPlist + ) : menuChainPlist([ + 'File', + 'Close Book' + ]) + ) + ) : Boolean(dict.location) ? ( + clickViewButton(proc)('list') + ) : kme.doScript(hitReturnPlist) + ) : 'page' !== currentView ? ( + 'page' !== path[0] ? ( + clickViewButton(proc)(currentView) + ) : kme.doScript( + menuChainPlist(['File', 'Open Book']) + ) + ) : kme.doScript( + menuChainPlist(['File', 'Close Book']) + ), + // In JavaScript interpreter: + viewHarvestFromProcessLR(kme)(proc)( + dict + ) + ); + }; + + + // linkFromDictLR :: Dict -> Either String String + const linkFromDictLR = dict => + // Either a message or an MD Link string + bindLR( + Boolean(dict.name) ? ( + Right(dict.name) + ) : Left('Name field not found') + )(name => { + const + i = [...name].findIndex( + c => ',' === c + ); + return -1 !== i ? (() => { + const [title, rest] = splitAt(i)(name); + const + authorList = Object.keys( + rest.slice(1).split(/; /g).reduce( + (a, k) => Object.assign({}, + a, { + [ + reverse( + k.split(', ') + ).join(' ') + ]: 1 + }), {} + ) + ); + return Right( + `[${title} – ${authorList.join(', ')}]` + ( + '(kindle://book?action=open&asin=' + ( + `${dict.asin}&location=${dict.location})` + ) + ) + ); + })() : Left('No comma found in name field.'); + }); + + + // isComplete :: Dict -> Bool + const isComplete = dict => { + // Complete if the dictionary + // holds all 3 key values <- {asin, location, name} + const ks = Object.keys(dict); + return ['name', 'asin', 'location'].every( + k => ks.includes(k) + ); + }; + + + // homeAndDryOrSunk :: Either String Dict -> Bool + const homeAndDryOrSunk = lrDict => + // Either an explanatory message, or + // True if all the sought key values are + // present in the dictionary, and the path + // shows a return to initial position. + Boolean(lrDict.Left) || (() => { + const + dict = lrDict.Right, + path = dict.viewPath; + return 1 < path.length && ( + path[0] === last(path) + ) && isComplete(dict); + })(); + + + // viewHarvestFromProcessLR :: Application -> + // process -> Dict -> Either String Dict + const viewHarvestFromProcessLR = kme => + // Either a message or a dictionary with + // additional fields and an extended path + // of visited views. + process => history => bindLR( + kindleProcWinLR(process) + )(window => bindLR( + kindleWindowToolbarLR(window) + )(toolbar => bindLR( + kindleToolbarControlsLR(toolbar) + )(controls => bindLR( + kindleWindowTypeLR(controls) + )(winType => 'page' === winType ? ( + locationFromKindlePageLR(history)(window) + ) : 'list' === winType ? ( + authorFromKindleListLR(history)(window) + ) : 'icon' === winType ? ( + bindLR( + titlePosnSizeFromKindleIconLR(window) + )( + asinEtcFromIconDetailsLR(kme)(history) + ) + ) : Left('other'))))); + + + // asinEtcFromIconDetailsLR :: Application -> + // Dict -> (String, String, String) -> + // Either String Dict + const asinEtcFromIconDetailsLR = kme => + // Either a message or a dictionary with an + // extended viewPath and updated or added + // `asin` and `name` fields. + history => ([label, posn, size]) => { + const + x = posn[0] + size[0] / 2, + y = posn[1] + size[1] / 2; + return Right( + ( + copyText(''), + kme.doScript( + clickPointPlist(x)(y) + ), + kme.doScript(controlCplist), + Object.assign({}, history, { + viewPath: history.viewPath.concat('icon'), + name: label.split(', Reading')[0], + asin: either( + _ => 'Empty clipboard - asin not found.' + )( + x => x.split('-0-')[0] + )(clipTextLR()) + }) + ) + ); + }; + + + // titlePosnSizeFromKindleIconLR :: Window -> + // Either String [String, (Int, Int), (Int, Int)] + const titlePosnSizeFromKindleIconLR = window => { + const lists = window.lists; + return bindLR( + 0 < lists.length ? ( + Right(lists.at(0)) + ) : Left('No icon list found - perhaps not icon view ?') + )(list => { + const + staticText = lists.at(0) + .staticTexts.at(0); + return Right([ + 'title', 'position', 'size' + ].map(k => staticText[k]())); + }); + }; + + + // kindleProcLR :: () -> Either String Process + const kindleProcLR = () => { + // Either a message, or a reference + // to a running Kindle process. + const + kindleProcs = Application('System Events') + .applicationProcesses.where({ + bundleIdentifier: 'com.amazon.Kindle' + }); + return 0 < kindleProcs.length ? ( + Right(kindleProcs.at(0)) + ) : Left('Kindle reader not found.'); + }; + + + // kindleProcWinLR :: Process -> Either String Window + const kindleProcWinLR = process => { + const wins = process.windows; + return 0 < wins.length ? ( + Right(wins.at(0)) + ) : Left('Open window not found in Kindle.'); + }; + + + // kindleToolbarControlsLR :: Toolbar -> + // Either Message Buttons + const kindleToolbarControlsLR = toolbar => { + const controls = toolbar.uiElements; + return 0 < controls.length ? ( + Right(controls) + ) : Left('No UI elements found in toolbar.'); + }; + + + // kindleWindowToolbarLR :: Window -> Either String Toolbar + const kindleWindowToolbarLR = window => { + const toolbars = window.toolbars; + return 0 < toolbars.length ? ( + Right(toolbars.at(0)) + ) : Left( + 'No toolbars found in Kindle window: ' + ( + window.name() + ) + ); + }; + + + // kindleWindowTypeLR :: controls -> + // Either String String + const kindleWindowTypeLR = controls => { + const count = controls.length; + return [3, 4, 8].includes(count) ? ( + Right(({ + 3: 'list', + 4: 'icon', + 8: 'page' + })[count]) + ) : Left( + 'Unrecognised pattern of controls on toolbar.' + ); + }; + + + // authorFromKindleListLR :: Dict -> + // Window -> Either String Dict + const authorFromKindleListLR = history => + window => { + const tables = window.tables; + return bindLR( + 0 < tables.length ? ( + Right(tables.at(0)) + ) : Left('No table found in window - perhaps not list ?') + )( + table => { + const staticTexts = table.staticTexts; + return bindLR( + 1 < staticTexts.length ? ( + Right(staticTexts) + ) : Left('Less than 2 static texts found.') + )( + texts => Right( + Object.assign({}, history, { + viewPath: history.viewPath + .concat('list'), + author: texts.at(1).title() + }) + ) + ); + } + ); + }; + + + // --------------------- TOKENS ---------------------- + + // locationFromKindlePageLR :: Dict -> + // Window -> Either String Dict + const locationFromKindlePageLR = history => + window => { + const + staticTexts = window.staticTexts(), + iLabel = staticTexts.findIndex( + x => 0 < x.uiElements.length + ); + return -1 !== iLabel ? (() => { + const + xs = staticTexts[iLabel] + .uiElements.at(0).value() + .split(/\s+/g), + lng = xs.length; + return bindLR( + 2 < lng ? ( + Right(xs[lng - 3]) + ) : Left( + 'Location string not found.' + ) + )(label => isNaN(label) ? ( + Left('Expected a location integer.') + ) : Right( + Object.assign({}, history, { + viewPath: history.viewPath.concat('page'), + location: parseInt(label) + }) + )); + })() : Left('Library page'); + }; + + + // --------------------- KINDLE ---------------------- + + // kindlePageBookNameLR :: Either String String + const kindlePageBookNameLR = () => { + const + se = Application('System Events'), + kindleProcs = se.applicationProcesses.where({ + bundleIdentifier: 'com.amazon.Kindle' + }); + return bindLR( + 0 < kindleProcs.length ? ( + Right(kindleProcs.at(0)) + ) : Left('Kindle reader not found.') + )(kindleProc => { + const ws = kindleProc.windows; + return bindLR( + 0 < ws.length ? ( + Right(ws.at(0)) + ) : Left('No windows open in Kindle') + )(win => { + const + toolbar = win.toolbars.at(0), + buttons = toolbar.buttons; + return buttons.length !== 4 ? ( + Left('This is not a reading page') + ) : Right(win.title().split(' - ').slice(1)[0]); + }); + }); + }; + + + // kindleButtonTypeLR :: () -> IO Dict + const kindleButtonTypeLR = () => { + // Either a message, or a dictionary with + // a single key drawn from {'icon', 'list', 'library'} + // the value of the key is a button which can be + // clicked with the method: + // (dct[k]).actions.at(0).perform() + const + se = Application('System Events'), + kindleProcs = se.applicationProcesses.where({ + bundleIdentifier: 'com.amazon.Kindle' + }); + return bindLR( + 0 < kindleProcs.length ? ( + Right(kindleProcs.at(0)) + ) : Left('Kindle reader not found.') + )(kindleProc => { + const ws = kindleProc.windows; + return bindLR( + 0 < ws.length ? ( + Right(ws.at(0)) + ) : Left('No windows open in Kindle') + )(win => { + const + toolbar = win.toolbars.at(0), + buttons = toolbar.buttons; + return Right( + 0 < buttons.length ? ({ + library: buttons.at(0) + }) : (() => { + const + groups = toolbar.groups, + blnIconView = 3 < groups.length, + group = groups.at( + blnIconView ? ( + 2 + ) : 1 + ); + return { + [blnIconView ? 'list' : 'icons']: ( + group.radioGroups.at(0) + .radioButtons.at( + blnIconView ? ( + 1 + ) : 0 + ) + ) + }; + })() + ); + }); + }); + }; + + + // clickViewButton :: Process -> String -> Kindle IO + const clickViewButton = proc => + // A button clicked + // Either the List view or Icons view button, + // to toggle from the current view to its sibling. + viewName => { + const + toolbar = proc.windows.at(0) + .toolbars.at(0), + blnIconView = 'list' !== viewName, + group = toolbar.groups.at( + blnIconView ? 2 : 1 + ), + button = group.radioGroups.at(0) + .radioButtons.at( + blnIconView ? 1 : 0 + ); + return button.actions.at(0).perform(); + }; + + + // kindleLocationFromPageLR :: + const kindleLocationFromPageLR = () => { + const + se = Application('System Events'), + kindleProcs = se.applicationProcesses.where({ + bundleIdentifier: 'com.amazon.Kindle' + }); + return bindLR( + 0 < kindleProcs.length ? ( + Right(kindleProcs.at(0)) + ) : Left('Kindle reader not found.') + )(kindleProc => { + const ws = kindleProc.windows; + return bindLR( + 0 < ws.length ? ( + Right(ws.at(0)) + ) : Left('No windows open in Kindle') + )(win => { + const + staticTexts = win.staticTexts(), + iLabel = staticTexts.findIndex( + x => 0 < x.uiElements.length + ); + return -1 !== iLabel ? (() => { + const + xs = staticTexts[iLabel] + .uiElements.at(0).value() + .split(/\s+/g), + lng = xs.length; + return bindLR( + 2 < lng ? ( + Right(xs[lng - 3]) + ) : Left( + 'Location string not found.' + ) + )(label => isNaN(label) ? ( + Left('Expected a location integer.') + ) : Right(parseInt(label))); + })() : Left('Library page'); + }); + }); + }; + + + // kindleTypeAndFieldLR :: () -> Either String (String, String) + const kindleTypeAndFieldLR = () => + // Either a message or a key value pair in which + // the key is drawn from {'name', 'asin', 'location'} + either( + _ => bindLR( + kindleLocationFromPageLR() + )( + label => Right(['location', label]) + ) + )( + txt => 0 < txt.length ? ( + txt.endsWith('EBOK') ? ( + Right(['asin', txt.split('-')[0]]) + ) : Right(['name', txt]) + ) : Left('No text copied in Library view.') + )( + clipTextLR() + ); + + + // ------------------- KM ACTIONS -------------------- + + // activateKindlePlist :: XML String + const activateKindlePlist = `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>AllWindows</key> + <true/> + <key>AlreadyActivatedActionType</key> + <string>Normal</string> + <key>Application</key> + <dict> + <key>BundleIdentifier</key> + <string>com.amazon.Kindle</string> + <key>Name</key> + <string>Kindle</string> + <key>NewFile</key> + <string>/Applications/Kindle.app</string> + </dict> + <key>MacroActionType</key> + <string>ActivateApplication</string> + <key>ReopenWindows</key> + <false/> + <key>TimeOutAbortsMacro</key> + <true/> + </dict> + </array> + </plist>`; + + + // clickPointPlist :: Int -> Int -> XML String + const clickPointPlist = x => + y => `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>Action</key> + <string>MoveAndClick</string> + <key>Button</key> + <integer>0</integer> + <key>ClickCount</key> + <integer>1</integer> + <key>DisplayMatches</key> + <false/> + <key>DragHorizontalPosition</key> + <string>0</string> + <key>DragVerticalPosition</key> + <string>0</string> + <key>Fuzz</key> + <integer>15</integer> + <key>HorizontalPositionExpression</key> + <string>${x}</string> + <key>MacroActionType</key> + <string>MouseMoveAndClick</string> + <key>Modifiers</key> + <integer>0</integer> + <key>MouseDrag</key> + <string>None</string> + <key>Relative</key> + <string>Window</string> + <key>RelativeCorner</key> + <string>TopLeft</string> + <key>RestoreMouseLocation</key> + <false/> + <key>VerticalPositionExpression</key> + <string>${y}</string> + </dict> + </array> + </plist>`; + + + // menuChainPlist :: [String] -> XML String + const menuChainPlist = ks => `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>MacroActionType</key> + <string>SelectMenuItem</string> + <key>Menu</key> + <array> + ${ks.map(k => ['<string>',k,'</string>'].join('')).join('\n')} + </array> + <key>TargetApplication</key> + <dict> + <key>BundleIdentifier</key> + <string>com.amazon.Kindle</string> + <key>Name</key> + <string>Kindle</string> + <key>NewFile</key> + <string>/Applications/Kindle.app</string> + </dict> + <key>TargetingType</key> + <string>Specific</string> + </dict> + </array> + </plist>`; + + + // controlCplist :: XML String + const controlCplist = `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>KeyCode</key> + <integer>8</integer> + <key>MacroActionType</key> + <string>SimulateKeystroke</string> + <key>Modifiers</key> + <integer>256</integer> + <key>ReleaseAll</key> + <false/> + <key>TargetApplication</key> + <dict/> + <key>TargetingType</key> + <string>Front</string> + </dict> + </array> + </plist>`; + + + // hitReturnPlist :: XML String + const hitReturnPlist = `<?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>KeyCode</key> + <integer>36</integer> + <key>MacroActionType</key> + <string>SimulateKeystroke</string> + <key>Modifiers</key> + <integer>0</integer> + <key>ReleaseAll</key> + <false/> + <key>TargetApplication</key> + <dict/> + <key>TargetingType</key> + <string>Front</string> + </dict> + </array> + </plist>`; + + + // pausePlist :: XML String + const pausePlist = ` + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + <plist version="1.0"> + <array> + <dict> + <key>MacroActionType</key> + <string>Pause</string> + <key>Time</key> + <string>0.2</string> + <key>TimeOutAbortsMacro</key> + <true/> + </dict> + </array> + </plist>`; + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + + // copyText :: String -> IO String + const copyText = s => { + const pb = $.NSPasteboard.generalPasteboard; + return ( + pb.clearContents, + pb.setStringForType( + $(s), + $.NSPasteboardTypeString + ), + s + ); + }; + + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + + // last :: [a] -> a + const last = xs => + // The last item of a list. + 0 < xs.length ? ( + xs.slice(-1)[0] + ) : undefined; + + + // reverse :: [a] -> [a] + const reverse = xs => + 'string' !== typeof xs ? ( + xs.slice(0).reverse() + ) : xs.split('').reverse().join(''); + + + // showLog :: a -> IO () + const showLog = (...args) => + console.log( + args + .map(JSON.stringify) + .join(' -> ') + ); + + // splitAt :: Int -> [a] -> ([a], [a]) + const splitAt = n => + xs => [ + xs.slice(0, n), + xs.slice(n) + ]; + + // until :: (a -> Bool) -> (a -> a) -> a -> a + const until = p => + f => x => { + let v = x; + while (!p(v)) v = f(v); + return v; + }; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + + + ActionUID + 15997190 + DeviceID + SOUNDEFFECTS + MacroActionType + PlaySound + Path + /System/Library/Sounds/Glass.aiff + TimeOutAbortsMacro + + + + CreationDate + 630112216.709167 + ModificationDate + 634818441.35937905 + Name + com.amazon.Kindle + Triggers + + UID + 18223386-E979-45C1-821E-E88E2A39595C + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997172 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997173 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + File + Copy + Link + + TargetApplication + + BundleIdentifier + com.agiletortoise.Drafts-OSX + Name + Drafts + NewFile + /Applications/Drafts.app + + TargetingType + Specific + + + ActionName + Pause Until non-empty text in clipboard + ActionUID + 15997174 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997175 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + mdURL + + + ActionUID + 15997176 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997177 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + File + Copy + Contents + + TargetApplication + + BundleIdentifier + com.agiletortoise.Drafts-OSX + Name + Drafts + NewFile + /Applications/Drafts.app + + TargetingType + Specific + + + ActionName + Pause Until non-empty text in clipboard + ActionUID + 15997178 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997179 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => either( + alert('Copy as MD link') + )( + txt => { + const + mdURL = Application( + 'Keyboard Maestro Engine' + ).getvariable('mdURL'); + return `[${txt}](${mdURL})`; + } + )( + bindLR( + clipTextLR() + )( + txt => Right( + txt.split(/[\n\r]+/)[0] + ) + ) + ); + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipTextLR :: () -> Either String String + const clipTextLR = () => ( + v => Boolean(v) && 0 < v.length ? ( + Right(v) + ) : Left('No utf8-plain-text found in clipboard.') + )( + ObjC.unwrap($.NSPasteboard.generalPasteboard + .stringForType($.NSPasteboardTypeString)) + ); + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 630094796.31117594 + ModificationDate + 634818434.67391503 + Name + com.agiletortoise.Drafts-OSX + Triggers + + UID + 20B09FEB-FD74-44D8-85AE-2635A4B58AD8 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997057 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + local_BundleId + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + // Rob Trew @2020, @2024 + + // main :: IO () + const main = () => { + const + version = kmvar.local_BundleId, + op = Application(version), + wins = op.windows; + + return either( + // A user message, + alert("MD link") + )( + // or a Markdown link. + x => x + )( + fmapLR( + linkForWindow + )( + 0 < wins.length + ? Right(wins.at(0)) + : Left(`No documents open in ${version}.`) + ) + ); + }; + + // linkForWindow :: Window -> String + const linkForWindow = window => { + const + selns = window.selectedTasks(), + doc = window.document, + fp = Path(doc.file()).toString(); + + return 0 < selns.length + ? taskMDLink(fp)(selns) + : fileMDLink(doc)(fp); + }; + + + // taskMDLink :: FilePath -> OP Selections -> String + const taskMDLink = fp => + selns => { + const + opURL = encodeURI(`omniplan://localhost${fp}`), + taskNames = selns.map(x => x.name()) + .join(","), + taskIds = selns.map(x => x.id()) + .join(",%2520"); + + return `[${taskNames}](${opURL}/task/${taskIds})`; + }; + + + // fileMDLink :: Document -> FilePath -> String + const fileMDLink = doc => + fp => { + const fileURL = encodeURI(`file://${fp}`); + + return `[${doc.name()}](${fileURL})`; + }; + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + // --------------------- GENERIC --------------------- + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c + const fmapLR = f => + // Either f mapped into the contents of any Right + // value in e, or e unchanged if is a Left value. + e => "Left" in e + ? e + : Right(f(e.Right)); + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 740911379.07701302 + ModificationDate + 740912834.35791695 + Name + Omniplan variant + Triggers + + + MacroTriggerType + Subroutine + Parameters + + local_BundleId + + ReturnsValue + + + + UID + 9815DA78-703E-424C-9025-2D182032A61F + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997133 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997134 + MacroActionType + SelectMenuItem + Menu + + Edit + Copy + + TargetApplication + + BundleIdentifier + com.apple.AddressBook + Name + Contacts + NewFile + /System/Applications/Contacts.app + + TargetingType + Specific + + + ActionUID + 15997135 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997136 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + ObjC.import('AppKit'); + + // Rob Trew @2020 + + // Any Contacts in the clipboard rewritten as + // [name](addressbook://etc) Markdown links + // and stored in an "mdLink" KM Variable + + // main :: IO () + const main = () => { + const + pBoard = $.NSPasteboard.generalPasteboard, + unwrap = ObjC.deepUnwrap; + return defaultOrFromFirst( + 'No contact found in clipboard' + )( + x => x + )( + unwrap( + pBoard.pasteboardItems.js[0] + .types + ) + .filter(k => k.startsWith('dyn')) + .flatMap(clipType => { + const + dict = unwrap( + pBoard + .propertyListForType(clipType) + ); + return !!dict ? ( + Object.keys(dict) + .filter(k => k.endsWith('ABPerson')) + .flatMap(uid => { + const + x = dict[uid], + pre = x.First || '', + nom = x.Last || '', + label = !!(pre + nom) ? ( + `${pre} ${nom}` + ) : (x.Organization || '?'); + return [ + `[${label}]` + ( + `(addressbook://${uid})` + ) + ]; + }).join('\n') + ) : []; + }) + ); + }; + + // ----------------- GENERIC FUNCTIONS ----------------- + // https://github.com/RobTrew/prelude-jxa + + // defaultOrFromFirst :: b -> (a -> b) -> [a] -> b + const defaultOrFromFirst = v => + // Equivalent of maybe | either for + // the list monad. + f => xs => 0 < xs.length ? ( + f(xs[0]) + ) : v; + + // MAIN --- + return main(); +})(); + + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 622187946.18329895 + ModificationDate + 634818445.21108699 + Name + com.apple.AddressBook + Triggers + + UID + 3EDBA831-A2BA-4FF3-BAE8-9875BE35BD6E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionName + Clear System Clipboard to empty string + ActionUID + 15997199 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997200 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + File + Copy Markdown Deeplink + + TargetApplication + + BundleIdentifier + com.lukilabs.lukiapp + Name + Craft + + TargetingType + Specific + + + ActionName + Pause Until Clipboard has non-empty text + ActionUID + 15997201 + Conditions + + ConditionList + + + ClipboardConditionType + IsNot + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997202 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + mdLink + + + CreationDate + 634235267.36757696 + ModificationDate + 634818488.53573096 + Name + com.lukilabs.lukiapp + Triggers + + UID + 496C3291-5872-40BB-8785-3B6AD48D9D8E + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997049 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997050 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + // Rob Trew @2020 + const + finder = Application('com.apple.finder'), + xs = finder.selection(); + return 0 < xs.length ? ( + xs.map( + x => `[${x.name()}](${x.url()})` + ).join('\n') + ) : (() => { + const + url = finder.insertionLocation().url(), + fp = decodeURI(url).slice(7); + return `[${fp}](${url})`; + })(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620251830.94660604 + ModificationDate + 697800471.51998305 + Name + com.apple.finder + Triggers + + UID + 0CA4309A-CBFF-463A-B6CE-451D5BD75292 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997064 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + return Application('HoudahSpot') + .selection() + .map( + x => `[${x.name()}]` + ( + `(${encodeURI('file://' + x.path())})` + ) + ) + .join('\n'); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 629131329.70978701 + ModificationDate + 634818477.52116799 + Name + com.houdah.HoudahSpot4 + Triggers + + UID + 4EF48A12-8FF0-4093-AFF6-AB19D31E6538 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997203 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997204 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + de.zettelkasten.TheArchive + Name + The Archive + NewFile + /Applications/The Archive.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997205 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Edit + Copy Link to Note + + TargetApplication + + BundleIdentifier + de.zettelkasten.TheArchive + Name + The Archive + NewFile + /Applications/The Archive.app + + TargetingType + Specific + + + ActionUID + 15997206 + Conditions + + ConditionList + + + ClipboardConditionType + StartsWith + ClipboardText + [[ + ConditionType + Clipboard + + + ConditionListMatch + Any + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997207 + Delete + + Destination + Clipboard + First + 3 + IsDisclosed + + MacroActionType + Substring + Second + -2 + Source + Clipboard + StringRangeType + From + + + ActionUID + 15997208 + Delete + + Destination + Clipboard + First + 2 + IsDisclosed + + MacroActionType + Substring + Second + -2 + Source + Clipboard + StringRangeType + Last + + + ActionUID + 15997209 + IsDisclosed + + MacroActionType + SetVariableToText + Text + [%SystemClipboard%]( + Variable + mdLink + + + Action + PercentEncodeForURL + ActionUID + 15997210 + Destination + Clipboard + IsDisclosed + + MacroActionType + Filter + Source + Clipboard + + + ActionUID + 15997211 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + TargetNamedClipboardRedundantDisplayName + mdLink + TargetNamedClipboardUID + DFCE8C45-2347-4DE1-988E-B2682FFE34BC + TargetUseNamedClipboard + + Text + %Variable%mdLink%thearchive://match/%SystemClipboard%) + + + CreationDate + 621966806.012748 + ModificationDate + 634818679.58455896 + Name + de.zettelkasten.TheArchive + Triggers + + UID + 2214E66C-17A6-4B1D-B0BA-6E0C551A295F + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997061 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + return Application('HoudahSpot') + .selection() + .map( + x => `[${x.name()}]` + ( + `(${encodeURI('file://' + x.path())})` + ) + ) + .join('\n'); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 635934929.79883099 + ModificationDate + 635934979.944368 + Name + com.houdah.HoudahSpot-setapp + Triggers + + UID + 58FCE941-F9D2-4ABE-8D2C-42045D3E5B3C + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997114 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + // main :: IO () + const main = () => { + // com.omnigroup.OmniOutliner5 + const + docs = Application("OmniOutliner") + .documents; + + return either( + msg => ( + alert("Copy as MD links")(msg), + "" + ) + )( + mdLink => mdLink + )( + bindLR( + 0 < docs.length ? ( + Right(docs.at(0)) + ) : Left( + "No documents open in OmniOutliner." + ) + )( + ooLinksLR + ) + ); + }; + + // --------------- OMNIOUTLINER LINKS ---------------- + + // ooLinksLR :: OO Document -> Either String String + const ooLinksLR = doc => { + const file = doc.file(); + + return null !== file ? (() => { + const + prefix = "omnioutliner:///open?row=", + fp = Path(doc.file()).toString(), + mdDoc = `[${doc.name()}](file://${fp})`, + selns = doc.selectedRows; + + return Right( + 0 < selns.length ? (() => { + const + rowLinks = selns() + .map(row => { + const + k = row.name(), + v = `${prefix}${row.id()}`; + + return `[${k}](${v})`; + }) + .join("\n"); + + return `${mdDoc}\n${rowLinks}`; + })() : mdDoc + ); + })() : Left( + "OmniOutliner document not saved." + ); + }; + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + + // --------------------- GENERIC --------------------- + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => m.Left ? ( + m + ) : mf(m.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => e.Left ? ( + fl(e.Left) + ) : fr(e.Right); + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 654893935.81021404 + ModificationDate + 654893959.09812498 + Name + com.omnigroup.OmniOutliner5 + Triggers + + UID + 2B4FE83E-1647-4240-963F-9DDF87BE7D4C + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997012 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997013 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + "use strict"; + + const + devon = Application("DEVONthink 3"), + tab = devon.thinkWindows.at(0).currentTab, + contentRecord = tab.contentRecord, + [name, ref] = contentRecord.exists() + ? [contentRecord, tab] + : (() => { + const + selectedRecords = devon.selectedRecords, + record = 1 === selectedRecords.length + ? selectedRecords.at(0) + : devon.viewerWindows.at(0).root; + + return [record, record]; + })(); + + return `[${name.name()}](${ref.referenceURL()})`; +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620239734.951316 + ModificationDate + 734221570.93161905 + Name + com.devon-technologies.think3 + Triggers + + UID + BA792D01-8417-4B7C-ADCD-8903619366ED + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997187 + MacroActionType + ExecuteSubroutine + MacroUID + 9BB504DE-9FA1-423A-9B08-8F6075FA3C73 + Parameters + + com.omnigroup.OmniFocus4.MacAppStore + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 740397011.78672504 + ModificationDate + 740492509.96030295 + Name + com.omnigroup.OmniFocus4.MacAppStore + Triggers + + UID + 4EDEB11D-1860-44C7-8315-3ED28FA43011 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997033 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.culturedcode.ThingsMac + Name + Things + NewFile + /Applications/Things3.app + + IsDisclosed + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997034 + IsDisclosed + + MacroActionType + SetVariableToText + Text + + Variable + mdLink + + + ActionUID + 15997035 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997036 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Items + Share + Copy Link + + TargetApplication + + BundleIdentifier + com.culturedcode.ThingsMac + Name + Things + NewFile + /Applications/Things3.app + + TargetingType + Specific + + + ActionUID + 15997037 + Conditions + + ConditionList + + + ClipboardConditionType + StartsWith + ClipboardText + things + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997038 + IsDisclosed + + MacroActionType + SetVariableToText + Text + %SystemClipboard% + Variable + mdURL + + + ActionUID + 15997039 + MacroActionType + Pause + Time + .5 + TimeOutAbortsMacro + + + + ActionUID + 15997040 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Items + Share + Copy Text + + TargetApplication + + BundleIdentifier + com.culturedcode.ThingsMac + Name + Things + NewFile + /Applications/Things3.app + + TargetingType + Specific + + + ActionUID + 15997041 + Conditions + + ConditionList + + + ClipboardConditionType + DoesNotMatch + ClipboardText + ^things* + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997042 + MacroActionType + SetVariableToText + Text + [%SystemClipboard%](%Variable%mdURL%) + Variable + mdLink + + + CreationDate + 628200210.26363599 + ModificationDate + 634818462.92986596 + Name + com.culturedcode.ThingsMac + Triggers + + UID + 075A5BD2-20BD-4B97-9D10-747D098FD15D + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997067 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997068 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Edit + Copy + + TargetApplication + + BundleIdentifier + com.apple.iCal + Name + Calendar + NewFile + /System/Applications/Calendar.app + + TargetingType + Specific + + + ActionName + Pause until clipboard has text + ActionUID + 15997069 + Conditions + + ConditionList + + + ClipboardConditionType + HasText + ClipboardText + + ConditionType + Clipboard + + + ConditionListMatch + All + + IsDisclosed + + MacroActionType + PauseUntil + TimeOutAbortsMacro + + + + ActionUID + 15997070 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + + // Rob Trew @2020 + + ObjC.import('AppKit'); + + // main :: IO () + const main = () => + either(alert('Calendar link'))(x => x)( + bindLR( + clipOfTypeLR('public.utf8-plain-text') + )(k => bindLR( + clipOfTypeLR('public.url') + )(v => Right(`[${k}](${v})`))) + ); + + // ------------------------ JXA ------------------------ + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application('System Events'), { + includeStandardAdditions: true + }); + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ['OK'], + defaultButton: 'OK' + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + strClip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + return 0 < strClip.length ? ( + Right(strClip) + ) : Left( + 'No clipboard content found for type "' + + utiOrBundleID + '"' + ); + }; + + // ----------------- GENERAL FUNCTIONS ----------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: 'Either', + Left: x + }); + + // Right :: b -> Either a b + const Right = x => ({ + type: 'Either', + Right: x + }); + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => 'Either' === e.type ? ( + undefined !== e.Left ? ( + fl(e.Left) + ) : fr(e.Right) + ) : undefined; + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = m => + mf => undefined !== m.Left ? ( + m + ) : mf(m.Right); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 620246382.12232697 + ModificationDate + 634818454.38636994 + Name + com.apple.iCal + Triggers + + UID + AFA16B8B-A4AC-4437-BC7F-C57220982F3C + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997089 + IsDisclosed + + MacroActionType + DeletePastClipboard + PastExpression + 0 + + + ActionUID + 15997090 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + 9999 + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + (() => { + 'use strict'; + // Rob Trew @2023 + const + pfinder = Application('com.cocoatech.PathFinder'), + xs = pfinder.selection(); + + return 0 < xs.length ? ( + xs.map( + x => `[${x.name()}](${x.url()})` + ).join('\n') + ) : (() => { + const + url = pfinder.insertionLocation().url(), + fp = decodeURI(url).slice(7); + + return `[${fp}](${url})`; + })(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 697799802.28194404 + ModificationDate + 697801093.75284004 + Name + com.cocoatech.PathFinder + Triggers + + UID + 51477C01-7B30-4208-B543-AEDCAD8E7431 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 16464173 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + ObjC.import("sqlite3"); + + // MD Link(s) from com.apple.notes selection(s) + // Rob Trew @2024 + // Ver 0.2 + + // ---------------------- MAIN ----------------------- + const main = () => { + const + fpDB = [ + "~/Library", + "Group Containers/group.com.apple.notes", + "NoteStore.sqlite" + ].join("/"), + pks = Application("Notes").selection().map( + x => x.id().split("/") + .slice(-1)[0].slice(1) + ); + + return either( + msg => msg + )( + gen => Array.from(gen).join("\n") + )( + doesFileExist(fpDB) + ? 0 < pks.length + ? sqliteQueryMatchesLR(fpDB)( + unwords([ + "SELECT '['", + "|| ZTITLE1", + "|| '](applenotes:note/'", + "|| ZIDENTIFIER", + "|| ')'", + "FROM ZICCLOUDSYNCINGOBJECT", + "WHERE Z_PK IN", + `(${pks.join(",")})` + ]) + ) + : Left("Nothing selected in Notes.") + : Left(`File not found: ${fpDB}`) + ); + }; + + // --------------------- SQLITE ---------------------- + + // sqliteQueryMatchesLR :: FilePath -> + // SQL String -> Either String [String] + const sqliteQueryMatchesLR = fpSqliteDB => + sqlQuery => { + const + SQLITE_OK = parseInt($.SQLITE_OK, 10), + SQLITE_ROW = parseInt($.SQLITE_ROW, 10), + ppDb = Ref(); + + return fmapLR( + tpl => unfoldr( + stmt => SQLITE_ROW === $.sqlite3_step( + stmt + ) + ? Just( + Tuple( + $.sqlite3_column_text( + stmt, 0 + ) + )(stmt) + ) + : ( + $.sqlite3_finalize(stmt), + $.sqlite3_close(tpl[0]), + Nothing() + ) + )(tpl[1]) + )( + bindLR( + SQLITE_OK !== $.sqlite3_open( + filePath(fpSqliteDB), ppDb + ) + ? Left($.sqlite3_errmsg(ppDb[0])) + : Right(ppDb[0]) + )(db => { + const ppStmt = Ref(); + + return ( + SQLITE_OK !== $.sqlite3_prepare_v2( + db, sqlQuery, -1, + ppStmt, Ref() + ) + ? Left($.sqlite3_errmsg(db)) + : Right( + Tuple(db)(ppStmt[0]) + ) + ); + }) + // Accumulation of all available rows + // in the table: + ); + }; + + // ----------------------- JXA ----------------------- + + // doesFileExist :: FilePath -> IO Bool + const doesFileExist = fp => { + const ref = Ref(); + + return $.NSFileManager + .defaultManager + .fileExistsAtPathIsDirectory( + $(fp).stringByStandardizingPath, + ref + ) && !ref[0]; + }; + + // --------------------- GENERIC --------------------- + + // Just :: a -> Maybe a + const Just = x => ({ + type: "Maybe", + Just: x + }); + + + // Nothing :: Maybe a + const Nothing = () => ({ + type: "Maybe", + Nothing: true + }); + + + // Tuple (,) :: a -> b -> (a, b) + const Tuple = a => + // A pair of values, possibly of + // different types. + b => ({ + type: "Tuple", + "0": a, + "1": b, + length: 2, + *[Symbol.iterator]() { + for (const k in this) { + if (!isNaN(k)) { + yield this[k]; + } + } + } + }); + + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = lr => + // Bind operator for the Either option type. + // If lr has a Left value then lr unchanged, + // otherwise the function mf applied to the + // Right value in lr. + mf => "Left" in lr + ? lr + : mf(lr.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // filePath :: String -> FilePath + const filePath = s => + // The given file path with any tilde expanded + // to the full user directory path. + ObjC.unwrap( + $(s).stringByStandardizingPath + ); + + // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c + const fmapLR = f => + // Either f mapped into the contents of any Right + // value in e, or e unchanged if is a Left value. + e => "Left" in e + ? e + : Right(f(e.Right)); + + + // unfoldr :: (b -> Maybe (a, b)) -> b -> Gen [a] + const unfoldr = f => + // A lazy (generator) list unfolded from a + // seed value by repeated application of f to a + // value until no residue remains. + // Dual to fold/reduce. + // f returns either Nothing or + // Just (value, residue). + // For a strict output list, + // wrap with `list` or Array.from + x => ( + function* () { + let maybePair = f(x); + + while (!maybePair.Nothing) { + const valueResidue = maybePair.Just; + + yield valueResidue[0]; + maybePair = f(valueResidue[1]); + } + }() + ); + + + // unwords :: [String] -> String + const unwords = xs => + // A space-separated string derived + // from a list of words. + xs.join(" "); + + // MAIN --- + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 754777737.43829 + ModificationDate + 754822641.13659406 + Name + com.apple.Notes + Triggers + + UID + 9506C565-89BB-4B35-A412-2C325C777412 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997066 + MacroActionType + ExecuteSubroutine + MacroUID + 9BB504DE-9FA1-423A-9B08-8F6075FA3C73 + Parameters + + com.omnigroup.OmniFocus4 + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 740391178.58374703 + ModificationDate + 740492483.36745906 + Name + com.omnigroup.OmniFocus4 + Triggers + + UID + 98FF02F8-0644-424F-8531-8AB9E6DFAB7F + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997027 + IsDisclosed + + JustDisplay + + MacroActionType + SetClipboardToText + Text + + + + ActionUID + 15997028 + AllWindows + + AlreadyActivatedActionType + Normal + Application + + BundleIdentifier + com.binarynights.ForkLift + Name + ForkLift + NewFile + /Applications/ForkLift.app + + MacroActionType + ActivateApplication + ReopenWindows + + TimeOutAbortsMacro + + + + ActionUID + 15997029 + Conditions + + ConditionList + + + ConditionType + Menu + MenuConditionSelectionType + Is + MenuConditionType + IsEnabled + MenuModifiers + 256 + MenuShortcut + C + MenuTitle + Copy + + + ConditionListMatch + All + + ElseActions + + MacroActionType + IfThenElse + ThenActions + + + ActionUID + 15997030 + IsDisclosed + + MacroActionType + SelectMenuItem + Menu + + Edit + Copy + + TargetApplication + + BundleIdentifier + com.binarynights.ForkLift + Name + ForkLift + NewFile + /Applications/ForkLift.app + + TargetingType + Specific + + + ActionUID + 15997031 + IsActive + + IsDisclosed + + MacroActionType + Pause + Time + .1 + TimeOutAbortsMacro + + + + ActionUID + 15997032 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + ObjC.import("AppKit"); + + const main = () => + either( + alert("Copy as MD link from ForkLift") + )( + mdLink => mdLink + )( + bindLR( + clipOfTypeLR("public.file-url") + )( + url => Right( + `[${takeFileName(url)}](${url})` + ) + ) + ); + + + // ----------------------- JXA ----------------------- + + // alert :: String => String -> IO String + const alert = title => + s => { + const sa = Object.assign( + Application("System Events"), { + includeStandardAdditions: true + }); + + return ( + sa.activate(), + sa.displayDialog(s, { + withTitle: title, + buttons: ["OK"], + defaultButton: "OK" + }), + s + ); + }; + + // clipOfTypeLR :: String -> Either String String + const clipOfTypeLR = utiOrBundleID => { + const + clip = ObjC.deepUnwrap( + $.NSString.alloc.initWithDataEncoding( + $.NSPasteboard.generalPasteboard + .dataForType(utiOrBundleID), + $.NSUTF8StringEncoding + ) + ); + + return 0 < clip.length + ? Right(clip) + : Left( + "No clipboard content found " + ( + `for type '${utiOrBundleID}'` + ) + ); + }; + + // --------------------- GENERIC --------------------- + + // Left :: a -> Either a b + const Left = x => ({ + type: "Either", + Left: x + }); + + + // Right :: b -> Either a b + const Right = x => ({ + type: "Either", + Right: x + }); + + + // bindLR (>>=) :: Either a -> + // (a -> Either b) -> Either b + const bindLR = lr => + // Bind operator for the Either option type. + // If lr has a Left value then lr unchanged, + // otherwise the function mf applied to the + // Right value in lr. + mf => "Left" in lr + ? lr + : mf(lr.Right); + + + // either :: (a -> c) -> (b -> c) -> Either a b -> c + const either = fl => + // Application of the function fl to the + // contents of any Left value in e, or + // the application of fr to its Right value. + fr => e => "Left" in e + ? fl(e.Left) + : fr(e.Right); + + + // takeFileName :: FilePath -> FilePath + const takeFileName = fp => + // The file name component of a filepath. + 0 < fp.length + ? "/" !== fp[fp.length - 1] + ? fp.split("/").slice(-1)[0] + : "" + : ""; + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + TimeOutAbortsMacro + + + + CreationDate + 728246646.694754 + ModificationDate + 728249043.407812 + Name + com.binarynights.ForkLift + Triggers + + UID + DF89E29C-96F3-4633-A9FA-86D88C1E4557 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997063 + MacroActionType + ExecuteSubroutine + MacroUID + 9BB504DE-9FA1-423A-9B08-8F6075FA3C73 + Parameters + + com.omnigroup.OmniFocus3 + + ResultVariable + VarName + TimeOutAbortsMacro + + + + CreationDate + 628968821.703493 + ModificationDate + 740492244.83095002 + Name + com.omnigroup.OmniFocus3 + Triggers + + UID + E3BF9020-661B-4952-B3C1-D28CF9D98B54 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + + Activate + Normal + CreationDate + 620208716.32096696 + Macros + + + Actions + + + ActionUID + 15997065 + DisplayKind + Variable + HonourFailureSettings + + IncludeStdErr + + IncludedVariables + + MacroActionType + ExecuteJavaScriptForAutomation + Path + + Text + return (() => { + "use strict"; + + // Rob Trew @2020, 2023 + // Ver 0.02 + + // main :: IO () + const main = () => { + const + mail = Application("com.apple.mail"), + msgID = x => x.messageId(), + timeStamp = x => x.dateSent(), + msgs = nubBy(on(eq)(msgID))( + sortOn(timeStamp)(mail.selection()) + ); + + return unlines(msgs.map( + x => { + const + label = `${x.subject()} ${ + taskPaperDateString( + new Date( + Math.max( + x.dateReceived(), + x.dateSent() + ) + ) + )} ${x.sender()}`, + url = mailURL(x.messageId()); + + return `[${label}](${url})`; + } + )); + }; + + // iso8601Local :: Date -> String + const iso8601Local = dte => + new Date(dte - (6E4 * dte.getTimezoneOffset())) + .toISOString(); + + + // taskPaperDateString :: Date -> String + const taskPaperDateString = dte => + second(t => t.slice(0, 5))( + iso8601Local(dte).split("T") + ) + .join(" "); + + + // mailURL :: String -> String + const mailURL = messageID => + `message://%3C${messageID}%3E`; + + + // ---------------------- GENERIC ---------------------- + + // comparing :: Ord a => (b -> a) -> b -> b -> Ordering + const comparing = f => + // The ordering of f(x) and f(y) as a value + // drawn from {-1, 0, 1}, representing {LT, EQ, GT}. + x => y => { + const + a = f(x), + b = f(y); + + return a < b ? -1 : (a > b ? 1 : 0); + }; + + + // eq (==) :: Eq a => a -> a -> Bool + const eq = a => + // True when a and b are equivalent in the terms + // defined below for their shared data type. + b => a === b; + + + // nubBy :: (a -> a -> Bool) -> [a] -> [a] + const nubBy = p => + // A sublist of xs from which all duplicates, + // (as defined by the equality predicate p) + // are excluded. + xs => xs.reduce( + (seen, x) => seen.some(p(x)) ? ( + seen + ) : [x].concat(seen), + [] + ); + + + // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c + const on = f => + // e.g. groupBy(on(eq)(length)) + g => a => b => f(g(a))(g(b)); + + + // second :: (a -> b) -> ((c, a) -> (c, b)) + const second = f => + // A function over a simple value lifted + // to a function over a tuple. + // f (a, b) -> (a, f(b)) + xy => [xy[0], f(xy[1])]; + + + // sortBy :: (a -> a -> Ordering) -> [a] -> [a] + const sortBy = f => + // A copy of xs sorted by the comparator function f. + xs => xs.slice() + .sort((a, b) => f(a)(b)); + + + // sortOn :: Ord b => (a -> b) -> [a] -> [a] + const sortOn = f => + // Equivalent to sortBy(comparing(f)), but with f(x) + // evaluated only once for each x in xs. + // ('Schwartzian' decorate-sort-undecorate). + xs => sortBy( + comparing(x => x[0]) + )( + xs.map(x => [f(x), x]) + ) + .map(x => x[1]); + + + // unlines :: [String] -> String + const unlines = xs => + // A single string formed by the intercalation + // of a list of strings with the newline character. + xs.join("\n"); + + return main(); +})(); + TimeOutAbortsMacro + + TrimResults + + TrimResultsNew + + UseModernSyntax + + UseText + + Variable + mdLink + + + CreationDate + 699254658.57702303 + ModificationDate + 740996738.75287104 + Name + com.apple.mail + Triggers + + UID + C75402F0-EB7F-4C29-AF94-1492719DE0E5 + + + Name + MD Link tools + ToggleMacroUID + 396599CD-B6E6-42BD-9CF7-3139986864CE + UID + 05096E60-C3F6-4ED0-9664-B87A21A1B57C + + +