@FutureNathan do you use Keyboard Maestro, FastScripts 3, or Alfred - Productivity App for macOS ?
I’ve sketched rough drafts of a pair of experimental scripts:
{Save fold state, restore fold state}
(just for testing with dummy files, for the moment, and we might need to ask @jessegrosjean, when he gets back, whether it’s sensible to be storing custom attributes in the Bike file itself, or whether we should really be keeping the state elsewhere)
Experimental drafts for testing with dummy data only:
SaveRestoreBikeFoldFocus.kmmacros.zip (4.9 KB)
Expand disclosure triangle to view script: Save Fold and Focus State
(() => {
"use strict";
// Save fold state of front document as custom attribute
// custom.foldState :: JSON Array of folded row ids
// Rob Trew @2022
// EXPERIMENTAL DRAFT – USE WITH DUMMY DATA ONLY
// Ver 0.02
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (() => {
const
fp = `${doc.file()}`,
collapsedIdsJSON = JSON.stringify(
doc.rows.where({
collapsed: true
}).id()
),
maybeFocusedRow = doc.focusedRow(),
kvs = [
[
"custom.foldState",
`'${collapsedIdsJSON}'`
],
[
"custom.focusRow",
Boolean(maybeFocusedRow) ? (
maybeFocusedRow.id()
) : "''"
]
];
return either(
alert("Save fold state")
)(x => x)(
fileAttrsSetLR(filePath(fp))(kvs)
);
})() : "No documents open in Bike";
};
// fileAttrSetLR :: FilePath ->
// [(String, String)] -> Either String Dict
const fileAttrsSetLR = fp =>
kvs => {
const fpFull = filePath(fp);
return bindLR(
doesFileExist(fpFull) ? (
Right(fpFull)
) : Left(`File not found: ${fpFull}`)
)(fpFound => {
try {
Object.assign(
Application.currentApplication(),
{includeStandardAdditions: true}
)
.doShellScript(
kvs.map(
([k, v]) =>
`xattr -w ${k} ${v} "${fpFound}"`
).join("\n")
);
return Right(
[
kvs.map(
([k, v]) => `${k}:${v}`
).join("\n"),
fpFound
].join("\n")
);
} catch (e) {
return Left(e.message);
}
});
};
// ----------------------- 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
);
};
// 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
);
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && !ref[0];
};
// --------------------- 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 :: IO()
return main();
})();
Expand disclosure triangle to view script: Restore Fold and Focus
(() => {
"use strict";
// Restore fold and focus state of front document
// from custom file attributes
// custom.foldState :: JSON Array of any folded row ids
// custom.focusRow :: id of focus row (if any)
// Rob Trew @2022
// EXPERIMENTAL DRAFT – USE WITH DUMMY DATA ONLY
// Ver 0.03
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (
either(
alert("Restore fold and focus state")
)(
restoredFoldsAndFocus(doc)
)(
fileAttrsReadLR(
filePath(`${doc.file()}`)
)([
"custom.foldState",
"custom.focusRow"
])
)
) : "No documents open in Bike";
};
// ----------------------- 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
) && !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
);
// fileAttrsReadLR :: FilePath -> String -> Maybe Dict
const fileAttrsReadLR = fp =>
ks => {
const fpFull = filePath(fp);
return "null" !== fpFull ? (
bindLR(
doesFileExist(fpFull) ? (
Right(fpFull)
) : Left(`File not found: ${fpFull}`)
)(fpFound => {
try {
return Right(
zip(ks)(lines(
Object.assign(
Application.currentApplication(),
{includeStandardAdditions: true}
)
.doShellScript(
ks.map(
k => `xattr -p ${k} "${fpFound}"`
).join("\n")
)
))
.reduce(
(a, [k, v]) => Object.assign(
{[k]: v},
a
),
{}
)
);
// );
} catch (e) {
return Left(e.message);
}
})
) : Left("Front document not saved.");
};
// restoredFoldsAndFocus ::
// Bike Doc Dict -> IO String
const restoredFoldsAndFocus = doc =>
dict => {
const
foldedIDs = either(() => [])(x => x)(
jsonParseLR(dict["custom.foldState"])
),
focusRowId = dict["custom.focusRow"] || "",
rows = doc.rows,
focusMessage = Boolean(focusRowId) ? (
restoreFocus(doc)(focusRowId)
) : "",
foldMessage = (() => {
rows.where({containsRows: true})()
.forEach(x => x.expand());
return foldedIDs.flatMap(k => {
const row = rows.byId(k);
return row.exists() ? [(
row.collapse(),
row.name()
)] : [];
}).join();
})();
return [
`Restored folds:\n${foldMessage}`,
focusMessage
].join("\n");
};
// restoreFocus :: Bike Rows -> String -> Bike IO ()
const restoreFocus = doc =>
rowID => {
const row = doc.rows.byId(rowID);
return row.exists() ? (
doc.focusedRow = row,
`Focus restored: '${row.name()}'`
) : `Focus row not found: ${rowID}`;
};
// --------------------- 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);
// jsonParseLR :: String -> Either String a
const jsonParseLR = s => {
try {
return Right(JSON.parse(s));
} catch (e) {
return Left(
[
e.message,
`(line:${e.line} col:${e.column})`
].join("\n")
);
}
};
// lines :: String -> [String]
const lines = s =>
// A list of strings derived from a single string
// which is delimited by \n or by \r\n or \r.
Boolean(s.length) ? (
s.split(/\r\n|\n|\r/u)
) : [];
// zip :: [a] -> [b] -> [(a, b)]
const zip = xs =>
// The paired members of xs and ys, up to
// the length of the shorter of the two lists.
ys => Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => [xs[i], ys[i]]);
// showLog :: a -> IO ()
const showLog = (...args) =>
// eslint-disable-next-line no-console
console.log(
args
.map(JSON.stringify)
.join(" -> ")
);
// main :: IO()
return main();
})();