Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0

## [Unreleased]

### Added

- Give Markdown files associated with Leafdown a dedicated folded-page icon with the green leaf and Markdown symbol, separate from the application icon. Both Windows installers include it without changing your default app.

## [0.1.0-alpha.2] - 2026-10-05

### Added
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ File associations come from `src-tauri/windows/installer-hooks.nsh` for NSIS and
- a `Leafdown.Markdown` value under each extension's `OpenWithProgids`; and
- `Capabilities` under the product's registry key, listed in `RegisteredApplications`, so Default apps can offer Leafdown.

The document icon is separate from the application icon. `bundle.resources` maps `src-tauri/icons/document.ico` to `document.ico` beside the installed executable, and both installers point the ProgID's `DefaultIcon` at that quoted path. The source and installed filenames match because the bundled WiX generator retains the source filename when mapping resources. The bundler owns installing and removing this resource; MSI also repairs it. The editable source is `src-tauri/icons/document/icon.svg`, with separate 16, 20, 24, and 32 pixel sources for small-size adjustments. The ICO contains those sizes plus 40, 48, 64, 128, and 256 pixel frames. Application, window, and shortcut icons continue to use the existing leaf artwork.

Neither writes an extension's default value or its `UserChoice`. The MSI's values belong to one component, so Windows Installer restores them on repair and removes them on uninstall and major upgrade. The NSIS installer writes them on every install, and its uninstaller removes them only while the ProgID's command still names its own executable, so uninstalling a stale installation location leaves another location's registration in place.

## Security
Expand Down
2 changes: 1 addition & 1 deletion docs/help/file-and-folder-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Leafdown keeps the open document and the folder context separate. The folder con

**File > Open** opens one Markdown file: `.md`, `.markdown`, `.mdown`, or `.mkd`. If no folder is open, Leafdown also opens its parent folder as context. If a folder is already pinned, opening a file elsewhere leaves that context in place. **File > Open folder** scans the folder for Markdown articles and opens a matching root-level `readme` or `index` file when present. Ignored directories do not appear in the navigator.

On Windows, installing Leafdown adds it to File Explorer's **Open with** menu for these files and to **Settings > Apps > Default apps**, without changing which app opens them by default. Choose Leafdown there to open Markdown files from File Explorer. Each file opened this way starts its own Leafdown window, with the file's folder as context. **File > Open** works either way.
On Windows, installing Leafdown adds it to File Explorer's **Open with** menu for these files and to **Settings > Apps > Default apps**, without changing which app opens them by default. Choose Leafdown there to open Markdown files from File Explorer. Files associated with Leafdown show a folded-page icon with the green leaf and Markdown symbol. Each file opened this way starts its own Leafdown window, with the file's folder as context. **File > Open** works either way.

The welcome screen and **File > Open recent** show previously opened files and folders when **Record recent files and folders** is on. You can remove one recent item or clear both lists without deleting files. See [Settings reference](settings-reference.md) for the setting.

Expand Down
1 change: 1 addition & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ Workflows execute upon successful completion of dirty-state checks. If a dirty c
### Open From The Operating System

- The Windows installers register Leafdown as able to open each supported Markdown extension, so File Explorer offers it under Open with and Settings lists it under Default apps. Installing, upgrading, repairing, or reinstalling never makes Leafdown the default application: each extension's default and the user's choice stay as they were, and choosing Leafdown is the user's step in Windows.
- Files associated with Leafdown use a document icon showing the green leaf above the Markdown M and down arrow on a folded page. The application and its shortcuts keep the leaf icon.
- Opening a file through that registration, or starting Leafdown with a path as its first command-line argument, starts a new Leafdown window with that file, even when another Leafdown window is open. A relative path resolves against the directory Leafdown started in; later arguments are ignored.
- The file opens through [Open File](#open-file) once preferences have loaded, so it sets the folder context and records recents as File > Open does. A missing, unreadable, oversized, unsupported, or undecodable file shows its open error, as described in [Error Handling](#error-handling), and leaves the welcome screen.
- Upgrading, reinstalling, or repairing points the registration at the installed executable. Uninstalling removes the registration that installation made and nothing else. The NSIS installation registers for the current user and the MSI installation for all users; while both are installed, Windows uses the current user's.
Expand Down
Binary file added src-tauri/icons/document.ico
Binary file not shown.
7 changes: 7 additions & 0 deletions src-tauri/icons/document/16.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src-tauri/icons/document/20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src-tauri/icons/document/24.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src-tauri/icons/document/32.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src-tauri/icons/document/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 58 additions & 1 deletion src-tauri/src/installer_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fs;
use std::{collections::BTreeSet, fs};

use crate::document::MARKDOWN_FILE_EXTENSIONS;

Expand Down Expand Up @@ -58,3 +58,60 @@ fn installers_register_every_listed_extension_without_claiming_its_default() {
}
}
}

