Close Reflect

by InVerse
5
4
3
2
1
Score: 50/100

Description

Intercept Obsidian's quit and show a reflection prompt with an editable title, Markdown body and action buttons before the window closes.

Reviews

No reviews yet.

Stats

0
stars
12
downloads
0
forks
2
days
0
days
1
days
0
total PRs
0
open PRs
0
closed PRs
0
merged PRs
0
total issues
0
open issues
0
closed issues
6
commits

Latest Version

a day ago

Changelog

What's new

The prompt's body is live. It is rendered Markdown, so it can hold wikilinks, external links, embeds, Meta Bind controls, task checkboxes — anything the renderer produces. Two things were missing and are now fixed.

Embeds are clickable. Obsidian renders an embed as an element carrying its src linktext and only handles navigation for regular links, so clicking an embedded note's body did nothing at all. The whole embed now navigates to its target.

Using any of it keeps you in Obsidian. Clicking content that does something now cancels the quit and steps the prompt aside, instead of leaving you with the quit held and the prompt still covering the screen. You are going somewhere, not leaving.

The detection is structural rather than a list of plugin-specific classes — a link, a button, a form control, a data-href, an embed. A wikilink, an external link, a Meta Bind control and a task checkbox all qualify without this plugin knowing anything about them.

Notes

  • Where the content does its own work (a link), the click is left untouched so Obsidian still handles it; where it does nothing on its own (an embed), the plugin opens the target itself.
  • Because the whole embed is clickable, a click that follows selecting text inside an embed will also navigate. That is the cost of making the entire block clickable.
  • The settings modal's preview gets the same embed behaviour, but its buttons stay inert — they would otherwise drive the quit.

No settings changes.

README file from

Github

Close Reflect

Intercept Obsidian's quit and show a reflection prompt before the window closes — a chance to look at where you are before you leave.

The prompt has an editable title, Markdown body, and as many action buttons as you want. Buttons can cancel the quit, let the app close, open a note, run a command, or open a URL.

The body is real Markdown, so wikilinks, external links, embeds, Meta Bind controls and task checkboxes all work inside it — and embeds are clickable anywhere on their body, which they are not by default. Clicking something that does anything cancels the quit and steps the prompt aside: you are going somewhere, not leaving.

The plugin depends on Obsidian internals that a future release could change — see Limitations.

How the interception works

Obsidian does not make this easy, and the interesting part of this plugin is the three obstacles it works around. All three were established by reading Obsidian 1.13.7's own bundle, and each one is verified by the plugin's own developer log.

1. Obsidian's quit hook erases itself. The workspace quit event is fired from inside Obsidian's own window.onbeforeunload hook, and that hook starts by setting window.onbeforeunload = null so the window.close() it re-issues cannot loop back into it. One session therefore gets exactly one quit event. The plugin captures the hook before its first run and puts it back after a quit it cancelled, which is what lets the second close be intercepted too.

2. Obsidian force-closes any window that survives a close for three seconds. Its main process arms setTimeout(() => { !v.defaultPrevented && g.destroy() }, 3000) on every window close, and destroy() bypasses beforeunload entirely. The catch is that close.defaultPrevented is only ever written by a main-process listener calling preventDefault() — a renderer's beforeunload cancellation travels a different channel (will-prevent-unload) and never marks it. Obsidian's own bootstrap hands the renderer the window through @electron/remote (window.electronWindow), so the plugin joins that same close event and marks it once it has decided to hold — which makes the timer stand down. The mark has to happen after dispatch, not during it: preventing the close outright would stop beforeunload from running at all, and the prompt would never appear.

3. Holding the quit strands Obsidian's "Saving..." screen. That screen lives on a module-private singleton inside Obsidian's bundle and is only taken down by the very window.close() the plugin swallows. There is no way to reach the instance, so the plugin repeats what its hide() does — remove the container and drop the in-progress body class.

Every failure path fails open: an unanswered prompt follows the configured timeout action, and a prompt that could not be shown at all releases the quit, so a broken overlay can never leave the app unclosable.

On mobile

