Apple suggests using subPathsOfDirectory
for macOS 10.5+, so I post an updated macro:
Javascript source:
return (() => {
"use strict";
// Copyright (c) 2023 Gabriel Scalise
// Twitter - @unlocked2412
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// jxaContext :: IO ()
const jxaContext = () => {
// main :: IO ()
const main = () => {
const
// dir = filePath("~/Dropbox"),
// colorName = "Yellow";
dir = filePath(kmvar.localDir),
colorName = kmvar.localColorName;
return bindLR(
listDirectoryRecursiveLR(dir)
)(
compose(
Right,
filter(compose(elem(colorName), finderTags)),
map(combine(dir))
)
)
};
// GENERICS ----------------------------------------------------------------
// listDirectoryRecursiveLR :: FilePath -> Either String (IO [FilePath])
const listDirectoryRecursiveLR = fp => {
const
e = $(),
ns = $.NSFileManager.defaultManager
.subpathsOfDirectoryAtPathError(
$(fp).stringByStandardizingPath,
e
);
return ns.isNil() ? (
Left(ObjC.unwrap(e.localizedDescription))
) : Right(
ObjC.deepUnwrap(ns)
)
};
// JS For Automation -------------------------------------------
// finderTags :: FilePath -> [String]
const finderTags = fp => {
const
ref = Ref(),
error = $(),
aURL = $.NSURL.fileURLWithPath(
fp
),
result = aURL.getResourceValueForKeyError(
ref,
$.NSURLTagNamesKey,
error
);
return ref[0].isNil() ? (
[]
) : ObjC.deepUnwrap(ref[0])
};
// JS Prelude --------------------------------------------------
// 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);
// 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];
}
}
}
});
// all :: (a -> Bool) -> [a] -> Bool
const all = p =>
// True if p(x) holds for every x in xs.
xs => [...xs].every(p);
// and :: [Bool] -> Bool
const and = xs =>
// True unless any value in xs is false.
[...xs].every(Boolean);
// any :: (a -> Bool) -> [a] -> Bool
const any = p =>
// True if p(x) holds for at least
// one item in xs.
xs => [...xs].some(p);
// combine (</>) :: FilePath -> FilePath -> FilePath
const combine = fp =>
// The concatenation of two filePath segments,
// without omission or duplication of "/".
fp1 => Boolean(fp) && Boolean(fp1) ? (
"/" === fp1.slice(0, 1) ? (
fp1
) : "/" === fp.slice(-1) ? (
fp + fp1
) : `${fp}/${fp1}`
) : fp + fp1;
// 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
);
// cycle :: [a] -> Generator [a]
const cycle = function* (xs) {
// An infinite repetition of xs,
// from which a prefix of arbritrary
// length may be drawn.
const n = xs.length;
let i = 0;
while (true) {
yield xs[i];
i = (1 + i) % n;
}
};
// elem :: Eq a => a -> [a] -> Bool
const elem = x =>
// True if xs contains an instance of x.
xs => {
const t = xs.constructor.name;
return "Array" !== t ? (
xs["Set" !== t ? "includes" : "has"](x)
) : xs.some(eq(x));
};
// 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 => {
const t = typeof a;
return t !== typeof b ? (
false
) : "object" !== t ? (
"function" !== t ? (
a === b
) : a.toString() === b.toString()
) : (() => {
const kvs = Object.entries(a);
return kvs.length !== Object.keys(b).length ? (
false
) : kvs.every(([k, v]) => eq(v)(b[k]));
})();
};
// 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
);
// filter :: (a -> Bool) -> [a] -> [a]
const filter = p =>
// The elements of xs which match
// the predicate p.
xs => [...xs].filter(p);
// 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.
([x, y]) => Tuple(f(x))(y);
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
// The catamorphism on trees. A summary
// value obtained by a depth-first fold.
const go = tree => f(
root(tree)
)(
nest(tree).map(go)
);
return go;
};
// length :: [a] -> Int
const length = xs =>
// Returns Infinity over objects without finite
// length. This enables zip and zipWith to choose
// the shorter argument when one is non-finite,
// like cycle, repeat etc
"Node" !== xs.type ? (
"GeneratorFunction" !== xs.constructor
.constructor.name ? (
xs.length
) : Infinity
) : lengthTree(xs);
// lengthTree :: Tree a -> Int
const lengthTree = tree =>
// The count of nodes in a given tree.
foldTree(
() => xs => xs.reduce(
(a, x) => a + x, 1
)
)(tree);
// 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 || []);
// 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);
// nest :: Tree a -> [a]
const nest = tree => {
// Allowing for lazy (on-demand) evaluation.
// If the nest turns out to be a function β
// rather than a list β that function is applied
// here to the root, and returns a list.
const xs = tree.nest;
return "function" !== typeof xs ? (
xs
) : xs(root(tree));
};
// 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));
// or :: [Bool] -> Bool
const or = xs =>
xs.some(Boolean);
// repeat :: a -> Generator [a]
const repeat = function* (x) {
while (true) {
yield x;
}
};
// root :: Tree a -> a
const root = tree =>
// The value attached to a tree node.
tree.root;
// MAIN --
return main();
};
return JSON.stringify(
jxaContext()
);
})();
Download Macro(s): List of tagged items (recursive).kmmacros (17 KB)
Macro-Image
Macro-Notes
- Macros are always disabled when imported into the Keyboard Maestro Editor.
- The user must ensure the macro is enabled.
- The user must also ensure the macro's parent macro-group is enabled.
System Information
- macOS 14.3.1
- Keyboard Maestro v11.0.2