Skip to content

Instantly share code, notes, and snippets.

@chhoumann
Created November 14, 2025 11:28
Show Gist options
  • Select an option

  • Save chhoumann/8d37578e6624f8a325db74667dd2bb13 to your computer and use it in GitHub Desktop.

Select an option

Save chhoumann/8d37578e6624f8a325db74667dd2bb13 to your computer and use it in GitHub Desktop.
/**
* QuickAdd user script that recreates the core commands from the
* luhman-obsidian-plugin without requiring the original plugin.
*
* Exports individual helpers (`::createChildZettel`, `::insertZettelLink`, ...)
* and a default `entry` function that lets you pick an action via a suggester.
*/
const CHECK_SETTINGS_MESSAGE = "Try adjusting the script settings if this seems wrong.";
const idOnlyRegex = /([0-9]+|[a-z]+)/g;
const lettersIDComponentSuccessors = {
a: "b",
b: "c",
c: "d",
d: "e",
e: "f",
f: "g",
g: "h",
h: "i",
i: "j",
j: "k",
k: "l",
l: "m",
m: "n",
n: "o",
o: "p",
p: "q",
q: "r",
r: "s",
s: "t",
t: "u",
u: "v",
v: "w",
w: "x",
x: "y",
y: "z",
z: "aa",
};
const DEFAULT_SETTINGS = {
matchRule: "strict",
separator: "⁝ ",
addTitle: false,
addAlias: false,
useLinkAlias: false,
templateFile: "",
templateRequireTitle: true,
templateRequireLink: true,
};
const OPTION_KEYS = {
matchRule: "ID Matching Rule",
separator: "ID Separator",
addTitle: "Append Title To Filename",
addAlias: "Add Title Alias To Frontmatter",
useLinkAlias: "Use Title Alias In Inserted Links",
templateFile: "Template File Path",
templateRequireTitle: "Template Requires {{title}}",
templateRequireLink: "Template Requires {{link}}",
};
const ACTIONS = [
{
key: "createSibling",
label: "Create sibling zettel",
runner: (engine) => engine.createSibling(true),
},
{
key: "createSiblingSilent",
label: "Create sibling zettel (stay on current note)",
runner: (engine) => engine.createSibling(false),
},
{
key: "createChild",
label: "Create child zettel",
runner: (engine) => engine.createChild(true),
},
{
key: "createChildSilent",
label: "Create child zettel (stay on current note)",
runner: (engine) => engine.createChild(false),
},
{
key: "insertZettelLink",
label: "Insert link to existing zettel",
runner: (engine) => engine.insertZettelLink(),
},
{
key: "openZettelByTitle",
label: "Open zettel by markdown title",
runner: (engine) => engine.openZettelByTitle(),
},
{
key: "openParentZettel",
label: "Open parent zettel",
runner: (engine) => engine.openParentZettel(),
},
{
key: "outdentZettel",
label: "Outdent current zettel",
runner: (engine) => engine.outdentActiveZettel(),
},
];
const ACTION_LOOKUP = Object.fromEntries(
ACTIONS.map((action) => [action.key, action.runner])
);
const script = {
entry: async (params, settings = {}) => {
const engine = new LuhmannQuickAddEngine(params, settings);
const labels = ACTIONS.map((action) => action.label);
const keys = ACTIONS.map((action) => action.key);
const selection = await params.quickAddApi.suggester(
labels,
keys,
"Select a Luhmann action"
);
if (!selection) return;
await ACTION_LOOKUP[selection](engine);
},
settings: {
name: "Luhmann QuickAdd Toolkit",
author: "QuickAdd",
options: {
[OPTION_KEYS.matchRule]: {
type: "dropdown",
options: ["strict", "separator", "fuzzy"],
defaultValue: DEFAULT_SETTINGS.matchRule,
description:
"Strict = filename is only the ID. Separator = enforce the separator after the ID. Fuzzy = stop at the first non-alphanumeric character.",
},
[OPTION_KEYS.separator]: {
type: "text",
defaultValue: DEFAULT_SETTINGS.separator,
description: "Used between the ID and the title when titles are appended.",
},
[OPTION_KEYS.addTitle]: {
type: "toggle",
defaultValue: DEFAULT_SETTINGS.addTitle,
description: "Append the title after the ID when creating files.",
},
[OPTION_KEYS.addAlias]: {
type: "toggle",
defaultValue: DEFAULT_SETTINGS.addAlias,
description: "Add the created title to the note's frontmatter aliases.",
},
[OPTION_KEYS.useLinkAlias]: {
type: "toggle",
defaultValue: DEFAULT_SETTINGS.useLinkAlias,
description: "Use the title as an alias in links that get inserted into the current note.",
},
[OPTION_KEYS.templateFile]: {
type: "text",
defaultValue: DEFAULT_SETTINGS.templateFile,
description: "Optional vault path to a template containing {{title}} and/or {{link}} placeholders.",
},
[OPTION_KEYS.templateRequireTitle]: {
type: "toggle",
defaultValue: DEFAULT_SETTINGS.templateRequireTitle,
description: "Require {{title}} to exist inside the template file before running.",
},
[OPTION_KEYS.templateRequireLink]: {
type: "toggle",
defaultValue: DEFAULT_SETTINGS.templateRequireLink,
description: "Require {{link}} to exist inside the template file before running.",
},
},
},
};
const ACTION_EXPORTS = [
{ exportName: "createSiblingZettel", key: "createSibling" },
{ exportName: "createSiblingZettelNoOpen", key: "createSiblingSilent" },
{ exportName: "createChildZettel", key: "createChild" },
{ exportName: "createChildZettelNoOpen", key: "createChildSilent" },
{ exportName: "insertZettelLink", key: "insertZettelLink" },
{ exportName: "openZettel", key: "openZettelByTitle" },
{ exportName: "openParentZettel", key: "openParentZettel" },
{ exportName: "outdentZettel", key: "outdentZettel" },
];
for (const { exportName, key } of ACTION_EXPORTS) {
script[exportName] = createRunnerForAction(key);
}
module.exports = script;
function createRunnerForAction(actionKey) {
const runner = ACTION_LOOKUP[actionKey];
return async (params, settings = {}) => {
const engine = new LuhmannQuickAddEngine(params, settings);
return await runner(engine);
};
}
class LuhmannQuickAddEngine {
constructor(params, rawSettings = {}) {
this.app = params.app;
this.params = params;
this.quickAddApi = params.quickAddApi;
this.obsidian = params.obsidian || globalThis.obsidian || {};
this.MarkdownView = this.obsidian.MarkdownView;
this.Notice = this.obsidian.Notice || globalThis.Notice || FakeNotice;
this.settings = normalizeSettings(rawSettings);
}
notice(message, duration = 5000) {
try {
new this.Notice(`[Luhmann QuickAdd] ${message}`, duration);
} catch (error) {
console.log(`[Luhmann QuickAdd] ${message}`);
}
}
currentFile() {
return this.app.workspace.getActiveFile();
}
getEditor() {
if (this.MarkdownView) {
return this.app.workspace.getActiveViewOfType(this.MarkdownView)?.editor;
}
const leaf = this.app.workspace.getActiveViewOfType?.("markdown");
return leaf?.editor;
}
isZettelFile(name) {
const match = /(.*)\.md$/.exec(name);
return Boolean(match && this.fileToId(match[1]));
}
fileToId(basename) {
const separator = escapeRegExp(this.settings.separator || "");
const ruleRegexes = {
strict: /^((?:[0-9]+|[a-z]+)+)$/,
separator: new RegExp(`^((?:[0-9]+|[a-z]+)+)${separator}.*`),
fuzzy: /^((?:[0-9]+|[a-z]+)+).*/,
};
const rule = ruleRegexes[this.settings.matchRule] || ruleRegexes.strict;
const match = basename.match(rule);
return match ? match[1] : "";
}
idExists(id) {
return this.app.vault
.getMarkdownFiles()
.some((file) => this.fileToId(file.basename) === id);
}
firstAvailableID(startingID) {
let next = startingID;
while (this.idExists(next)) {
next = this.incrementID(next);
}
return next;
}
incrementID(id) {
const parts = id.match(idOnlyRegex);
if (!parts || parts.length === 0) return id;
const last = parts.pop();
return parts.concat([this.incrementIDComponent(last)]).join("");
}
incrementIDComponent(idComponent) {
if (/^\d+$/.test(idComponent)) {
return (parseInt(idComponent, 10) + 1).toString();
}
return this.incrementStringIDComponent(idComponent);
}
incrementStringIDComponent(component) {
const bits = component.split("");
const last = bits.pop();
const next = lettersIDComponentSuccessors[last];
if (!next) {
return component + "a";
}
return bits.concat([next]).join("");
}
parentID(id) {
const parts = id.match(idOnlyRegex);
if (!parts || parts.length === 0) return "";
parts.pop();
return parts.join("");
}
nextComponentOf(id) {
const parts = id.match(idOnlyRegex);
if (!parts || parts.length === 0) return "1";
const last = parts.pop();
return /^\d+$/.test(last) ? "a" : "1";
}
firstChildOf(parentID) {
return parentID + this.nextComponentOf(parentID);
}
makeNoteForNextSiblingOf(file) {
return this.firstAvailableID(
this.incrementID(this.fileToId(file.basename))
);
}
makeNoteForNextChildOf(file) {
return this.firstAvailableID(
this.firstChildOf(this.fileToId(file.basename))
);
}
async createSibling(openNewFile) {
return this.createLinkedZettel({
openNewFile,
idFactory: (file) => this.makeNoteForNextSiblingOf(file),
});
}
async createChild(openNewFile) {
return this.createLinkedZettel({
openNewFile,
idFactory: (file) => this.makeNoteForNextChildOf(file),
});
}
async createLinkedZettel({ openNewFile, idFactory }) {
const file = this.currentFile();
if (!file) {
this.notice("No active file.");
return;
}
if (!this.isZettelFile(file.name)) {
this.notice(`Couldn't find an ID in "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`);
return;
}
const editor = this.getEditor();
if (!editor) {
this.notice("Open a Markdown editor pane before running this script.");
return;
}
const nextID = idFactory(file);
const parentLink = `[[${file.basename}]]`;
const selectionText = editor.getSelection();
let replacementRange = null;
let spacesBefore = 0;
let spacesAfter = 0;
let title = "";
if (selectionText) {
const info = this.extractSelectionInfo(selectionText);
title = info.title;
spacesBefore = info.spacesBefore;
spacesAfter = info.spacesAfter;
replacementRange = this.getOrderedSelectionRange(editor);
} else {
title = await this.promptForTitle();
}
const filename = this.buildFilename(nextID, title);
const folder = this.app.fileManager.getNewFileParent(file.path).path;
const path = joinPath(folder, `${filename}.md`);
const linkText = this.buildLinkText(nextID, title);
const successCallback = replacementRange
? () => {
const prefix = " ".repeat(spacesBefore);
const suffix = " ".repeat(spacesAfter);
editor.replaceRange(
`${prefix}${linkText}${suffix}`,
replacementRange.from,
replacementRange.to
);
}
: () => this.insertLinkAtCursor(editor, linkText);
await this.makeNote({
path,
title,
fileLink: parentLink,
placeCursorAtStart: true,
openZettel: openNewFile,
successCallback,
});
}
async insertZettelLink() {
const entries = await this.getZettelTitleEntries();
if (entries.length === 0) {
this.notice("No zettels found.");
return;
}
const display = entries.map(
(entry) => `${entry.title} (${entry.file.basename})`
);
const selected = await this.quickAddApi.suggester(
display,
entries,
"Insert which zettel?"
);
if (!selected) return;
const editor = this.getEditor();
if (!editor) {
this.notice("Open a Markdown editor pane before inserting links.");
return;
}
const text = `[[${selected.file.basename}]]`;
this.insertLinkAtCursor(editor, text);
}
async openZettelByTitle() {
const entries = await this.getZettelTitleEntries();
if (entries.length === 0) {
this.notice("No zettels available to open.");
return;
}
const display = entries.map(
(entry) => `${entry.title} (${entry.file.basename})`
);
const selected = await this.quickAddApi.suggester(
display,
entries,
"Open which zettel?"
);
if (!selected) return;
await this.app.workspace.getLeaf().openFile(selected.file);
}
async openParentZettel() {
const file = this.currentFile();
if (!file) {
this.notice("No active file.");
return;
}
const id = this.fileToId(file.basename);
const parentId = this.parentID(id);
if (!parentId) {
this.notice(`No parent found for "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`);
return;
}
const parentFile = this.findZettelById(parentId);
if (!parentFile) {
this.notice(`Couldn't find file for ID ${parentId}. ${CHECK_SETTINGS_MESSAGE}`);
return;
}
await this.app.workspace.getLeaf().openFile(parentFile);
}
async outdentActiveZettel() {
const file = this.currentFile();
if (!file) {
this.notice("No active file.");
return;
}
const id = this.fileToId(file.basename);
if (!id) {
this.notice(`Couldn't read the ID from "${file.basename}". ${CHECK_SETTINGS_MESSAGE}`);
return;
}
await this.outdentZettel(id);
}
findZettelById(id) {
return this.app.vault
.getMarkdownFiles()
.find((file) => this.fileToId(file.basename) === id);
}
async renameZettel(id, toId) {
const target = this.findZettelById(id);
if (!target) {
this.notice(`Couldn't find file for ID ${id}. ${CHECK_SETTINGS_MESSAGE}`);
return;
}
const rest = target.basename.replace(this.fileToId(target.basename), "");
const dir = target.parent?.path || "";
const nextPath = joinPath(dir, `${toId}${rest}.${target.extension}`);
await this.app.fileManager.renameFile(target, nextPath);
}
async moveChildrenDown(id) {
const children = this.getDirectChildZettels(id);
for (const child of children) {
await this.moveZettelDown(this.fileToId(child.basename));
}
}
async moveZettelDown(id) {
await this.moveChildrenDown(id);
await this.renameZettel(id, this.firstAvailableID(id));
}
async outdentZettel(id) {
const newID = this.incrementID(this.parentID(id));
if (this.idExists(newID)) {
await this.moveZettelDown(newID);
}
const childZettels = this.getDirectChildZettels(id);
for (const child of childZettels) {
const childID = this.firstAvailableID(this.firstChildOf(newID));
await this.renameZettel(this.fileToId(child.basename), childID);
}
await this.renameZettel(id, newID);
}
getZettels() {
return this.app.vault.getMarkdownFiles().filter((file) => {
const shouldIgnore = /^(_layouts|templates|scripts)/.test(file.path);
return !shouldIgnore && this.fileToId(file.basename) !== "";
});
}
getDirectChildZettels(parentId) {
return this.getZettels().filter((file) => {
return this.parentID(this.fileToId(file.basename)) === parentId;
});
}
async getZettelTitleEntries() {
const regex = /^#\s+(.+)$/m;
const entries = [];
for (const file of this.getZettels()) {
const text = await this.app.vault.cachedRead(file);
const match = text.match(regex);
entries.push({
title: match ? match[1].trim() : file.basename,
file,
});
}
return entries.sort((a, b) => a.title.localeCompare(b.title));
}
buildFilename(id, title) {
if (this.settings.addTitle && title) {
return `${id}${this.settings.separator}${title}`;
}
return id;
}
buildLinkText(id, title) {
const alias = this.settings.useLinkAlias && title ? `|${title}` : "";
const suffix = this.settings.addTitle && title ? `${this.settings.separator}${title}` : "";
return `[[${id}${suffix}${alias}]]`;
}
insertLinkAtCursor(editor, text) {
const cursor = editor.getCursor();
editor.replaceRange(text, cursor, cursor);
}
getOrderedSelectionRange(editor) {
const selection = editor.listSelections?.()[0];
if (!selection) return null;
const { anchor, head } = selection;
const anchorBeforeHead =
anchor.line < head.line ||
(anchor.line === head.line && anchor.ch <= head.ch);
return anchorBeforeHead
? { from: anchor, to: head }
: { from: head, to: anchor };
}
extractSelectionInfo(selectionText) {
const trimStart = selectionText.trimStart();
const trimBoth = trimStart.trimEnd();
const spacesBefore = selectionText.length - trimStart.length;
const spacesAfter = trimStart.length - trimBoth.length;
return {
spacesBefore,
spacesAfter,
title: toTitleCase(trimBoth),
};
}
async promptForTitle() {
try {
const value = await this.quickAddApi.inputPrompt(
"Zettel title",
"Title (optional)",
""
);
return value || "";
} catch (error) {
throw error;
}
}
async makeNote({
path,
title,
fileLink,
placeCursorAtStart,
openZettel,
successCallback,
}) {
const useTemplate = Boolean(this.settings.templateFile.trim());
const headingText = title
? (useTemplate ? title.trimStart() : `# ${title.trimStart()}`)
: "";
const backlinkRegex = /{{link}}/g;
const titleRegex = /{{title}}/g;
let file;
if (useTemplate) {
const templatePath = this.settings.templateFile.trim();
let templateContent = "";
try {
templateContent = await this.app.vault.adapter.read(templatePath);
} catch (error) {
this.notice(
`Couldn't read template file at "${templatePath}". Check the script settings.`,
15000
);
return;
}
const requiresTitle = this.settings.templateRequireTitle;
const requiresLink = this.settings.templateRequireLink;
if (
(requiresTitle && !titleRegex.test(templateContent)) ||
(requiresLink && !backlinkRegex.test(templateContent))
) {
this.notice(
"Template is missing required {{title}} and/or {{link}} placeholders.",
15000
);
return;
}
const content = templateContent
.replace(titleRegex, headingText)
.replace(backlinkRegex, fileLink || "");
file = await this.app.vault.create(path, content);
} else {
const fullContent = `${headingText}\n\n${fileLink || ""}`;
file = await this.app.vault.create(path, fullContent);
}
if (typeof successCallback === "function") {
await successCallback();
}
if (this.settings.addAlias && file && title) {
await this.app.fileManager.processFrontMatter(file, (frontmatter = {}) => {
const aliases = Array.isArray(frontmatter.aliases)
? frontmatter.aliases
: frontmatter.aliases
? [frontmatter.aliases]
: [];
if (!aliases.includes(title)) {
aliases.push(title);
}
frontmatter.aliases = aliases;
return frontmatter;
});
}
if (!openZettel) return;
const leaf = this.app.workspace.getLeaf();
if (!leaf) return;
await leaf.openFile(file);
if (placeCursorAtStart && !useTemplate) {
const editor = this.getEditor();
if (!editor) return;
let line = 2;
if (this.settings.addAlias) {
line += 4;
}
editor.setCursor({ line, ch: 0 });
} else {
const editor = this.getEditor();
if (editor?.exec) {
editor.exec("goEnd");
} else if (editor) {
const lastLine = editor.lastLine ? editor.lastLine() : editor.lineCount?.() - 1 || 0;
const lastCh = editor.getLine ? editor.getLine(lastLine).length : 0;
editor.setCursor({ line: lastLine, ch: lastCh });
}
}
}
}
function normalizeSettings(settings = {}) {
return {
matchRule: settings[OPTION_KEYS.matchRule] || DEFAULT_SETTINGS.matchRule,
separator: settings[OPTION_KEYS.separator] ?? DEFAULT_SETTINGS.separator,
addTitle: Boolean(
settings[OPTION_KEYS.addTitle] ?? DEFAULT_SETTINGS.addTitle
),
addAlias: Boolean(
settings[OPTION_KEYS.addAlias] ?? DEFAULT_SETTINGS.addAlias
),
useLinkAlias: Boolean(
settings[OPTION_KEYS.useLinkAlias] ?? DEFAULT_SETTINGS.useLinkAlias
),
templateFile:
(settings[OPTION_KEYS.templateFile] ?? DEFAULT_SETTINGS.templateFile).trim(),
templateRequireTitle: Boolean(
settings[OPTION_KEYS.templateRequireTitle] ??
DEFAULT_SETTINGS.templateRequireTitle
),
templateRequireLink: Boolean(
settings[OPTION_KEYS.templateRequireLink] ??
DEFAULT_SETTINGS.templateRequireLink
),
};
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function toTitleCase(text) {
if (!text) return "";
return text
.split(/\s+/)
.filter(Boolean)
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
}
function joinPath(folder, filename) {
if (!folder) return filename;
return folder.endsWith("/") ? `${folder}${filename}` : `${folder}/${filename}`;
}
class FakeNotice {
constructor(message) {
console.log(message);
}
}
title
Macro: Luhmann QuickAdd Toolkit

Need Luhmann-style IDs without installing a second plugin? The Luhmann QuickAdd Toolkit user script recreates the flagship commands from the luhman-obsidian-plugin and lets you run them straight from QuickAdd macros.

📍 Script file: docs/static/scripts/luhmannQuickAdd.js

What you get

  • ✅ Create sibling or child zettels that inherit the parent ID chain (23b23c, 23b123b1a, etc.).
  • ✅ Automatically insert links back into the source note, with optional alias text.
  • ✅ Jump to zettels by their markdown title, insert links anywhere, open the parent, or outdent an entire branch.
  • ✅ Template + alias support that mirrors the original plugin settings.
  • ✅ Multiple exports so your macro can call exactly the action you need (or show a friendly chooser).

1. Install the user script

  1. Copy docs/static/scripts/luhmannQuickAdd.js into your vault (e.g. .obsidian/scripts/).
  2. In Obsidian, open Settings → QuickAdd → Macros → Manage Macros.
  3. Add a new macro (e.g. Luhmann Toolkit).
  4. Inside the macro, choose Add Command → User Script, point to the copied file, and click Add.

🪄 Want individual commands? Append ::exportName when selecting the script (examples below).

2. Configure settings

When you click the gear next to the user script entry, you'll see the same options as the original plugin:

Setting What it does
ID Matching Rule strict, separator, or fuzzy – defines how IDs are read from filenames.
ID Separator String inserted between ID and title (default ).
Append Title To Filename Adds the typed title after the ID when creating notes.
Add Title Alias To Frontmatter Drops the title into aliases in frontmatter.
Use Title Alias In Inserted Links Adds `
Template File Path Optional vault path that contains {{title}}/{{link}} placeholders.
Template Requires {{title}} / {{link}} Toggle whether the template must include those tags.

All defaults mirror the upstream plugin, so you can start with no changes.

3. Wire up commands

You have two ways to trigger actions:

A. Menu-driven entry (default export)

  • Add the script without ::something.
  • When the macro runs, QuickAdd shows a suggester listing every Luhmann action (create sibling/child, insert link, open parent, outdent, etc.).

B. Direct exports (call specific behavior)

  • Append the export name to the script path when adding it to the macro, e.g. luhmannQuickAdd.js::createChildZettel.
  • Available exports:
    • createChildZettel
    • createChildZettelNoOpen
    • createSiblingZettel
    • createSiblingZettelNoOpen
    • insertZettelLink
    • openZettel
    • openParentZettel
    • outdentZettel

Mix and match—many users add separate macro steps for child/sibling creation so they can stick the commands on different hotkeys.

4. Create your first zettel

  1. Manually rename any existing note to a valid Luhmann ID (example: 1.md or 1⁝ Seed Note.md). This becomes your seed.
  2. Open that seed note in the editor.
  3. Run the macro:
    • Child notecreateChildZettel generates 1a (prompting for a title if nothing is selected) and links back to the seed.
    • Sibling notecreateSiblingZettel generates 2, 3, … (or 1b, 1c if you're on a letter branch).
  4. Keep going—the script automatically skips IDs that already exist, honors templates, and places the cursor at the start of the new note.

Selecting text = instant title

Highlight a phrase before running a create command to auto-title the new file and replace the selection with a link to the child/sibling. Spaces around the selection are preserved.

5. Everyday moves

  • Insert zettel link anywhere: run insertZettelLink to search all zettels by their H1 title and drop a wiki link at the cursor.
  • Open by title: openZettel gives you the same search UI but opens the file instead of inserting a link.
  • Jump to parent: openParentZettel reads the current file's ID, removes the last segment, and opens that parent (with a helpful notice if none exists).
  • Outdent a branch: outdentZettel promotes the current note to the next available sibling layer, shuffling children along with it so the tree stays valid.

6. Troubleshooting tips

  • “Couldn't find ID in…” notice → Double-check your filename matches the selected matching rule. Strict mode requires the entire basename to be the ID.
  • Template errors → If you enable “Template Requires {{title}}” and/or {{link}}, make sure those tokens appear somewhere in the referenced file.
  • Links look odd → Adjust the separator or disable “Use Title Alias In Inserted Links” if you prefer plain [[123a]] wiki links.
  • Need different folders? → QuickAdd respects Obsidian’s “New file location” setting; move the active note first or rely on app.fileManager.getNewFileParent(...) to drop children beside their parent.

7. Suggested macro layouts

  1. Single chooser (fast setup)

    • Macro ➜ User script (no export) ➜ assign a hotkey ➜ instant menu for every action.
  2. Dedicated commands (muscle memory)

    • Macro step 1: luhmannQuickAdd.js::createChildZettel
    • Macro step 2: luhmannQuickAdd.js::createSiblingZettel
    • Macro step 3: luhmannQuickAdd.js::insertZettelLink
    • Attach global hotkeys or add each macro step to QuickAdd’s main menu for one-click access.

Have fun building your slip-box! If you hit snags, share your macro config plus the script settings so others can help replicate the setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment