This macro is dual to (a mirror image of – moving in the opposition direction) another macro on this forum:
[Create a nested folder structure based on a plain text outline]
(Create a nested folder structure based on a plain text outline)
This one lets you:
- Select one or more folders in Finder,
- displaying, and copying to Clipboard, a plain text (tab-indented, so TaskPaper-compatible) outline of the folder sub-structure,
- either showing or hiding (depending on an option setting) the non-folder files in each directory.
Copy selected folder(s) and their descendants as a plain text outline.kmmacros (30 KB)
JS Source
(() => {
'use strict';
ObjC.import('AppKit');
// Plain text outline of selected folders and
// their descendants (copied to clipboard).
// Rob Trew @2020
// --------------------- OPTION ----------------------
// Include all visible files ?
// or just folders ?
const
foldersOnly = ['1', 'true'].includes(
Application('Keyboard Maestro Engine')
.getvariable('listFoldersOnly')
.trim()
.toLowerCase()
),
pFilter = foldersOnly ? (
fp => doesDirectoryExist(fp)
) : fp => !takeFileName(fp).startsWith('.');
// ---------------------- MAIN -----------------------
// main :: IO ()
const main = () => {
const
selectedFolders = Application(
'Finder'
).selection();
return bindLR(
undefined !== selectedFolders ? (
Right(
selectedFolders.flatMap(
x => filePathForest(
decodeURI(x.url()).slice(7)
)
)
)
) : Left('Nothing selected in Finder.')
)(
compose(
copyText,
outlineFromForest('\t')(x => {
const
xs = x.split('/'),
final = last(xs);
return Boolean(final) ? (
final
) : xs[xs.length - 2];
}),
nest,
foldTree(
x => xs => Node(x)(
sortBy(comparing(root))(xs)
)
),
Node(''),
map(filteredTree(pFilter))
)
);
};
// filePathForest :: FilePath -> [Tree FilePath]
const filePathForest = fp => {
const go = fp =>
Node(fp)(
doesDirectoryExist(fp) ? (
map(compose(go, combine(fp)))(
getDirectoryContents(fp)
)
) : []
);
return doesPathExist ? [go(fp)] : [];
};
// outlineFromForest :: String ->
// (a -> String) -> [Tree a] -> [String]
const outlineFromForest = unitIndent =>
// Indented text representation of a list of
// Trees.
// f is an (a -> String) function defining
// the string representation of tree nodes.
f => trees => {
const go = indent => x => {
const
s = indent + f(x.root),
xs = x.nest,
nextDepth = unitIndent + indent;
return 0 < xs.length ? (
[s].concat(
xs.flatMap(go(nextDepth))
)
) : s;
};
return unlines(trees.flatMap(go('')));
};
// --------------------- JXA ---------------------
// copyText :: String -> IO String
const copyText = s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// doesDirectoryExist :: FilePath -> IO Bool
const doesDirectoryExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && ref[0];
};
// doesPathExist :: FilePath -> IO Bool
const doesPathExist = fp =>
$.NSFileManager.defaultManager
.fileExistsAtPath(
$(fp).stringByStandardizingPath
);
// fileStatus :: FilePath -> Either String Dict
const fileStatus = fp => {
const
e = $(),
dct = $.NSFileManager.defaultManager
.attributesOfItemAtPathError(
ObjC.wrap(fp).stringByStandardizingPath,
e
);
return dct.isNil() ? (
Left(ObjC.unwrap(e.localizedDescription))
) : Right(ObjC.deepUnwrap(dct));
};
// ------------------- GENERIC -------------------
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: 'Node',
root: v,
nest: xs || []
});
// 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);
// combine (</>) :: FilePath -> FilePath -> FilePath
const combine = fp =>
// Two paths combined with a path separator.
// Just the second path if that starts
// with a path separator.
fp1 => Boolean(fp) && Boolean(fp1) ? (
'/' === fp1.slice(0, 1) ? (
fp1
) : '/' === fp.slice(-1) ? (
fp + fp1
) : fp + '/' + fp1
) : fp + fp1;
// 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
);
// filteredTree (a -> Bool) -> Tree a -> Tree a
const filteredTree = p =>
// A tree including only those children
// which either match the predicate p, or have
// descendants which match the predicate p.
foldTree(x => xs =>
Node(x)(xs.filter(
tree => (0 < tree.nest.length) || (
p(tree.root)
)
))
);
// 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(tree.root)(
tree.nest.map(go)
);
return go;
};
// getDirectoryContents :: FilePath -> IO [FilePath]
const getDirectoryContents = fp =>
ObjC.deepUnwrap(
$.NSFileManager.defaultManager
.contentsOfDirectoryAtPathError(
$(fp)
.stringByStandardizingPath, null
)
);
// last :: [a] -> a
const last = xs => (
// The last item of a list.
ys => 0 < ys.length ? (
ys.slice(-1)[0]
) : undefined
)(list(xs));
// 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(x));
};
// root :: Tree a -> a
const root = tree =>
tree.root;
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = f =>
xs => list(xs).slice()
.sort((a, b) => f(a)(b));
// takeFileName :: FilePath -> FilePath
const takeFileName = fp =>
'' !== fp ? (
'/' !== fp[fp.length - 1] ? (
fp.split('/').slice(-1)[0]
) : ''
) : '';
// 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');
// MAIN ---
return main();
})();