fix: force XWayland backend on Linux to fix unresponsive window controls#182
fix: force XWayland backend on Linux to fix unresponsive window controls#182GihanRathnayake wants to merge 5 commits into
Conversation
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
📝 WalkthroughWalkthroughThe PR adds multi-format note support for recognized text and code files, including extension-aware IDs, indexing, file creation, search metadata, and editor rendering. It also adds Inter font support, typography adjustments, and Linux X11 backend initialization. ChangesMulti-format note workflows
Inter typography
Linux backend initialization
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
actor User
participant Sidebar
participant NotesContext
participant Tauri
participant FileSystem
participant SearchIndex
participant Editor
User->>Sidebar: Select New File
Sidebar->>NotesContext: createFile(filename)
NotesContext->>Tauri: invoke create_file
Tauri->>FileSystem: create file with extension
Tauri->>SearchIndex: index note and extension
Tauri-->>NotesContext: return Note with extension
NotesContext-->>Editor: select created note
Editor->>Editor: render highlighted code or textarea
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment Warning |
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>
Feat/inter editor font
There was a problem hiding this comment.
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 winPrevent copy menu state leak for plain-text notes.
Pressing
Cmd+Shift+Con a plain-text note setscopyMenuOpentotrue. 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
package.jsonsrc-tauri/src/lib.rssrc-tauri/src/main.rssrc/App.csssrc/components/editor/Editor.tsxsrc/components/editor/codeHighlight.tssrc/components/editor/lowlight.tssrc/components/layout/Sidebar.tsxsrc/components/notes/FolderNameDialog.tsxsrc/components/notes/FolderTreeView.tsxsrc/components/notes/NoteList.tsxsrc/components/settings/EditorSettingsSection.tsxsrc/context/NotesContext.tsxsrc/context/ThemeContext.tsxsrc/main.tsxsrc/services/notes.tssrc/types/note.ts
| 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; | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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, | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| 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); |
There was a problem hiding this comment.
🎯 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.rsRepository: 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 -SRepository: 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.tsRepository: 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.
| if (sourceTimeoutRef.current) { | ||
| clearTimeout(sourceTimeoutRef.current); | ||
| sourceTimeoutRef.current = null; | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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.
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_BACKENDtox11(XWayland) on Linux before GTK/WebKitGTK initializes. This sidesteps the native-Wayland input-region issue while leaving an explicit user-setGDK_BACKENDuntouched.#[cfg(target_os = "linux")]); macOS/Windows unaffected.GDK_BACKENDoverride.main()beforerun(), so it applies before any GTK init — works in bothtauri devand shippedtauri buildbinaries.Testing
cargo buildpasses.GDK_BACKEND=x11 npm run tauri devmakes the close button respond on first click; this change applies that automatically.Note: PR built and fixed with Claude Code.
Summary by CodeRabbit