Mobile has no quit to intercept — the quit event is fired from inside Obsidian's Electron hook, which mobile does not have. What it has instead are gestures, and the plugin answers two of them, each behind its own toggle:

Gesture What happens
Back button The prompt appears when the back button would leave the app. A press with a note to go back to, or a sidebar to collapse, is navigation and is left alone — the plugin mirrors the condition Obsidian's own handler uses.
Going back to the home screen The prompt is raised as the app is backgrounded, and is waiting when you come back.

There is an asymmetry worth knowing. Capacitor delivers the back button to every listener and offers no way to stop the others, so this runs alongside Obsidian's handler rather than replacing it: pressing back twice in quick succession still sends the app to the background, because that second press is Obsidian's. And backgrounding cannot be cancelled at all — the app is already gone — which is why that second toggle is off by default and why "cancel the quit" on mobile simply means the prompt closes.

The obvious way to take the gesture over, removeAllListeners(), is not used: it drops every other listener Obsidian registered for that plugin too — URI opens, share intents, app state — and leaves the app worse off.

The edit modal

Settings keep the behavioural options. The title, the text and the buttons are edited in a dedicated modal: editor on the left (a real Obsidian Markdown editor, with live preview and [[ completion), live preview on the right, Save and Cancel at the bottom. Everything is edited on a draft, so Cancel really discards.

The modal adapts to the content source:

Inline text Linked note
Editor the overlay text the note's body, frontmatter excluded
Save writes the text into the settings writes the body back into the note, keeping its frontmatter

In linked mode the note is only written when its body actually changed, and only if it was readable in the first place — a note that could not be read is never written over.

The embedded editor borrows Obsidian's own CodeMirror through the module table Obsidian exposes to plugins, so no second copy of CodeMirror is bundled and Obsidian's internal editor state fields keep working. If that ever stops being available the modal falls back to a plain textarea.

Settings

Setting Meaning
Content source Write the text inline, or render it from a note in the vault.
Title, text and buttons Opens the edit modal.
Linked note The note whose contents the overlay shows. Start typing to search.
Remembered copy A copy of the linked note, used when the note cannot be read.
Interceptions before release How many quits to interrupt. Once spent, later quits close without asking.
Release timeout How long to wait for an answer.
When the timeout expires Cancel the quit and stay open, or let the app close. Cancelling is the default.
Write diagnostics to a file Appends one entry per quit attempt inside the vault.
Developer log Echoes diagnostics to the console and writes a step-by-step trace to close-reflect-trace.log in the system temp folder.
Ask on the back button Mobile only.
Ask on going back to the home screen Mobile only, off by default.

Limitations

  • The quit event is documented as "Not guaranteed to actually run." Crashes, an external kill, or a priority shutdown will skip the interception. This is best-effort.
  • Registering a quit task breaks Reload app without saving: the reload command closes the window instead of reloading. Disable the plugin if you need that command.
  • The interception relies on Obsidian internals (@electron/remote, the shape of its quit hook, the progress-screen markup, and on mobile the Capacitor bridge and the shape of Obsidian's own back handling). A future Obsidian release could change any of them; each is guarded and falls back rather than breaking the app.
  • The prompt is a speed bump, not a lock: on desktop it holds the quit, but on mobile Obsidian's own second back press still backgrounds the app.

Data and privacy

  • No network access. No telemetry. No analytics.
  • With Write diagnostics to a file on, the plugin appends one entry per quit attempt to a vault-relative path you choose.
  • With Developer log on, it also writes a step-by-step trace to close-reflect-trace.log in the system temp folder. Both are off by default.

Development

bun install
bun run build      # bundles src/main.ts -> main.js
bun run typecheck  # tsc --noEmit

Copy main.js, manifest.json and styles.css into <vault>/.obsidian/plugins/close-reflect/ and enable the plugin.

Credits

The embedded Markdown editor in src/markdown-editor.ts is ported from EmbeddableMarkdownEditor by Fevol, with the prototype-extraction technique originally from mgmeyers/obsidian-kanban — both MIT licensed. Only that MIT-licensed editor-construction code was carried over.

License

MIT