#[test]
fn installers_register_the_bundled_document_icon_without_replacing_the_app_icon() {
let config: serde_json::Value = serde_json::from_str(&read_manifest_file("tauri.conf.json"))
.expect("the Tauri configuration should be valid JSON");
let source = "icons/document.ico";

assert_eq!(config["bundle"]["resources"][source], "document.ico");
assert!(
config["bundle"]["icon"]
.as_array()
.expect("application icons should be listed")
.iter()
.any(|icon| icon == "icons/icon.ico")
);
assert!(read_manifest_file("windows/installer-hooks.nsh").contains(
"WriteRegStr SHCTX \"${LEAFDOWN_PROGID_KEY}\\DefaultIcon\" \"\" `\"$INSTDIR\\document.ico\",0`"
));
assert!(read_manifest_file("windows/file-associations.wxs").contains(
"<RegistryValue Key=\"DefaultIcon\" Type=\"string\" Value=\"&quot;[INSTALLDIR]document.ico&quot;,0\" />"
));

let icon = fs::read(format!("{}/{source}", env!("CARGO_MANIFEST_DIR")))
.expect("the bundled document icon should be readable");
assert_eq!(&icon[..4], &[0, 0, 1, 0]);
let frame_count = usize::from(u16::from_le_bytes([icon[4], icon[5]]));
let mut sizes = BTreeSet::new();
for index in 0..frame_count {
let entry = &icon[6 + index * 16..6 + (index + 1) * 16];
let width = if entry[0] == 0 {
256
} else {
u32::from(entry[0])
};
let height = if entry[1] == 0 {
256
} else {
u32::from(entry[1])
};
assert_eq!(width, height);
assert!(sizes.insert(width), "icon frame sizes should not repeat");

let length = u32::from_le_bytes(entry[8..12].try_into().unwrap()) as usize;
let offset = u32::from_le_bytes(entry[12..16].try_into().unwrap()) as usize;
let image = &icon[offset..offset + length];
assert_eq!(&image[..8], b"\x89PNG\r\n\x1a\n");
assert_eq!(u32::from_be_bytes(image[16..20].try_into().unwrap()), width);
assert_eq!(
u32::from_be_bytes(image[20..24].try_into().unwrap()),
height
);
}
assert_eq!(
sizes,
BTreeSet::from([16, 20, 24, 32, 40, 48, 64, 128, 256])
);
}
3 changes: 3 additions & 0 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"bundle": {
"active": true,
"targets": ["nsis", "msi"],
"resources": {
"icons/document.ico": "document.ico"
},
"windows": {
"nsis": {
"installerHooks": "windows/installer-hooks.nsh"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/windows/file-associations.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<Component Id="MarkdownFileAssociations" Guid="{995333E7-D28B-4160-A1C3-9BBDBBA99FDD}">
<RegistryKey Root="HKLM" Key="Software\Classes\Leafdown.Markdown">
<RegistryValue Type="string" Value="Markdown Document" KeyPath="yes" />
<RegistryValue Key="DefaultIcon" Type="string" Value="&quot;[LeafdownExecutable]&quot;,0" />
<RegistryValue Key="DefaultIcon" Type="string" Value="&quot;[INSTALLDIR]document.ico&quot;,0" />
<RegistryValue Key="shell\open\command" Type="string" Value="&quot;[LeafdownExecutable]&quot; &quot;%1&quot;" />
</RegistryKey>
<RegistryKey Root="HKLM" Key="Software\[Manufacturer]\[ProductName]\Capabilities">
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/windows/installer-hooks.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
!macro NSIS_HOOK_POSTINSTALL
!insertmacro LEAFDOWN_OPEN_COMMAND $R0
WriteRegStr SHCTX "${LEAFDOWN_PROGID_KEY}" "" "Markdown Document"
WriteRegStr SHCTX "${LEAFDOWN_PROGID_KEY}\DefaultIcon" "" `"$INSTDIR\${MAINBINARYNAME}.exe",0`
WriteRegStr SHCTX "${LEAFDOWN_PROGID_KEY}\DefaultIcon" "" `"$INSTDIR\document.ico",0`
WriteRegStr SHCTX "${LEAFDOWN_PROGID_KEY}\shell\open\command" "" $R0

WriteRegStr SHCTX "${MANUPRODUCTKEY}\Capabilities" "ApplicationName" "${PRODUCTNAME}"
Expand Down