diff --git a/package.json b/package.json
index 1a7f250..541878f 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "helixnotes",
"private": true,
"license": "AGPL-3.0-or-later",
- "version": "1.0.2",
+ "version": "1.0.3",
"type": "module",
"scripts": {
"dev": "vite dev",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index fd9dee4..96647bf 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1778,7 +1778,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helixnotes"
-version = "1.0.2"
+version = "1.0.3"
dependencies = [
"chrono",
"dirs",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index fd3a7da..9df50de 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helixnotes"
-version = "1.0.2"
+version = "1.0.3"
description = "Local-first markdown note-taking app"
authors = ["HelixNotes"]
license = "AGPL-3.0-or-later"
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 6e68341..0b00e29 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -1350,6 +1350,8 @@ pub fn get_install_type() -> String {
"windows".to_string()
} else if std::env::var("APPIMAGE").is_ok() {
"appimage".to_string()
+ } else if std::path::Path::new("/var/lib/dpkg/info/helix-notes.list").exists() {
+ "deb".to_string()
} else {
"native".to_string()
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 5bb5154..4f59327 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HelixNotes",
- "version": "1.0.2",
+ "version": "1.0.3",
"identifier": "com.helixnotes.app",
"build": {
"frontendDist": "../build",
diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte
index aa9af0d..0e2c430 100644
--- a/src/lib/components/AppLayout.svelte
+++ b/src/lib/components/AppLayout.svelte
@@ -20,11 +20,15 @@
showCommandPalette,
theme,
focusMode,
- activeNote
+ activeNote,
+ activeNotePath,
+ editorDirty,
+ showInfo,
+ showSettings
} from '$lib/stores/app';
const appWindow = getCurrentWindow();
- import { loadVaultState, saveVaultState } from '$lib/api';
+ import { loadVaultState, saveVaultState, readNote } from '$lib/api';
import { debounce } from '$lib/utils/debounce';
import type { VaultState, FileEvent } from '$lib/types';
@@ -32,6 +36,39 @@
let noteList: NoteList;
let editor: Editor;
let unlistenFileChange: (() => void) | null = null;
+ let navigatingFromHistory = false;
+ let noteHistory: string[] = [];
+ let noteHistoryIndex = -1;
+
+ // Track note navigation in history stack
+ $effect(() => {
+ const path = $activeNotePath;
+ if (navigatingFromHistory) {
+ navigatingFromHistory = false;
+ return;
+ }
+ if (path) {
+ // Trim forward history and push
+ noteHistory = [...noteHistory.slice(0, noteHistoryIndex + 1), path];
+ noteHistoryIndex = noteHistory.length - 1;
+ }
+ });
+
+ function navigateHistory(direction: -1 | 1) {
+ const newIndex = noteHistoryIndex + direction;
+ if (newIndex < 0 || newIndex >= noteHistory.length) return;
+ const path = noteHistory[newIndex];
+ noteHistoryIndex = newIndex;
+ navigatingFromHistory = true;
+ readNote(path).then((content) => {
+ $activeNote = content;
+ $activeNotePath = path;
+ $editorDirty = false;
+ editor?.loadNote(path, content.content);
+ }).catch(() => {
+ // Note may have been deleted, ignore
+ });
+ }
const persistState = debounce(async () => {
const state: VaultState = {
@@ -69,7 +106,22 @@
editor?.focusTitle();
}
+ function handleMouseDown(e: MouseEvent) {
+ if (e.button === 3) { e.preventDefault(); navigateHistory(-1); }
+ if (e.button === 4) { e.preventDefault(); navigateHistory(1); }
+ }
+
function handleKeydown(e: KeyboardEvent) {
+ if (e.altKey && e.key === 'ArrowLeft') {
+ e.preventDefault();
+ navigateHistory(-1);
+ return;
+ }
+ if (e.altKey && e.key === 'ArrowRight') {
+ e.preventDefault();
+ navigateHistory(1);
+ return;
+ }
if (e.ctrlKey && !e.shiftKey && e.key === 'n') {
e.preventDefault();
createAndFocusNote();
@@ -77,9 +129,19 @@
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
e.preventDefault();
}
- if (e.ctrlKey && e.key === 'f') {
+ if (e.ctrlKey && e.shiftKey && e.key === 'F') {
e.preventDefault();
$showSearch = true;
+ return;
+ }
+ if (e.ctrlKey && !e.shiftKey && e.key === 'f') {
+ e.preventDefault();
+ if ($activeNotePath) {
+ editor?.openNoteSearch();
+ } else {
+ $showSearch = true;
+ }
+ return;
}
if (e.ctrlKey && e.key === 'p') {
e.preventDefault();
@@ -90,7 +152,9 @@
editor?.forceSave();
}
if (e.key === 'Escape') {
- if ($focusMode) $focusMode = false;
+ if ($showSettings) $showSettings = false;
+ else if ($showInfo) $showInfo = false;
+ else if ($focusMode) $focusMode = false;
else if ($showSearch) $showSearch = false;
else if ($showCommandPalette) $showCommandPalette = false;
}
@@ -142,7 +206,7 @@
});
-
+
{#if $focusMode}
diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte
index 9fdc561..44b1676 100644
--- a/src/lib/components/Editor.svelte
+++ b/src/lib/components/Editor.svelte
@@ -93,6 +93,14 @@
let historySelected = $state
(null);
let historyLoading = $state(false);
+ // In-note search
+ let noteSearchOpen = $state(false);
+ let noteSearchQuery = $state('');
+ let noteSearchIndex = $state(0);
+ let noteSearchResults = $state<{from: number, to: number}[]>([]);
+ let noteSearchInput = $state(null!);
+ const noteSearchPluginKey = new PluginKey('noteSearch');
+
// Slash commands
let slashMenu = $state<{ x: number; y: number; query: string; from: number; to: number } | null>(null);
let slashSelectedIndex = $state(0);
@@ -267,6 +275,31 @@
codeLangDropdown = null;
}
+ // ── In-note search extension ──
+ const NoteSearchExtension = Extension.create({
+ name: 'noteSearch',
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ key: noteSearchPluginKey,
+ state: {
+ init() { return DecorationSet.empty; },
+ apply(tr, old) {
+ const meta = tr.getMeta(noteSearchPluginKey);
+ if (meta !== undefined) return meta;
+ return old.map(tr.mapping, tr.doc);
+ },
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state);
+ },
+ },
+ }),
+ ];
+ },
+ });
+
const CodeBlockLanguageSelect = Extension.create({
name: 'codeBlockLanguageSelect',
addProseMirrorPlugins() {
@@ -397,7 +430,14 @@
closeSlashMenu();
return true;
}
- if (event.key === 'ArrowRight' || event.key === 'Tab') {
+ if (event.key === 'Tab') {
+ event.preventDefault();
+ if (slashTableHover.rows > 0 && slashTableHover.cols > 0) {
+ slashInsertTable(slashTableHover.rows, slashTableHover.cols);
+ }
+ return true;
+ }
+ if (event.key === 'ArrowRight') {
event.preventDefault();
slashTableHover = { rows: Math.max(1, slashTableHover.rows), cols: Math.min(10, (slashTableHover.cols || 0) + 1) };
return true;
@@ -436,7 +476,7 @@
slashSelectedIndex = (slashSelectedIndex - 1 + slashFiltered.length) % Math.max(1, slashFiltered.length);
return true;
}
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' || event.key === 'Tab') {
if (slashFiltered.length > 0) {
event.preventDefault();
executeSlashCommand(slashSelectedIndex);
@@ -1038,9 +1078,12 @@
return '```' + lang + '\n' + code + '\n```\n';
}
case 'blockquote': {
- const inner: string[] = [];
- node.forEach((child: any) => inner.push(serializeNode(child)));
- return inner.join('').split('\n').filter((l: string) => l !== '').map((l: string) => '> ' + l).join('\n') + '\n';
+ const blocks: string[] = [];
+ node.forEach((child: any) => {
+ const lines = serializeNode(child).replace(/\n$/, '').split('\n');
+ blocks.push(lines.map((l: string) => '> ' + l).join('\n'));
+ });
+ return blocks.join('\n>\n') + '\n';
}
case 'bulletList': {
const items: string[] = [];
@@ -1144,6 +1187,82 @@
return parts.join('');
}
+ function autofocus(el: HTMLElement) {
+ requestAnimationFrame(() => el.focus());
+ }
+
+ // ── In-note search functions ──
+ function updateNoteSearch(query: string) {
+ if (!editor) return;
+ if (!query) {
+ noteSearchResults = [];
+ noteSearchIndex = 0;
+ const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
+ editor.view.dispatch(tr);
+ return;
+ }
+ const results: {from: number, to: number}[] = [];
+ const lowerQuery = query.toLowerCase();
+ editor.state.doc.descendants((node, pos) => {
+ if (!node.isText || !node.text) return;
+ const text = node.text.toLowerCase();
+ let idx = text.indexOf(lowerQuery);
+ while (idx !== -1) {
+ results.push({ from: pos + idx, to: pos + idx + query.length });
+ idx = text.indexOf(lowerQuery, idx + 1);
+ }
+ });
+ noteSearchResults = results;
+ if (noteSearchIndex >= results.length) noteSearchIndex = 0;
+ applySearchDecorations();
+ }
+
+ function applySearchDecorations() {
+ if (!editor) return;
+ const decorations = noteSearchResults.map((m, i) =>
+ Decoration.inline(m.from, m.to, { class: i === noteSearchIndex ? 'note-search-match note-search-active' : 'note-search-match' })
+ );
+ const decoSet = DecorationSet.create(editor.state.doc, decorations);
+ const tr = editor.state.tr.setMeta(noteSearchPluginKey, decoSet);
+ editor.view.dispatch(tr);
+ scrollToCurrentMatch();
+ }
+
+ function scrollToCurrentMatch() {
+ if (!editor || noteSearchResults.length === 0) return;
+ const match = noteSearchResults[noteSearchIndex];
+ editor.commands.setTextSelection(match.from);
+ editor.commands.scrollIntoView();
+ }
+
+ function noteSearchNext() {
+ if (noteSearchResults.length === 0) return;
+ noteSearchIndex = (noteSearchIndex + 1) % noteSearchResults.length;
+ applySearchDecorations();
+ }
+
+ function noteSearchPrev() {
+ if (noteSearchResults.length === 0) return;
+ noteSearchIndex = (noteSearchIndex - 1 + noteSearchResults.length) % noteSearchResults.length;
+ applySearchDecorations();
+ }
+
+ export function openNoteSearch() {
+ noteSearchOpen = true;
+ }
+
+ function closeNoteSearch() {
+ noteSearchOpen = false;
+ noteSearchQuery = '';
+ noteSearchResults = [];
+ noteSearchIndex = 0;
+ if (editor) {
+ const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
+ editor.view.dispatch(tr);
+ editor.commands.focus();
+ }
+ }
+
function stripAssetSrc(src: string): string {
// blob: URLs are not persistable — they were temporary browser references
if (src.startsWith('blob:')) return '';
@@ -1418,6 +1537,7 @@
DetailsContent,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
SlashCommands,
+ NoteSearchExtension,
...($appConfig?.enable_wiki_links ? [WikiLink, WikiLinkAutocomplete] : []),
],
content: html,
@@ -2294,6 +2414,16 @@
{#if readOnly}
View Mode
{/if}
+