···11+// follow-or-create.js — obsidian-vimrc-support jsfile command
22+// Args from plugin: editor, view, selection
33+// Executed via Function() constructor — return value is NOT awaited.
44+55+(async function() {
66+ try {
77+ var app = view.app;
88+ var activeFile = view.file;
99+ var sourcePath = activeFile ? activeFile.path : "";
1010+ var cur = editor.getCursor();
1111+ var line = editor.getLine(cur.line);
1212+1313+ var text = "";
1414+ var rangeFrom = null;
1515+ var rangeTo = null;
1616+ var insideWikilink = false;
1717+1818+ // --- Check if cursor is inside a [[wikilink]] ---
1919+ var before = line.substring(0, cur.ch);
2020+ var after = line.substring(cur.ch);
2121+ var openIdx = before.lastIndexOf("[[");
2222+ var closeInBefore = before.lastIndexOf("]]");
2323+ var closeIdx = after.indexOf("]]");
2424+2525+ if (openIdx !== -1 && (closeInBefore === -1 || closeInBefore < openIdx) && closeIdx !== -1) {
2626+ insideWikilink = true;
2727+ var fullLink = line.substring(openIdx + 2, cur.ch + closeIdx);
2828+ text = fullLink.split("|")[0].trim();
2929+ rangeFrom = { line: cur.line, ch: openIdx };
3030+ rangeTo = { line: cur.line, ch: cur.ch + closeIdx + 2 };
3131+ }
3232+3333+ // --- Visual mode: use the `selection` parameter from the plugin ---
3434+ // The ':' keystroke collapses the editor selection before the ex-command
3535+ // runs, so editor.getSelection() returns "". However, the plugin captures
3636+ // the selection on cursorActivity (before collapse) and passes it as the
3737+ // `selection` argument. We use it when anchor !== head.
3838+ if (!insideWikilink && !text && selection && selection.anchor && selection.head) {
3939+ var a = selection.anchor;
4040+ var h = selection.head;
4141+ if (a.line !== h.line || a.ch !== h.ch) {
4242+ if (a.line < h.line || (a.line === h.line && a.ch <= h.ch)) {
4343+ rangeFrom = { line: a.line, ch: a.ch };
4444+ rangeTo = { line: h.line, ch: h.ch };
4545+ } else {
4646+ rangeFrom = { line: h.line, ch: h.ch };
4747+ rangeTo = { line: a.line, ch: a.ch };
4848+ }
4949+ text = editor.getRange(rangeFrom, rangeTo).trim();
5050+ }
5151+ }
5252+5353+ // --- Normal mode: word under cursor ---
5454+ if (!insideWikilink && !text) {
5555+ var isWordChar = function(c) {
5656+ if (!c) return false;
5757+ if (c === "'" || c === "\u2019") return true; // straight and curly apostrophe
5858+ return /[\p{L}\p{N}_\-]/u.test(c);
5959+ };
6060+ var start = cur.ch;
6161+ var end = cur.ch;
6262+ while (start > 0 && isWordChar(line[start - 1])) start--;
6363+ while (end < line.length && isWordChar(line[end])) end++;
6464+ text = line.substring(start, end);
6565+ rangeFrom = { line: cur.line, ch: start };
6666+ rangeTo = { line: cur.line, ch: end };
6767+ }
6868+6969+ if (!text) return;
7070+7171+ // --- If inside a wikilink, just open it in a new tab ---
7272+ if (insideWikilink) {
7373+ await app.workspace.openLinkText(text, sourcePath, "tab");
7474+ return;
7575+ }
7676+7777+ // --- Sanitize text for use in [[wikilink|alias]] syntax ---
7878+ // Pipe and closing brackets would break the link.
7979+ var safeAlias = text.replace(/\|/g, "-").replace(/\]\]/g, ")");
8080+8181+ // --- Search for existing note by link path OR resolved title ---
8282+ var foundFile = null;
8383+8484+ // First: try standard link-path resolution (handles basenames, paths, etc.)
8585+ foundFile = app.metadataCache.getFirstLinkpathDest(text, sourcePath);
8686+8787+ // Second: use obsidian-front-matter-title plugin's resolver if available.
8888+ // This respects the plugin's configured template (e.g. "title", "foo.bar")
8989+ // so we don't hardcode which frontmatter key holds the display name.
9090+ // Falls back to raw frontmatter.title if the plugin isn't installed.
9191+ if (!foundFile) {
9292+ var fmtPlugin = app.plugins.getPlugin("obsidian-front-matter-title-plugin");
9393+ var resolver = null;
9494+ if (fmtPlugin && fmtPlugin.getDefer) {
9595+ var defer = fmtPlugin.getDefer();
9696+ if (defer && defer.isPluginReady && defer.isPluginReady()) {
9797+ var api = defer.getApi();
9898+ if (api) {
9999+ var factory = api.getResolverFactory();
100100+ if (factory) {
101101+ resolver = factory.createResolver("explorer");
102102+ }
103103+ }
104104+ }
105105+ }
106106+107107+ var allFiles = app.vault.getMarkdownFiles();
108108+ var lowerText = text.toLowerCase();
109109+ for (var i = 0; i < allFiles.length; i++) {
110110+ var resolved = null;
111111+ if (resolver) {
112112+ resolved = resolver.resolve(allFiles[i].path);
113113+ }
114114+ if (!resolved) {
115115+ var cache = app.metadataCache.getFileCache(allFiles[i]);
116116+ resolved = cache && cache.frontmatter && cache.frontmatter.title
117117+ ? String(cache.frontmatter.title)
118118+ : null;
119119+ }
120120+ if (resolved && resolved.toLowerCase() === lowerText) {
121121+ foundFile = allFiles[i];
122122+ break;
123123+ }
124124+ }
125125+ }
126126+127127+ if (foundFile) {
128128+ var linkStr = foundFile.basename === text
129129+ ? "[[" + text + "]]"
130130+ : "[[" + foundFile.basename + "|" + safeAlias + "]]";
131131+ editor.replaceRange(linkStr, rangeFrom, rangeTo);
132132+ await app.workspace.openLinkText(foundFile.path, sourcePath, "tab");
133133+ return;
134134+ }
135135+136136+ // --- Create a new note with zk-prefixer style ID ---
137137+ var now = new Date();
138138+ var pad = function(n, w) { return String(n).padStart(w, "0"); };
139139+ var yy = pad(now.getFullYear() % 100, 2);
140140+ var mm = pad(now.getMonth() + 1, 2);
141141+ var dd = pad(now.getDate(), 2);
142142+ var hh = pad(now.getHours(), 2);
143143+ var mi = pad(now.getMinutes(), 2);
144144+ var zkId = yy + mm + dd + "-" + hh + mi;
145145+ var fileName = zkId + ".md";
146146+147147+ // Handle collision: if file with same zkId exists, append a letter suffix
148148+ var suffix = "";
149149+ var alphabet = "abcdefghijklmnopqrstuvwxyz";
150150+ while (app.vault.getAbstractFileByPath(fileName)) {
151151+ if (suffix === "") {
152152+ suffix = "a";
153153+ } else {
154154+ var idx = alphabet.indexOf(suffix);
155155+ if (idx >= alphabet.length - 1) {
156156+ throw new Error("Too many notes created this minute (" + zkId + "a-z exhausted)");
157157+ }
158158+ suffix = alphabet[idx + 1];
159159+ }
160160+ fileName = zkId + suffix + ".md";
161161+ }
162162+ var noteId = suffix ? zkId + suffix : zkId;
163163+164164+ // Sanitize title for YAML: backslashes first, then double quotes, then newlines
165165+ var safeTitle = text
166166+ .replace(/\\/g, "\\\\")
167167+ .replace(/"/g, '\\"')
168168+ .replace(/[\r\n]+/g, " ");
169169+170170+ var frontmatter = [
171171+ "---",
172172+ 'title: "' + safeTitle + '"',
173173+ "tags:",
174174+ 'date: "' + zkId + '"',
175175+ "update:",
176176+ "---",
177177+ "",
178178+ ].join("\n");
179179+180180+ // Do editor replacement BEFORE async vault operation to avoid stale positions.
181181+ // Save original text for rollback if vault.create fails.
182182+ var originalText = editor.getRange(rangeFrom, rangeTo);
183183+ var linkStr = "[[" + noteId + "|" + safeAlias + "]]";
184184+ editor.replaceRange(linkStr, rangeFrom, rangeTo);
185185+186186+ try {
187187+ await app.vault.create(fileName, frontmatter);
188188+ } catch(createErr) {
189189+ // Rollback: restore original text since the note wasn't created
190190+ var linkEnd = {
191191+ line: rangeFrom.line,
192192+ ch: rangeFrom.ch + linkStr.length
193193+ };
194194+ editor.replaceRange(originalText, rangeFrom, linkEnd);
195195+ throw createErr;
196196+ }
197197+198198+ await app.workspace.openLinkText(noteId, sourcePath, "tab");
199199+200200+ } catch(e) {
201201+ console.error("followOrCreate error:", e);
202202+ try { new Notice("followOrCreate: " + e.message, 5000); } catch(_) {}
203203+ }
204204+})();
+105
home/profiles/obsidian/obsidian.vimrc
···11+" ============================================================
22+" Obsidian Vimrc (requires Vimrc Support plugin by esm7)
33+" ============================================================
44+55+" --- Clipboard ---
66+" All yanks go to system clipboard
77+set clipboard=unnamedplus
88+99+" Make Y yank to end of line (consistent with C and D)
1010+nnoremap Y y$
1111+1212+" --- Unbind Space for use as leader ---
1313+unmap <Space>
1414+1515+" --- Sidebar & Navigation ---
1616+" Toggle left sidebar
1717+exmap toggleSidebar obcommand app:toggle-left-sidebar
1818+nmap <Space>e :toggleSidebar<CR>
1919+2020+" Reveal active file in file explorer
2121+exmap revealFile obcommand file-explorer:reveal-active-file
2222+nmap <Space>f :revealFile<CR>
2323+2424+" Command palette
2525+exmap commandPalette obcommand command-palette:open
2626+nmap <Space>p :commandPalette<CR>
2727+2828+" --- Tab Navigation ---
2929+" Next tab (matches Vim gt)
3030+exmap nextTab obcommand workspace:next-tab
3131+nmap gt :nextTab<CR>
3232+3333+" Previous tab (matches Vim gT)
3434+exmap prevTab obcommand workspace:previous-tab
3535+nmap gT :prevTab<CR>
3636+3737+" Close current tab
3838+exmap closeTab obcommand workspace:close
3939+nmap gd :closeTab<CR>
4040+4141+" Jump past frontmatter and enter insert mode
4242+exmap goBody jscommand { var lines = editor.getValue().split("\n"); var count = 0; for (var i = 0; i < lines.length; i++) { if (lines[i].trim() === "---") { count++; if (count === 2) { editor.setCursor(i + 1, 0); return; } } } editor.setCursor(0, 0); }
4343+nmap gi :goBody<CR>i
4444+4545+" --- Notes ---
4646+" Daily note (Periodic Notes)
4747+exmap dailyNote obcommand periodic-notes:open-daily-note
4848+nmap <Space>d :dailyNote<CR>
4949+5050+" Weekly note (Periodic Notes)
5151+exmap weeklyNote obcommand periodic-notes:open-weekly-note
5252+nmap <Space>w :weeklyNote<CR>
5353+5454+" New Zettelkasten note
5555+exmap newZK obcommand zk-prefixer
5656+nmap <Space>zn :newZK<CR>
5757+5858+" Split (QuickAdd macro)
5959+exmap splitNote obcommand quickadd:choice:1d350c80-9184-4f8a-b37e-4e7c0712a19f
6060+nmap <Space>zs :splitNote<CR>
6161+6262+" --- Folding ---
6363+exmap unfoldAtCursor obcommand editor:unfold
6464+exmap foldAtCursor obcommand editor:fold
6565+exmap toggleFold obcommand editor:toggle-fold
6666+exmap unfoldAll obcommand editor:unfold-all
6767+exmap foldAll obcommand editor:fold-all
6868+6969+nmap zo :unfoldAtCursor<CR>
7070+nmap zc :foldAtCursor<CR>
7171+nmap za :toggleFold<CR>
7272+nmap zR :unfoldAll<CR>
7373+nmap zM :foldAll<CR>
7474+7575+" --- Spelling ---
7676+" Open spelling suggestions via the editor suggest/context menu
7777+exmap spellcheck jscommand { var pos = view.editor.cm.coordsAtPos(view.editor.cm.state.selection.main.head); view.editor.cm.dom.dispatchEvent(new MouseEvent("contextmenu", {bubbles: true, cancelable: true, clientX: pos.left, clientY: pos.top})); }
7878+nmap z= :spellcheck<CR>
7979+8080+" --- Quick Switcher ---
8181+exmap quickSwitcher obcommand switcher:open
8282+nmap <Space><Space> :quickSwitcher<CR>
8383+8484+" --- Search ---
8585+exmap globalSearch obcommand global-search:open
8686+nmap <Space>fg :globalSearch<CR>
8787+8888+" --- Surround (vim-surround style) ---
8989+" The plugin's built-in surroundOperator acts as a Vim operator:
9090+" ys{motion}{char} in normal mode (e.g. ysiw" to surround word with quotes)
9191+" S{char} in visual mode (e.g. viwS( to surround selection with parens)
9292+" It opens a prompt for the surround character; brackets auto-match.
9393+nunmap s
9494+vunmap s
9595+nmap ys <A-y>s
9696+vmap S <A-y>s
9797+9898+" --- Follow or Create Note ---
9999+" Enter in normal/visual mode: if on a [[wikilink]], open it in a new tab.
100100+" Otherwise, check if a note with the word/selection as title exists:
101101+" - If it does, wrap in [[link]] and open in a new tab.
102102+" - If not, create a new zk-prefixed note with that title and link to it.
103103+exmap followOrCreate jsfile scripts/follow-or-create.js
104104+nmap <CR> :followOrCreate<CR>
105105+vmap <CR> :followOrCreate<CR>
+22
home/profiles/opencode/default.nix
···17171818 # github-mcp-server binary path from nixpkgs
1919 githubMcpServer = "${pkgs.github-mcp-server}/bin/github-mcp-server";
2020+2121+ # opencode-handoff plugin: fetch source and assemble into a single directory
2222+ # so the relative import from the entry point resolves correctly
2323+ opencode-handoff-src = pkgs.fetchFromGitHub {
2424+ owner = "Chickensoupwithrice";
2525+ repo = "opencode-handoff";
2626+ rev = "e66697d";
2727+ hash = "sha256-/drpkGLxKmoYlo3MZqYnQSedwLWHl7TQuO+NkY21xuQ=";
2828+ };
2929+3030+ opencode-handoff-plugin = pkgs.runCommand "opencode-handoff-plugin" { } ''
3131+ mkdir -p $out
3232+ cat > $out/handoff.ts <<'ENTRY'
3333+ export { HandoffPlugin } from "./handoff-src/plugin"
3434+ ENTRY
3535+ ln -s ${opencode-handoff-src}/src $out/handoff-src
3636+ '';
2037in
2138{
2239 home.packages = [
···6683 "opencode/agents".source = ./agents;
6784 "opencode/commands".source = ./commands;
6885 "opencode/skills".source = ./skills;
8686+8787+ # opencode-handoff plugin: single derivation with entry point + source
8888+ # so relative imports resolve correctly (home-manager would otherwise
8989+ # place them in separate nix store paths)
9090+ "opencode/plugins".source = opencode-handoff-plugin;
6991 };
70927193 home.file = lib.mkIf isBox {