From c385dd42fe6da5b40086f0ec3d1e7d855e93c234 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 10:56:07 +0800 Subject: [PATCH 1/9] fix: restore collapsed toolbar settings --- scripts/reloadOpenToolbar.test.ts | 6 ++-- src/lib/components/Settings.svelte | 56 ++++++++++++++++++------------ 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/scripts/reloadOpenToolbar.test.ts b/scripts/reloadOpenToolbar.test.ts index b7830656..194660df 100644 --- a/scripts/reloadOpenToolbar.test.ts +++ b/scripts/reloadOpenToolbar.test.ts @@ -24,10 +24,10 @@ test('preview-level open shortcut handles Ctrl/Cmd+O outside Monaco without doub assert.match(markdownViewer, /cmdOrCtrl[\s\S]*key === 'o'[\s\S]*selectFile\(\)/); }); -test('editor toolbar delegates to Monaco actions and exposes expected Markdown commands', () => { +test('editor toolbar forwards Monaco actions and optional payloads', () => { assert.match(markdownViewer, /import EditorToolbar from '\.\/components\/EditorToolbar\.svelte'/); - assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId\)\}/); - assert.match(editor, /export function runEditorAction\(actionId: string\)/); + assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId, payload\)\}/); + assert.match(editor, /export function runEditorAction\(actionId: string, payload\?: any\)/); for (const actionId of [ 'fmt-bold', diff --git a/src/lib/components/Settings.svelte b/src/lib/components/Settings.svelte index f1b8d7d6..15d8673e 100644 --- a/src/lib/components/Settings.svelte +++ b/src/lib/components/Settings.svelte @@ -1172,9 +1172,13 @@

{t('settings.toolbarsSettings', settings.language)}

-
-
-

{t('settings.applicationToolbar', settings.language)}

+
+ + + {t('settings.applicationToolbar', settings.language)} + +
+
{/each} +
-
- -
-
-

{t('settings.editorToolbar', settings.language)}

+ + +
+ + + {t('settings.editorToolbar', settings.language)} + +
+
{/each} +
-
+ {:else if activeCategory === 'files'}
@@ -1712,10 +1722,21 @@ display: none; } - .toolbar-section { - display: flex; - flex-direction: column; - gap: 8px; + .toolbar-settings-chevron { + width: 7px; + height: 7px; + border-right: 1.5px solid var(--color-fg-muted); + border-bottom: 1.5px solid var(--color-fg-muted); + transform: rotate(-45deg); + transition: transform 0.12s ease; + } + + .toolbar-settings[open] .toolbar-settings-chevron { + transform: rotate(45deg); + } + + .toolbar-settings-body { + padding-bottom: 12px; } .toolbar-section-header { @@ -1725,15 +1746,6 @@ padding-bottom: 2px; } - .toolbar-section-header h3 { - margin: 0; - font-size: 12px; - font-weight: 600; - color: var(--color-fg-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - } - .toolbar-settings-list { display: flex; flex-direction: column; From 584919e2412e80b2236054f2748e66828ff1265c Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 10:42:19 +0800 Subject: [PATCH 2/9] fix: enable PDF export on macOS --- scripts/macosPdfExport.test.ts | 16 ++++++++++++++++ src-tauri/capabilities/default.json | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 scripts/macosPdfExport.test.ts diff --git a/scripts/macosPdfExport.test.ts b/scripts/macosPdfExport.test.ts new file mode 100644 index 00000000..d563937c --- /dev/null +++ b/scripts/macosPdfExport.test.ts @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('all Markpad webviews may invoke Tauri native printing', () => { + const capability = JSON.parse(readFileSync('src-tauri/capabilities/default.json', 'utf8')) as { + windows: string[]; + permissions: string[]; + }; + + assert.deepEqual(capability.windows, ['main', 'installer', 'window-*']); + assert.ok( + capability.permissions.includes('core:webview:allow-print'), + 'macOS replaces window.print() with Tauri native printing, which requires this permission', + ); +}); diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 955f6753..afaf19a2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,6 +13,7 @@ "opener:allow-open-url", "opener:allow-open-path", "dialog:default", + "core:webview:allow-print", "core:window:allow-start-dragging", "core:window:allow-minimize", "core:window:allow-toggle-maximize", @@ -25,4 +26,4 @@ "updater:default", "process:default" ] -} \ No newline at end of file +} From a76cb5b5098465a4d6ffef96af78097f0720b65e Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 10:53:27 +0800 Subject: [PATCH 3/9] fix: allow native window title updates --- scripts/windowTitleCapability.test.ts | 14 ++++++++++++++ src-tauri/capabilities/default.json | 1 + 2 files changed, 15 insertions(+) create mode 100644 scripts/windowTitleCapability.test.ts diff --git a/scripts/windowTitleCapability.test.ts b/scripts/windowTitleCapability.test.ts new file mode 100644 index 00000000..78c0a8ef --- /dev/null +++ b/scripts/windowTitleCapability.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('all Markpad windows may update their native title', () => { + const capability = JSON.parse(readFileSync('src-tauri/capabilities/default.json', 'utf8')) as { + permissions: string[]; + }; + + assert.ok( + capability.permissions.includes('core:window:allow-set-title'), + 'window tags and active document names call appWindow.setTitle(), which requires this permission', + ); +}); diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index afaf19a2..c41704e0 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -15,6 +15,7 @@ "dialog:default", "core:webview:allow-print", "core:window:allow-start-dragging", + "core:window:allow-set-title", "core:window:allow-minimize", "core:window:allow-toggle-maximize", "core:window:allow-close", From 6947e98d257713013021acb0902c757354a12544 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:17:29 +0800 Subject: [PATCH 4/9] fix: preserve tabs after document read errors --- scripts/documentLoadFailure.test.ts | 10 ++++++++++ src/lib/sessions/documentSession.svelte.ts | 9 ++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 scripts/documentLoadFailure.test.ts diff --git a/scripts/documentLoadFailure.test.ts b/scripts/documentLoadFailure.test.ts new file mode 100644 index 00000000..8fc579d0 --- /dev/null +++ b/scripts/documentLoadFailure.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'); + +test('a transient missing-file read error preserves the open tab', () => { + assert.doesNotMatch(session, /tabManager\.closeTab/); + assert.match(session, /options\.onError\('Error loading file', error\);/); +}); diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index 4817f096..591d2c81 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -142,11 +142,10 @@ export function createDocumentSession(options: DocumentSessionOptions) { if (filePath) options.saveRecentFile(filePath); } catch (error) { console.error('Error loading file:', error); - const errorText = String(error); - if (errorText.includes('The system cannot find the file specified') || errorText.includes('No such file or directory')) { - options.deleteRecentFile(filePath); - if (tabManager.activeTab?.path === filePath) tabManager.closeTab(tabManager.activeTab.id); - } else options.onError('Error loading file', error); + // A watcher can observe a transient gap while another process replaces + // the file. Keep the already-open buffer and recent-file entry instead + // of closing the tab and discarding the user's recovery path. + options.onError('Error loading file', error); } } From 8cec9a91096f254e948d74253971e55d686be96d Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:46:06 +0800 Subject: [PATCH 5/9] fix: route Live Mode changes to the watched file --- scripts/liveModeWatchedPath.test.ts | 20 ++++++++++++++++++++ src-tauri/src/window_runtime.rs | 3 ++- src/lib/MarkdownViewer.svelte | 22 ++++++++++++---------- src/lib/sessions/documentSession.svelte.ts | 2 -- 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 scripts/liveModeWatchedPath.test.ts diff --git a/scripts/liveModeWatchedPath.test.ts b/scripts/liveModeWatchedPath.test.ts new file mode 100644 index 00000000..7f009744 --- /dev/null +++ b/scripts/liveModeWatchedPath.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const runtime = readFileSync('src-tauri/src/window_runtime.rs', 'utf8'); +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); + +test('Live Mode routes a watcher notification to its watched path', () => { + assert.match(runtime, /emit_to\(event_label\.as_str\(\), "file-changed", watched_path\.clone\(\)\)/); + assert.match(viewer, /await appWindow\.listen\('file-changed', \(event\) => \{/); + assert.match(viewer, /const changedPath = event\.payload as string;/); + assert.match(viewer, /if \(!liveMode \|\| !currentFile \|\| changedPath !== currentFile\) return;/); +}); + +test('Live Mode follows the active file instead of retaining a previous tab watcher', () => { + assert.match(viewer, /if \(liveMode && currentFile\) \{\n\t\t\tinvoke\('watch_file', \{ path: currentFile \}\)/); + assert.doesNotMatch(readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'), /isLiveMode\(\)\) invoke\('watch_file'/); + const toggleLiveMode = viewer.slice(viewer.indexOf('function toggleLiveMode'), viewer.indexOf('async function saveImageAs')); + assert.doesNotMatch(toggleLiveMode, /loadMarkdown\(/); +}); diff --git a/src-tauri/src/window_runtime.rs b/src-tauri/src/window_runtime.rs index a1a77709..26ddab7b 100644 --- a/src-tauri/src/window_runtime.rs +++ b/src-tauri/src/window_runtime.rs @@ -288,10 +288,11 @@ pub fn watch_file( let mut watchers = state.watchers.lock().unwrap(); watchers.remove(&label); let event_label = label.clone(); + let watched_path = path.clone(); let mut watcher = RecommendedWatcher::new( move |result: Result| { if result.is_ok() { - let _ = handle.emit_to(event_label.as_str(), "file-changed", ()); + let _ = handle.emit_to(event_label.as_str(), "file-changed", watched_path.clone()); } }, Config::default(), diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index d2044950..441b7991 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -525,7 +525,6 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu scrollFuture = []; }, renderMarkdown: renderMarkdownPreview, - isLiveMode: () => liveMode, afterLoad: tick, saveRecentFile, deleteRecentFile, @@ -620,6 +619,14 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu findOpen = false; }); + $effect(() => { + if (liveMode && currentFile) { + invoke('watch_file', { path: currentFile }).catch(console.error); + } else { + invoke('unwatch_file').catch(console.error); + } + }); + function processHighlights(root: Element) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { @@ -1907,14 +1914,8 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu if (currentFile) await invoke('open_file_folder', { path: currentFile }); } - async function toggleLiveMode() { + function toggleLiveMode() { liveMode = !liveMode; - if (liveMode && currentFile) { - await invoke('watch_file', { path: currentFile }); - if (tabManager.activeTabId) await loadMarkdown(currentFile); - } else { - await invoke('unwatch_file'); - } } async function saveImageAs(src: string) { @@ -2638,8 +2639,9 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu }), ); unlisteners.push( - await appWindow.listen('file-changed', () => { - if (!liveMode || !currentFile) return; + await appWindow.listen('file-changed', (event) => { + const changedPath = event.payload as string; + if (!liveMode || !currentFile || changedPath !== currentFile) return; // Skip events caused by our own auto-save / save invocations, // otherwise the reload would clobber any keystrokes that landed // between fs::write and this listener firing. diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index 591d2c81..18021636 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -16,7 +16,6 @@ type DocumentSessionOptions = { currentFile: () => string; resetScrollHistory: () => void; renderMarkdown: (raw: string, path: string) => Promise; - isLiveMode: () => boolean; afterLoad: () => Promise; saveRecentFile: (path: string) => void; deleteRecentFile: (path: string) => void; @@ -137,7 +136,6 @@ export function createDocumentSession(options: DocumentSessionOptions) { if (tab) tab.isEditing = true; tabManager.setTabRawContent(activeId, content); } - if (options.isLiveMode()) invoke('watch_file', { path: filePath }).catch(console.error); await options.afterLoad(); if (filePath) options.saveRecentFile(filePath); } catch (error) { From d44c62f2b0087b400d8887d9ca2877b9b6d17113 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:48:09 +0800 Subject: [PATCH 6/9] fix: allow opening multiple documents --- scripts/openMultipleFiles.test.ts | 12 ++++++++++++ src/lib/MarkdownViewer.svelte | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 scripts/openMultipleFiles.test.ts diff --git a/scripts/openMultipleFiles.test.ts b/scripts/openMultipleFiles.test.ts new file mode 100644 index 00000000..3efbbd33 --- /dev/null +++ b/scripts/openMultipleFiles.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); +const selectFile = viewer.slice(viewer.indexOf('async function selectFile'), viewer.indexOf('async function reloadFromDisk')); + +test('Open File accepts multiple documents and loads each selected path', () => { + assert.match(selectFile, /multiple: true/); + assert.match(selectFile, /const paths = Array\.isArray\(selected\) \? selected : \[selected\];/); + assert.match(selectFile, /for \(const path of paths\) await loadMarkdown\(path\);/); +}); diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 441b7991..dd9d75c0 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -1848,13 +1848,15 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu async function selectFile() { const selected = await open({ - multiple: false, + multiple: true, filters: [ { name: 'Markdown', extensions: ['md', 'markdown', 'mdown', 'mkd'] }, { name: 'All Files', extensions: ['*'] }, ], }); - if (selected && typeof selected === 'string') loadMarkdown(selected); + if (!selected) return; + const paths = Array.isArray(selected) ? selected : [selected]; + for (const path of paths) await loadMarkdown(path); } async function reloadFromDisk() { From 276d00f3be1890df4737bc5e29da2c4fb77e3b62 Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:54:04 +0800 Subject: [PATCH 7/9] fix: retain all macOS open-document paths --- scripts/macosOpenDocumentPaths.test.ts | 16 ++++++++++++++++ src-tauri/src/lib.rs | 4 ++-- src-tauri/src/window_runtime.rs | 7 ++++--- src/lib/MarkdownViewer.svelte | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 scripts/macosOpenDocumentPaths.test.ts diff --git a/scripts/macosOpenDocumentPaths.test.ts b/scripts/macosOpenDocumentPaths.test.ts new file mode 100644 index 00000000..6b373ab7 --- /dev/null +++ b/scripts/macosOpenDocumentPaths.test.ts @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const runtime = readFileSync('src-tauri/src/window_runtime.rs', 'utf8'); +const tauriLib = readFileSync('src-tauri/src/lib.rs', 'utf8'); +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); + +test('macOS open-document events preserve every delivered file path', () => { + assert.match(runtime, /startup_files: Mutex>/); + assert.match(tauriLib, /for url in urls/); + assert.match(tauriLib, /startup_files\.lock\(\)\.unwrap\(\)\.push\(path_str\.clone\(\)\)/); + assert.match(runtime, /startup_files\.lock\(\)\.unwrap\(\)\.drain\(\.\.\)\.collect\(\)/); + assert.match(runtime, /for path in startup_files\.into_iter\(\)\.rev\(\)/); + assert.match(viewer, /for \(const path of args\) await loadMarkdown\(path\);/); +}); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 99ce9cd8..dfbbe5b4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1569,12 +1569,12 @@ pub fn run() { .run(|_app_handle, _event| { #[cfg(target_os = "macos")] if let tauri::RunEvent::Opened { urls } = _event { - if let Some(url) = urls.first() { + for url in urls { if let Ok(path_buf) = url.to_file_path() { let path_str = path_buf.to_string_lossy().to_string(); let state = _app_handle.state::(); - *state.startup_file.lock().unwrap() = Some(path_str.clone()); + state.startup_files.lock().unwrap().push(path_str.clone()); if let Some(window) = pick_delivery_window(_app_handle) { let _ = _app_handle.emit_to(window.label(), "file-path", path_str); diff --git a/src-tauri/src/window_runtime.rs b/src-tauri/src/window_runtime.rs index 26ddab7b..41767b7d 100644 --- a/src-tauri/src/window_runtime.rs +++ b/src-tauri/src/window_runtime.rs @@ -21,7 +21,7 @@ impl WatcherState { } pub struct AppState { - pub(crate) startup_file: Mutex>, + pub(crate) startup_files: Mutex>, pub(crate) last_focused_viewer: Mutex>, window_registry: Mutex>, window_counter: AtomicU64, @@ -30,7 +30,7 @@ pub struct AppState { impl AppState { pub fn new() -> Self { Self { - startup_file: Mutex::new(None), + startup_files: Mutex::new(Vec::new()), last_focused_viewer: Mutex::new(None), window_registry: Mutex::new(HashMap::new()), window_counter: AtomicU64::new(0), @@ -315,7 +315,8 @@ pub fn send_markdown_path(state: State<'_, AppState>) -> Vec { .skip(1) .filter(|arg| !arg.starts_with('-')) .collect(); - if let Some(path) = state.startup_file.lock().unwrap().take() { + let startup_files: Vec = state.startup_files.lock().unwrap().drain(..).collect(); + for path in startup_files.into_iter().rev() { if !files.contains(&path) { files.insert(0, path); } diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index dd9d75c0..9b4c4761 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -2947,7 +2947,7 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu try { const args: string[] = await invoke('send_markdown_path'); if (!isDisposed && args?.length > 0) { - await loadMarkdown(args[0]); + for (const path of args) await loadMarkdown(path); } } catch (error) { console.error('Error receiving Markdown file path:', error); From 4cfb8c62ca5079834c9a82bb9e2df3f12bc743db Mon Sep 17 00:00:00 2001 From: PathGao Date: Fri, 31 Jul 2026 13:57:03 +0800 Subject: [PATCH 8/9] fix: normalize imported VS Code theme slugs --- src-tauri/src/lib.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dfbbe5b4..73f16664 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -303,6 +303,11 @@ mod tests { fs::remove_dir_all(root).unwrap(); fs::remove_dir_all(outside).unwrap(); } + + #[test] + fn theme_slug_collapses_punctuation_runs() { + assert_eq!(theme_slug("SynthWave '84"), "synthwave-84"); + } } mod setup; @@ -818,6 +823,15 @@ async fn get_app_mode() -> String { } } +fn theme_slug(value: &str) -> String { + let lowercase = value.to_lowercase(); + lowercase + .split(|c: char| !c.is_alphanumeric()) + .filter(|segment| !segment.is_empty()) + .collect::>() + .join("-") +} + #[tauri::command] async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result { use std::io::{Cursor, Read}; @@ -892,9 +906,7 @@ async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result Date: Fri, 31 Jul 2026 13:59:30 +0800 Subject: [PATCH 9/9] fix: stop associating plain text files --- scripts/fileAssociations.test.ts | 13 +++++++++++++ src-tauri/tauri.conf.json | 8 -------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 scripts/fileAssociations.test.ts diff --git a/scripts/fileAssociations.test.ts b/scripts/fileAssociations.test.ts new file mode 100644 index 00000000..03ade7b9 --- /dev/null +++ b/scripts/fileAssociations.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const config = JSON.parse(readFileSync('src-tauri/tauri.conf.json', 'utf8')) as { + bundle: { fileAssociations: Array<{ ext: string[] }> }; +}; + +test('installer advertises only Markdown file associations', () => { + const extensions = config.bundle.fileAssociations.flatMap((association) => association.ext); + assert.deepEqual(extensions, ['md', 'markdown']); + assert.equal(extensions.includes('txt'), false); +}); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3c9c6b8a..d416c61e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -49,14 +49,6 @@ "name": "Markdown File", "description": "Opens Markdown files with Markpad", "role": "Editor" - }, - { - "ext": ["txt"], - "contentTypes": ["public.plain-text"], - "mimeType": "text/plain", - "name": "Text File", - "description": "Opens Text files with Markpad", - "role": "Editor" } ], "windows": {