Two one-time steps
This build is not signed with an Apple Developer ID and not notarized, so macOS blocks
the first launch. Allow it once:
- macOS 15 Sequoia and later β double-click, dismiss the warning, then open
System Settings βΈ Privacy & Security and click Open Anyway. Apple removed the
Control-click shortcut for unsigned apps in macOS 15, so right-clicking does not help.- macOS 13β14 β right-click the app βΈ Open, then confirm.
Then answer the folder prompts once β macOS asks separately for Desktop, Documents and
Downloads the first time the app looks at them. Allow them and it stays quiet from then on.
Markdown and HTML as PDFs, from the renderer that already draws them β and the five ways an SFTP
site could be left unable to log in at all.
Added
-
Markdown and HTML documents export as PDFs. With the cursor on a
.mdor.htmlfile,
Commands βΈ Export to PDFβ¦ β and the same entry in the panel's context menu β writes the
document as a PDF beside it, under its own name; a marked selection is exported one file after
another. The PDF is the page F3 shows, because it is produced by the same renderer: the same
stylesheet, the same coloured fences, the same Mermaid diagrams and KaTeX formulae, and the same
two web-view policies (the generated Markdown page may run the engines, a foreign.htmlfile may
run nothing). A4 or US Letter, a setting; a second export is numbered rather than overwriting the
first, unless that is asked for; the panel moves its cursor to the new file, and when that is
switched off a short message takes its place so an export is never silent. Offered on ordinary
folders only β inside an archive or on a mounted drive the cursor's path is a VFS path with no
file behind it on disk and nowhere beside it to write, so the entry is greyed out there.Pagination is done in the plugin rather than by WebKit, and that is measured rather than a
preference:WKWebView.printOperationdoes not return on this platform βop.run()wrote a
gigabyte of repeated pages in ninety seconds, first in a standalone harness and then in the
application, with the print view's own frame reported as 0Γ0. So the document is measured,
the page breaks are put at the block boundaries the page itself reports β and never directly below
a heading, which is the one break a reader notices β each page is captured withcreatePDF, and
the slices are composed onto real sheets with Core Graphics. Anything that will not fit the text
column shrinks the sheet to fit rather than running off its right edge: an eight-column table was
measured at 604 points inside a 487-point column and lost its last two columns, silently. The whole thing is a contributed
command: the host builds the menu entry from the plugin's manifest without loading it, so a
removed or disabled plugin takes the entry with it. There is deliberately no switch for the export
itself: nothing in the plugin's settings can be read at the moment the entry is built, so such a
switch could only make the command refuse β a menu entry that is present and says no.PDFExport=
in an oldermarkdown.iniis ignored.
Fixed
-
A diagram that hung took its own source with it. The Markdown plugin's rule is that a figure
which cannot be drawn says so where it was, with the block's source below it β "a silently
missing figure is the failure that gets reported as 'the viewer lost my text'". The parse-error
path obeyed it; the path where Mermaid's promise never settles did not, because the.catchthat
restores the source never ran. The<pre>had already been replaced by an empty holder, so the
diagram and its text were simply gone β in the viewer and in an exported PDF, with nothing said.
Found by deliberately arming arender()that never settles, which is what a broken engine file in
the reader's own folder amounts to. The page now carries its own ten-second deadline per diagram,
so the rule holds on the same path as a parse error. -
Choosing SFTP locked a connection to the anonymous login (#4). A new site starts with
"Anonymous" ticked, and SSH has no anonymous login β so picking SFTP greyed the box out while it
was still ticked, and.anonymousis what disables the user name and the password as well. All
three controls went dead together and the only way back was greyed out with them: the site could
not be given a user name at all, short of switching to FTP, unticking, and switching back. It was
saved that way too, so everyone who met this hasauth=anonymousunderprotocol=sftpin their
ftp-sites.iniβ such a site is read as an ordinary password (or key) login now, without the file
being rewritten behind anyone's back, and the dialog opens it unlocked. The rule lives with the
others inFtpConnectionRules, so the greyed-out controls, the warning label and the connection
all still say the same thing. Picking SFTP now clears the box and the name "anonymous" it had put
in the field, which is what makes the dialog ask for a real one. -
A bare
sftp://URL logged in as "anonymous" (#4). The same forcing sat on the quick-connect
route (Ctrl+N):sftp://hostwithout a user name became the user anonymous instead of the
local account, although the URL parser had deliberately left it empty for exactly that fallback.
It logs in as the local account now;ftp://is unchanged, since there the anonymous login is
real and is what a bare URL means. -
The passphrase of an encrypted SSH key was asked for and then thrown away. The secret field is
deliberately enabled for a key file β libssh2 wants the passphrase instead of a password, and
that field is where it is typed β but the site's secret was written to the Keychain only when the
login was a plain password. So the passphrase was read back on every selection and never once
stored, and had to be retyped every session. It is kept whenever the field is offered at all,
which is the same question the dialog already asks in order to enable it. -
An ssh-agent login was silently demoted to a password login.
auth=agentis a value only a
hand-writtenftp-sites.inicarries β no control in the dialog sets it β and the next keystroke
in any field rewrote it toauth=password. It is kept now while nothing in the form contradicts
it. The secret field is also no longer disabled for it: nothing in the dialog could set.agent
and nothing could leave it either, so such a site could not be given a password at all. An empty
field still sends no secret; typing in one is how a site stops being an agent site. -
A tick that no longer applied was carried over behind a greyed-out box. "Transfer via SCP" and
"Accept self-signed certificate" stayed set when the protocol moved to one that has no such
setting, and were written toftp-sites.inithat way β so a site that had been FTPS once accepted
any certificate again the moment it became FTPS a second time, without anybody choosing it twice.
Changing the protocol now clears the boxes the new protocol has no such thing for, visibly and by
the user's own action.