Skip to content

fix: force XWayland backend on Linux to fix unresponsive window controls - #182

Open
GihanRathnayake wants to merge 10 commits into
erictli:mainfrom
GihanRathnayake:main
Open

fix: force XWayland backend on Linux to fix unresponsive window controls#182
GihanRathnayake wants to merge 10 commits into
erictli:mainfrom
GihanRathnayake:main

Conversation

@GihanRathnayake

@GihanRathnayake GihanRathnayake commented Jun 26, 2026

Copy link
Copy Markdown

Problem

On Linux under native Wayland (reproduced on Fedora 44 / KDE Plasma / KWin), the window's title bar controls (close, minimize, maximize) are unresponsive on first click. The window's input region isn't committed until a resize, so the controls only start working after the window is maximized/restored.

Fix

Default GDK_BACKEND to x11 (XWayland) on Linux before GTK/WebKitGTK initializes. This sidesteps the native-Wayland input-region issue while leaving an explicit user-set GDK_BACKEND untouched.

#[cfg(target_os = "linux")]
if std::env::var_os("GDK_BACKEND").is_none() {
    std::env::set_var("GDK_BACKEND", "x11");
}
  • Linux-only (#[cfg(target_os = "linux")]); macOS/Windows unaffected.
  • Respects an explicit GDK_BACKEND override.
  • Set in main() before run(), so it applies before any GTK init — works in both tauri dev and shipped tauri build binaries.

Testing

  • cargo build passes.
  • Confirmed manually that GDK_BACKEND=x11 npm run tauri dev makes the close button respond on first click; this change applies that automatically.

Note: PR built and fixed with Claude Code.

Summary by CodeRabbit

  • New Features
    • Add read-only syntax-highlighted plain-text/code note mode, plus a dedicated source flow for non-markdown notes.
    • “New File…” in the sidebar to create extension-appropriate notes; duplication now preserves the correct extension.
    • Search adds Replace (Replace/Replace All).
  • Improvements
    • Extension-aware search, indexing, titles/previews, and “Copy Filepath” across note types.
    • Inter is now available as an editor font; refined typography/list spacing.
    • Sidebar width is resizable and persisted.
  • Bug Fixes
    • Improved Linux startup backend selection; added macOS WebKit spellcheck/autocorrect defaults.

GihanRathnayake and others added 2 commits June 26, 2026 15:09
Under native Wayland (e.g. KWin) the window's input region is not
committed until a resize, leaving the title bar controls unresponsive
until the window is maximized. Default GDK_BACKEND to x11 on Linux,
unless the user has set it explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: force XWayland backend on Linux to fix unresponsive window controls
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds extension-aware note storage, indexing, search, creation, duplication, and editing. It also adds find-and-replace, Inter typography, persistent sidebar resizing, and platform-specific desktop startup and layout behavior.

Changes

Multi-format note workflows

Layer / File(s) Summary
Extension-aware note lifecycle
src/types/note.ts, src/services/notes.ts, src-tauri/src/lib.rs, src/context/NotesContext.tsx, src/components/layout/Sidebar.tsx, src/components/notes/*
Supported extensions flow through note contracts, stable IDs, indexing, search results, creation, duplication, file watching, and filepath actions.
Plain-text editor and highlighting
src/components/editor/Editor.tsx, src/components/editor/codeHighlight.ts, src/components/editor/lowlight.ts
Non-markdown notes bypass markdown parsing, render syntax-highlighted previews, and use direct textarea editing.

Editor search replacement

Layer / File(s) Summary
Find-and-replace editor flow
src/components/editor/Editor.tsx, src/components/editor/SearchToolbar.tsx, src/components/icons/index.tsx
Replacement state, keyboard handling, editor transactions, toolbar controls, and replacement icons are added.

Desktop UI and settings

Layer / File(s) Summary
Typography, sidebar resizing, and platform layout
package.json, src/main.tsx, src/context/ThemeContext.tsx, src/components/settings/EditorSettingsSection.tsx, src/App.css, src/lib/sidebar.ts, src/components/layout/SidebarResizeHandle.tsx, src/App.tsx, src/lib/platform.ts, src/components/layout/*, src/components/settings/SettingsPage.tsx, src-tauri/*
Inter is loaded and selectable, sidebar width is persisted and adjustable, and Linux, macOS, and Windows-specific startup and layout behavior is updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • erictli/scratch#2: Both PRs modify the theme and typography settings stack, including shared font and editor settings.
  • erictli/scratch#7: Both PRs modify the shared editor search toolbar.
  • erictli/scratch#180: Both PRs implement editor find-and-replace behavior and related toolbar controls.

Suggested reviewers: erictli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main Linux-only change to force XWayland and fix unresponsive window controls.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch main
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

GihanRathnayake and others added 3 commits June 26, 2026 17:32
Bundle the Inter variable font (self-hosted via @fontsource-variable/inter,
works offline) and expose it as a fourth editor font alongside Sans, Serif,
and Mono.

- Add "inter" to the FontFamily type, font map, and settings dropdown
- Export fontFamilyMap and reuse it for the settings preview (removes a
  duplicated, now-incomplete font-stack ternary)
- Inter-scoped typographic tuning: negative letter-spacing on body and
  headings, plus font-optical-sizing; other families keep normal tracking
- List spacing polish: gap between sibling items, tighter marker-to-text
  gap, slightly deeper indent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends note discovery, search indexing, and the editor to recognize a
built-in allowlist of code/text extensions (.go, .py, .js, etc.) in
addition to .md, so users can keep code snippets alongside their
markdown notes in the same folder. Non-markdown files get a
syntax-highlighted read view with click-to-edit, reusing the existing
raw-textarea "source mode" plumbing, and a new "New File..." action to
create them from the sidebar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/editor/Editor.tsx (1)

1798-1807: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent copy menu state leak for plain-text notes.

Pressing Cmd+Shift+C on a plain-text note sets copyMenuOpen to true. Since the dropdown is hidden for plain-text notes, the menu won't appear immediately, but the state lingers. As a result, the export dropdown will spontaneously pop open the next time the user clicks on a markdown note.

🛠️ Proposed fix to ignore the shortcut for plain text
   // Keyboard shortcut for Cmd+Shift+C to open copy menu
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "c") {
+        if (isPlainTextNote) return;
         e.preventDefault();
         setCopyMenuOpen(true);
       }
     };
     document.addEventListener("keydown", handleKeyDown);
     return () => document.removeEventListener("keydown", handleKeyDown);
-  }, []);
+  }, [isPlainTextNote]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/Editor.tsx` around lines 1798 - 1807, Update the
keydown handler in the editor’s copy-menu useEffect to ignore Cmd/Ctrl+Shift+C
when the current note is plain text, so it never calls setCopyMenuOpen(true) in
that mode. Preserve the existing shortcut behavior for markdown notes and the
listener cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 1466-1476: Update the ID-building logic in both create_file and
duplicate_note so extension omission occurs only for the literal "md", matching
id_from_abs_path; preserve "markdown" in generated IDs and filenames. Extract
the duplicated closure logic into a shared build_note_id helper if practical,
and use it at both call sites so .markdown files retain their requested
extension.
- Around line 738-750: Update the extension check in the ID construction flow
around is_supported_note_extension so `.md` matching is case-insensitive. Ensure
any casing of the Markdown extension uses the strip_suffix behavior and produces
an extension-less legacy ID, while preserving extension retention for other
supported note types.
- Around line 1554-1607: Update duplicate_note after extract_title_for computes
title so markdown duplicates receive a distinct title by appending “ (Copy)”.
Apply this only when is_markdown_ext(&ext) is true, while preserving the
existing extracted title for non-markdown files.

In `@src/components/editor/Editor.tsx`:
- Around line 1424-1427: Prevent debounced source-mode edits from being
discarded by tracking the latest sourceContent in a ref and adding a flusher
that persists pending changes immediately. Invoke this flusher before cancelling
sourceTimeoutRef in both plain-text and markdown note-switch paths, and during
component unmount, while preserving the existing timeout cleanup.

---

Outside diff comments:
In `@src/components/editor/Editor.tsx`:
- Around line 1798-1807: Update the keydown handler in the editor’s copy-menu
useEffect to ignore Cmd/Ctrl+Shift+C when the current note is plain text, so it
never calls setCopyMenuOpen(true) in that mode. Preserve the existing shortcut
behavior for markdown notes and the listener cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60dfcc7d-cf0d-4ea4-b59f-e111f6f385ad

📥 Commits

Reviewing files that changed from the base of the PR and between a638f02 and c57f734.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • package.json
  • src-tauri/src/lib.rs
  • src-tauri/src/main.rs
  • src/App.css
  • src/components/editor/Editor.tsx
  • src/components/editor/codeHighlight.ts
  • src/components/editor/lowlight.ts
  • src/components/layout/Sidebar.tsx
  • src/components/notes/FolderNameDialog.tsx
  • src/components/notes/FolderTreeView.tsx
  • src/components/notes/NoteList.tsx
  • src/components/settings/EditorSettingsSection.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
  • src/main.tsx
  • src/services/notes.ts
  • src/types/note.ts

Comment thread src-tauri/src/lib.rs
Comment on lines +738 to +750
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/");

let id = if ext == "md" {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Case-sensitive .md check breaks the "extension-less legacy ID" invariant for non-lowercase .md files.

ext == "md" is a case-sensitive literal comparison, but is_supported_note_extension (the fallback branch) matches case-insensitively. A file named e.g. NOTES.MD fails the first check and falls into the "keep extension in ID" branch, producing ID "NOTES.MD" instead of the intended legacy extension-less "NOTES".

Concretely, extension_from_id still lowercases this to "md", so NoteMetadata.extension reports "md" while note.id still embeds .MD. Downstream, FolderTreeView.handleCopyFilepath (note.extension.toLowerCase() === "md" ? folder+"/"+note.id+".md" : ...) then appends .md a second time, producing a broken path like .../NOTES.MD.md. This persists until the note is edited and saved (which triggers the markdown rename path).

🐛 Proposed fix
-    let id = if ext == "md" {
+    let id = if ext.eq_ignore_ascii_case("md") {
         // Strip .md by converting to string and trimming (avoids with_extension
         // which breaks on stems containing dots like "meeting.2024-01-15.md").
-        rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
+        rel_str.strip_suffix(&format!(".{ext}"))?.replace(std::path::MAIN_SEPARATOR, "/")
     } else if is_supported_note_extension(ext) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/");
let id = if ext == "md" {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = if ext.eq_ignore_ascii_case("md") {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(&format!(".{ext}"))?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 738 - 750, Update the extension check in
the ID construction flow around is_supported_note_extension so `.md` matching is
case-insensitive. Ensure any casing of the Markdown extension uses the
strip_suffix behavior and produces an extension-less legacy ID, while preserving
extension retention for other supported note types.

Comment thread src-tauri/src/lib.rs
Comment on lines +1466 to +1476
let build_id = |s: &str| -> String {
let leaf = if is_markdown_ext(&ext) {
s.to_string()
} else {
format!("{}.{}", s, ext)
};
match target_folder.as_deref() {
Some(prefix) if !prefix.is_empty() => format!("{}/{}", prefix.trim_end_matches('/'), leaf),
_ => leaf,
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

is_markdown_ext misused to decide ID-extension omission — .markdown files silently become .md.

Both create_file and duplicate_note use is_markdown_ext(&ext) (true for both "md" and "markdown") to decide whether to omit the extension when building the note ID/leaf. But the ID scheme (see id_from_abs_path) only omits the extension for literal "md".markdown files always keep their extension embedded.

Concretely, creating a file named notes.markdown via create_file: build_id omits the extension (since is_markdown_ext("markdown") is true) → the resulting ID has no extension → abs_path_from_id appends .md by default → the file is actually written as notes.md, silently changing the extension the user explicitly requested. The returned Note.extension ("markdown") then no longer matches the real file's extension. The same pattern reproduces in duplicate_note for existing .markdown notes, producing a mangled filename like readme.markdown-copy.md instead of readme-copy.markdown.

🐛 Proposed fix (both sites)
 // create_file (~1466-1476)
     let build_id = |s: &str| -> String {
-        let leaf = if is_markdown_ext(&ext) {
+        let leaf = if ext == "md" {
             s.to_string()
         } else {
             format!("{}.{}", s, ext)
         };
         match target_folder.as_deref() {
             Some(prefix) if !prefix.is_empty() => format!("{}/{}", prefix.trim_end_matches('/'), leaf),
             _ => leaf,
         }
     };
 // duplicate_note (~1563-1579)
-    let stem = if is_markdown_ext(&ext) {
+    let stem = if ext == "md" {
         leaf
     } else {
         leaf.strip_suffix(&format!(".{ext}")).unwrap_or(&leaf).to_string()
     };

     let build_id = |s: &str| -> String {
-        let leaf = if is_markdown_ext(&ext) {
+        let leaf = if ext == "md" {
             s.to_string()
         } else {
             format!("{}.{}", s, ext)
         };
         match &dir_prefix {
             Some(prefix) => format!("{}/{}", prefix, leaf),
             None => leaf,
         }
     };

Consider extracting a shared fn build_note_id(dir_or_folder: Option<&str>, stem: &str, ext: &str) -> String helper used by both commands — the duplicated logic is exactly why this bug exists in two places at once.

Also applies to: 1563-1579

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 1466 - 1476, Update the ID-building logic
in both create_file and duplicate_note so extension omission occurs only for the
literal "md", matching id_from_abs_path; preserve "markdown" in generated IDs
and filenames. Extract the duplicated closure logic into a shared build_note_id
helper if practical, and use it at both call sites so .markdown files retain
their requested extension.

Comment thread src-tauri/src/lib.rs
Comment on lines +1554 to +1607
let content = fs::read_to_string(&source_path)
.await
.map_err(|e| e.to_string())?;
let ext = extension_from_id(&id);

let (dir_prefix, leaf) = match id.rfind('/') {
Some(pos) => (Some(id[..pos].to_string()), id[pos + 1..].to_string()),
None => (None, id.clone()),
};
let stem = if is_markdown_ext(&ext) {
leaf
} else {
leaf.strip_suffix(&format!(".{ext}")).unwrap_or(&leaf).to_string()
};

let build_id = |s: &str| -> String {
let leaf = if is_markdown_ext(&ext) {
s.to_string()
} else {
format!("{}.{}", s, ext)
};
match &dir_prefix {
Some(prefix) => format!("{}/{}", prefix, leaf),
None => leaf,
}
};

let mut new_stem = format!("{}-copy", stem);
let mut new_id = build_id(&new_stem);
let mut counter = 1;
while abs_path_from_id(&folder_path, &new_id)
.map(|p| p.exists())
.unwrap_or(false)
{
new_stem = format!("{}-copy-{}", stem, counter);
new_id = build_id(&new_stem);
counter += 1;
}

let dest_path = abs_path_from_id(&folder_path, &new_id)?;
fs::write(&dest_path, &content)
.await
.map_err(|e| e.to_string())?;

let metadata = fs::metadata(&dest_path)
.await
.map_err(|e| e.to_string())?;
let modified = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let title = extract_title_for(&content, &new_id, &ext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant functions and surrounding code.
rg -n "duplicateNote|extract_title_for|title =|build_id|is_markdown_ext|extension_from_id|abs_path_from_id" src-tauri/src/lib.rs src-tauri -S

# Show the implementation slice around the duplication logic.
sed -n '1500,1645p' src-tauri/src/lib.rs

# Show the title extraction helper if present in this file.
sed -n '1,420p' src-tauri/src/lib.rs

Repository: erictli/scratch

Length of output: 28656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where note titles are displayed / sourced on the frontend if needed.
rg -n "title|duplicateNote|id\.|extract_title|markdown" src-tauri/src frontend src -S

Repository: erictli/scratch

Length of output: 39012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '497,548p' src-tauri/src/lib.rs
sed -n '108,126p' src/components/notes/NoteList.tsx
sed -n '1,40p' src/lib/utils.ts

Repository: erictli/scratch

Length of output: 3694


Duplicated markdown notes need a distinct title. duplicate_note copies the markdown content verbatim, and extract_title_for derives the new title from that same content. Since the sidebar renders title, the copy is indistinguishable from the original; append (Copy) when duplicating markdown notes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 1554 - 1607, Update duplicate_note after
extract_title_for computes title so markdown duplicates receive a distinct title
by appending “ (Copy)”. Apply this only when is_markdown_ext(&ext) is true,
while preserving the existing extracted title for non-markdown files.

Comment on lines +1424 to +1427
if (sourceTimeoutRef.current) {
clearTimeout(sourceTimeoutRef.current);
sourceTimeoutRef.current = null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent data loss by flushing pending source mode saves.

When switching notes, clearing sourceTimeoutRef.current without saving discards any keystrokes made within the 300ms debounce window. Since plain-text notes exclusively use the textarea (sourceMode), this bug now consistently affects the primary editing path for plain-text files.

Track the latest sourceContent in a ref and create a flusher to persist pending changes before the timeout is cancelled during a note switch or component unmount.

💾 Proposed fix for source mode saves

First, track the source content and add a flush callback (e.g., around line 612 near the other refs):

   const notesCtxRef = useRef(notesCtx);
   notesCtxRef.current = notesCtx;
 
+  // Track latest source content for flushing on note switch
+  const sourceContentRef = useRef(sourceContent);
+  useEffect(() => {
+    sourceContentRef.current = sourceContent;
+  }, [sourceContent]);
+
+  const flushPendingSourceSave = useCallback(async () => {
+    if (sourceTimeoutRef.current && loadedNoteIdRef.current) {
+      clearTimeout(sourceTimeoutRef.current);
+      sourceTimeoutRef.current = null;
+      await saveImmediately(loadedNoteIdRef.current, sourceContentRef.current);
+    }
+  }, [saveImmediately]);
+
   // Keep ref in sync with current note ID
   currentNoteIdRef.current = currentNote?.id ?? null;

Then, replace the cancellation blocks in both the plain-text and markdown note-switch paths (lines ~1424 and ~1470):

-      if (sourceTimeoutRef.current) {
-        clearTimeout(sourceTimeoutRef.current);
-        sourceTimeoutRef.current = null;
-      }
+      if (sourceTimeoutRef.current) {
+        flushPendingSourceSave();
+      }

Finally, flush the save on unmount (around line 1581):

       if (needsSaveRef.current && editorRef.current) {
         needsSaveRef.current = false;
         const manager = editorRef.current.storage.markdown?.manager;
         const markdown = manager
           ? manager.serialize(editorRef.current.getJSON())
           : editorRef.current.getText();
         // Fire and forget - save will complete in background
         saveNote(markdown);
       }
+      if (sourceTimeoutRef.current && loadedNoteIdRef.current) {
+        clearTimeout(sourceTimeoutRef.current);
+        sourceTimeoutRef.current = null;
+        saveNote(sourceContentRef.current, loadedNoteIdRef.current);
+      }
       if (linkPopupRef.current) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (sourceTimeoutRef.current) {
clearTimeout(sourceTimeoutRef.current);
sourceTimeoutRef.current = null;
}
if (sourceTimeoutRef.current) {
flushPendingSourceSave();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/Editor.tsx` around lines 1424 - 1427, Prevent debounced
source-mode edits from being discarded by tracking the latest sourceContent in a
ref and adding a flusher that persists pending changes immediately. Invoke this
flusher before cancelling sourceTimeoutRef in both plain-text and markdown
note-switch paths, and during component unmount, while preserving the existing
timeout cleanup.

GihanRathnayake and others added 2 commits July 29, 2026 13:59
…om upstream v1.0.0

Ports several features and fixes from erictli/scratch's v1.0.0 release that
don't conflict with this fork's Linux window-controls fix, Inter font
option, or plain-text/code file editing support:

- Find & Replace with Replace/Replace All actions, platform-aware shortcut
  (⌥⌘F on macOS, Ctrl+H elsewhere) (upstream erictli#180)
- Drag-to-resize sidebar width, persisted per-folder (upstream erictli#170)
- Linux window-controls config (tauri.linux.conf.json, visible:true +
  show()/set_focus()), complementary to this fork's existing
  GDK_BACKEND=x11 fix — different root cause, same symptom (upstream erictli#160)
- macOS autocorrect/spellcheck support via WebKit defaults (upstream erictli#150)
- Windows native titlebar layout fix (upstream erictli#163)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Fedora/Ubuntu repos both ship an unrelated package named "scratch"
(MIT's educational Scratch), which Tauri's linux bundler collides with
since it derives the deb/rpm package name from productName lowercased.
Rename to "Scratch Notes" so local builds install as scratch-notes
without conflicting; this also covers any future CI-built releases
since they read the same config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/layout/SidebarResizeHandle.tsx (1)

9-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate --sidebar-width toggle logic across two files. Both sites implement the same "null removes the CSS property, otherwise sets it to ${px}px" logic independently, risking drift if the variable name or clamping semantics change.

  • src/components/layout/SidebarResizeHandle.tsx#L9-L16: keep applyWidthVar but move it into a shared module (e.g. src/lib/sidebar.ts) so both files import the same implementation.
  • src/context/ThemeContext.tsx#L358-L366: replace the inline null/px branch in this effect with the shared helper from src/lib/sidebar.ts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/SidebarResizeHandle.tsx` around lines 9 - 16, The
duplicated --sidebar-width toggle logic should use one shared implementation. In
src/components/layout/SidebarResizeHandle.tsx lines 9-16, move applyWidthVar
into a shared module such as src/lib/sidebar.ts and import it where needed; in
src/context/ThemeContext.tsx lines 358-366, replace the inline null/px branch
with that shared helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/editor/SearchToolbar.tsx`:
- Around line 95-105: The replacement UI controls rendered by the SearchToolbar
toggle and the controls around onToggleReplace must be keyboard reachable.
Override IconButton’s default tabIndex of -1 with a non-negative tabIndex for
the toggle and the replace buttons referenced around lines 157-171, preserving
their existing click behavior and labels.

---

Nitpick comments:
In `@src/components/layout/SidebarResizeHandle.tsx`:
- Around line 9-16: The duplicated --sidebar-width toggle logic should use one
shared implementation. In src/components/layout/SidebarResizeHandle.tsx lines
9-16, move applyWidthVar into a shared module such as src/lib/sidebar.ts and
import it where needed; in src/context/ThemeContext.tsx lines 358-366, replace
the inline null/px branch with that shared helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 92afe1a1-0f02-45de-960d-f7c8d904b9a3

📥 Commits

Reviewing files that changed from the base of the PR and between c57f734 and e035de8.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • src-tauri/Cargo.toml
  • src-tauri/src/lib.rs
  • src-tauri/tauri.linux.conf.json
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/SearchToolbar.tsx
  • src/components/icons/index.tsx
  • src/components/layout/FolderPicker.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/layout/SidebarResizeHandle.tsx
  • src/components/settings/SettingsPage.tsx
  • src/context/ThemeContext.tsx
  • src/lib/platform.ts
  • src/lib/sidebar.ts
  • src/types/note.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/layout/Sidebar.tsx
  • src-tauri/src/lib.rs
  • src/components/editor/Editor.tsx

Comment on lines 95 to 105
<IconButton
onClick={onPrevious}
disabled={totalMatches === 0}
title={`Previous match (${shift}↵)`}
onClick={onToggleReplace}
title={isReplaceOpen ? "Hide Replace" : "Show Replace"}
className="shrink-0"
>
<ArrowUpIcon className="w-4.5 h-4.5 stroke-[1.5]" />
{isReplaceOpen ? (
<ChevronDownIcon className="w-4 h-4 stroke-[1.8]" />
) : (
<ChevronRightIcon className="w-4 h-4 stroke-[1.8]" />
)}
</IconButton>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the replacement controls keyboard reachable.

IconButton defaults to tabIndex={-1}, so the new toggle and replace buttons are skipped by Tab navigation. Keyboard-only users cannot open the replacement UI or activate its buttons.

Proposed fix
         <IconButton
           onClick={onToggleReplace}
           title={isReplaceOpen ? "Hide Replace" : "Show Replace"}
           className="shrink-0"
+          tabIndex={0}
+          aria-expanded={isReplaceOpen}
         >
...
             <IconButton
               onClick={onReplace}
               disabled={totalMatches === 0}
               title="Replace (↵)"
+              tabIndex={0}
             >
...
             <IconButton
               onClick={onReplaceAll}
               disabled={totalMatches === 0}
               title="Replace All (Ctrl/Cmd+Enter)"
+              tabIndex={0}
             >

Also applies to: 157-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/SearchToolbar.tsx` around lines 95 - 105, The
replacement UI controls rendered by the SearchToolbar toggle and the controls
around onToggleReplace must be keyboard reachable. Override IconButton’s default
tabIndex of -1 with a non-negative tabIndex for the toggle and the replace
buttons referenced around lines 157-171, preserving their existing click
behavior and labels.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/tauri.conf.json`:
- Line 3: Update the preview-window title formatting in the relevant
preview-title logic in lib.rs to replace the remaining “ — Scratch” branding
with “Scratch Notes” or the project’s shared product-name constant. Preserve the
existing title format and behavior otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47aa1fa5-ff56-45ff-be83-0d05c37e3260

📥 Commits

Reviewing files that changed from the base of the PR and between e035de8 and 6329413.

📒 Files selected for processing (1)
  • src-tauri/tauri.conf.json

Comment thread src-tauri/tauri.conf.json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Scratch",
"productName": "Scratch Notes",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the remaining preview-window branding.

src-tauri/src/lib.rs:3912-3961 still formats preview titles with " — Scratch", so preview windows will display the old product name after this rename. Use the new product name or a shared branding constant there as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/tauri.conf.json` at line 3, Update the preview-window title
formatting in the relevant preview-title logic in lib.rs to replace the
remaining “ — Scratch” branding with “Scratch Notes” or the project’s shared
product-name constant. Preserve the existing title format and behavior
otherwise.

GihanRathnayake and others added 3 commits July 29, 2026 15:21
Adds a one-click "Apply Repose Light" preset in the light theme's color
editor, based on Monkeytype's Repose Light theme (cream background, muted
sage accent). Reuses the existing custom-color override system, so it's
fully reversible via the existing "Reset all" control.

bg-muted/bg-emphasis/border are derived from the theme's text color at the
same opacities the default theme uses; selection reuses the default light
theme's highlight color for visual consistency across themes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dark counterpart to the Repose Light preset, based on Monkeytype's Repose
Dark theme. Generalizes the color-editor preset button to pick the right
preset for the mode being edited instead of only offering one for light.

bg-muted/bg-emphasis/border are tinted from the theme's (light) text color
at the same opacities the default dark theme uses; selection reuses the
default dark theme's highlight color for consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a highlighter toolbar button with a 5-color picker (yellow/green/
blue/pink/red) via @tiptap/extension-highlight. Default yellow keeps
serializing as clean ==text== markdown; other colors persist through
save/reload as theme-adaptive inline <mark style="..."> HTML. Splits
the selection color from the highlight color so selected, searched,
and highlighted text stay visually distinct.

Also fixes right-click Copy/Cut losing the active text selection: the
table context-menu handler was unconditionally collapsing the
selection to the click point on every right-click, not just table
ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant