This is a concise, offline-readable summary. The full teaching write-up — with diagrams, failure modes, and "when not to" notes — lives in the project wiki.
Double Commander inherits Total Commander's plugin system. A plugin is a native
shared library exporting a fixed table of C functions; DC dlopens it and calls
those by name. There are four families:
| Type | Ext | Purpose |
|---|---|---|
| WLX | .wlx |
Lister — custom viewers (what markdown-wlx is) |
| WCX | .wcx |
Packers (archive formats) |
| WDX | .wdx |
Content fields (columns, search) |
| WFX | .wfx |
Virtual file systems |
A WLX plugin exports: ListLoad, ListLoadNext, ListCloseWindow,
ListGetDetectString, ListSetDefaultParams, ListSendCommand.
ListSendCommand is easy to miss and not optional in practice: Double Commander
binds Cmd+C and Cmd+A on its own Viewer form and, when a plugin owns the window,
dispatches them through this entry point rather than to the plugin's view. A
plugin that does not export it leaves both keys silently dead.
The ABI was designed around Windows HWND window handles. On macOS, Double
Commander maps those handles to NSView*:
ListLoad(parentNSView, path, flags)— build a view that displayspath, add it under the parent, and return it. DC sizes it to the viewer.ListLoadNext(...)— reuse the same view for the next file (viewer navigation).ListCloseWindow(view)— tear the view down.ListGetDetectString(buf, len)— a rule string (e.g.EXT="MD"|EXT="MARKDOWN") telling DC which files this plugin claims.
Because an Objective-C object crosses the C ABI boundary and must outlive the
function call, ownership is transferred explicitly with CFBridgingRetain on the
way out and CFBridgingRelease in ListCloseWindow.
ListLoadbuilds anNSViewcontaining aWKWebView.- It reads the Markdown file, base64-embeds it in a generated HTML document, and
references vendored assets (marked.js, highlight.js, GitHub CSS) by URL —
never inlined, because a minified library can contain a literal
</script>that would truncate an inline<script>tag. Those URLs are notfile://: WebKit's content process is sandboxed out of~/Library/Preferences/doublecmd/plugins, DC's own user-plugin directory, so afile://asset silently 404s once the plugin is installed. The plugin reads the bytes itself and serves them over a privatex-mdview://scheme through aWKURLSchemeHandler(book-wlxdoes the same for a book's own resources). - The page is loaded via
loadFileURL:allowingReadAccessToURL:with a<base>set to the document's directory, so the user's relative images resolve — only the plugin's own assets move offfile://. - marked.js renders Markdown → HTML client-side; highlight.js colors code blocks;
prefers-color-schemedrives light/dark live.
In Double Commander the viewer closes on Escape. With the plugin active it didn't —
pressing 1 (switch to Text) first, then Escape, worked. That pointed at keyboard
focus: a focused WKWebView consumes Escape instead of letting it travel up the
responder chain to DC's viewer window.
Rather than guess, a focused probe confirmed two things: a keyDown: override on a
WKWebView subclass is invoked, and forwarding the event to nextResponder
reaches the parent view. The fix is a few lines — forward only Escape up the
chain, leave every other key (scrolling, find, selection) to normal web handling:
@implementation MDWebView
- (void)keyDown:(NSEvent *)event {
if (event.keyCode == 53 /* Escape */) { [self.nextResponder keyDown:event]; return; }
[super keyDown:event];
}
@endThis is the kind of root-cause fix the collection favors over intercepting and re-dispatching events, which would be both more code and more fragile.
Anything public passes a generic leak-guard check
(secrets, private absolute paths, OS cruft) in CI and as a pre-commit hook. It is
intentionally generic — a checker that enumerates sensitive business terms would
itself leak them.