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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": {
"@tauri-apps/api": "^2.6.0",
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-opener": "^2.5.0",
"fuse.js": "^7.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
Expand Down
56 changes: 35 additions & 21 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,7 @@ fn index_applications() -> Vec<FileItem> {
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if is_app_file(&path.to_path_buf()) {
let path = entry.path(); if is_app_file(&path.to_path_buf()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Formatting glitch and missed AppImage match.

Split statements for readability; also ensure AppImage is detected (is_app_file currently matches "AppImage" but extensions are lowercased).

-                let path = entry.path();                if is_app_file(&path.to_path_buf()) {
+                let path = entry.path();
+                if is_app_file(&path.to_path_buf()) {

Additionally (outside this range), fix extension match:

-        "deb" | "rpm" | "AppImage" => true, // Linux
+        "deb" | "rpm" | "appimage" => true, // Linux
📝 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 path = entry.path(); if is_app_file(&path.to_path_buf()) {
let path = entry.path();
if is_app_file(&path.to_path_buf()) {
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around line 245, split the combined let/path check into
two statements for readability (first let path = entry.path(); then an if using
path), and update is_app_file so AppImage detection is case-insensitive by
lowercasing the extension or filename before matching (currently it matches
"AppImage" literally); also adjust the other extension-match locations (outside
this range) to compare lowercase extensions or names so matches like ".AppImage"
or "AppImage" in different cases are detected consistently.

if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) {
let modified = metadata
.modified()
Expand Down Expand Up @@ -497,6 +496,12 @@ fn refresh_file_index(
Ok(())
}

#[tauri::command]
fn hide_window(app: tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
fn start_clipboard_monitor(app_handle: tauri::AppHandle, db: Arc<Mutex<ClipboardDatabase>>) {
std::thread::spawn(move || {
let mut last_content = String::new();
Expand Down Expand Up @@ -545,8 +550,31 @@ fn start_clipboard_monitor(app_handle: tauri::AppHandle, db: Arc<Mutex<Clipboard
}

pub fn run() {
// --- FIX 1: Define the handler logic ---
// This handler will be attached to the main builder.
// It must be able to check *which* shortcut was pressed.
let shortcut_handler = ShortcutBuilder::new()
.with_handler(move |app, scut, event| {
// Re-create the shortcut struct to compare its ID
let shortcut = Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Space);

if scut.id() == shortcut.id() && event.state() == ShortcutState::Pressed {
let win = app.get_webview_window("main").expect("window not found");
if win.is_visible().unwrap_or(false) {
let _ = win.hide();
} else {
let _ = win.show();
let _ = win.set_focus();
}
}
})
.build();

Comment on lines +553 to +572

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Shortcut handler logic is sound; minor hardening optional.

Comparison via id + Pressed state is correct. Consider logging errors from show()/hide()/set_focus() for diagnosability.

🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 553 to 572, the shortcut handler currently
ignores results from window operations (show/hide/set_focus); update the handler
to check the Result returned by each call and log any errors for diagnosability.
Specifically, after calling win.hide(), win.show(), and win.set_focus(), match
or use if let Err(e) to capture errors and forward them to your logging facility
(e.g., processLogger/tracing or the crate logger) with a clear message including
the operation and the error; keep the existing visibility toggle logic intact.

tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_clipboard_manager::init())
// --- Add the handler plugin ---
.plugin(shortcut_handler)
.setup(|app| {
// Initialize clipboard database
let db_path = get_db_path(&app.handle());
Expand All @@ -573,27 +601,12 @@ pub fn run() {

#[cfg(desktop)]
{
// --- FIX 2: Register the shortcut ---
// The v2 register() function does NOT take a closure,
// as the handler is already registered above.
let shortcut =
Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Space);
let handle = app.handle();

handle.plugin(
ShortcutBuilder::new()
.with_handler(move |app, scut, event| {
if scut.id() == shortcut.id() && event.state() == ShortcutState::Pressed
{
let win = app.get_webview_window("main").expect("window not found");
if win.is_visible().unwrap_or(false) {
let _ = win.hide();
} else {
let _ = win.show();
let _ = win.set_focus();
}
}
})
.build(),
)?;


app.global_shortcut().register(shortcut)?;
}
Ok(())
Expand All @@ -609,6 +622,7 @@ pub fn run() {
get_recent_files,
open_file,
refresh_file_index,
hide_window,
])
.run(tauri::generate_context!())
.expect("error while running tauri");
Expand Down
67 changes: 47 additions & 20 deletions src/components/HomeOptions.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useKeyboardNavigation } from "../hooks/useKeyboardNavigation";
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import Fuse from "fuse.js";

const OPTIONS = [
Expand All @@ -14,29 +16,54 @@ export default function HomeOptions({ query, onSelect, clearQuery }) {
? fuse.search(query).map((result) => result.item)
: OPTIONS;

const { getItemProps } = useKeyboardNavigation(filtered, (item) => {
onSelect(item.page);
clearQuery();
const handleSearch = async (searchQuery) => {
if (!searchQuery.trim()) return;

try {
// Construct search URL (Google search)
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}`;

// Open URL in browser
await openUrl(searchUrl);

// Hide the PathFinder window
await invoke('hide_window');

// Clear query
clearQuery();
} catch (error) {
console.error("Failed to open browser:", error);
}
};
Comment on lines +19 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting the duplicate handleSearch logic.

The implementation is correct and handles the search flow properly (URL construction, opening browser, hiding window, clearing query). However, this logic is nearly identical to the handleSearch function in OnlineSearchPage.jsx (lines 14-29).

Consider extracting this shared logic into a custom hook or utility function:

// src/utils/webSearch.js or src/hooks/useWebSearch.js
export async function performWebSearch(searchQuery, clearQuery) {
  if (!searchQuery.trim()) return;

  try {
    const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}`;
    await openUrl(searchUrl);
    await invoke('hide_window');
    clearQuery();
  } catch (error) {
    console.error("Failed to open browser:", error);
  }
}

Then use it in both components:

const handleSearch = async (searchQuery) => {
  await performWebSearch(searchQuery, clearQuery);
};
🤖 Prompt for AI Agents
In src/components/HomeOptions.jsx around lines 19 to 37, the handleSearch
implementation duplicates logic found in OnlineSearchPage.jsx; extract the
shared behavior into a single exported utility or hook (e.g.,
src/utils/webSearch.js or src/hooks/useWebSearch.js) that accepts the
searchQuery and a clearQuery callback, moves URL construction, openUrl/invoke
calls and error handling into that function, and then replace the local
handleSearch with a simple call to the new performWebSearch/useWebSearch so both
HomeOptions.jsx and OnlineSearchPage.jsx import and reuse the same
implementation.


// If no matches, show web search option
const itemsToShow = filtered.length > 0
? filtered
: [{ title: `Search online for "${query}"`, icon: "🌐", isWebSearch: true }];

const { getItemProps } = useKeyboardNavigation(itemsToShow, (item) => {
if (item.isWebSearch) {
// Trigger web search
handleSearch(query);
} else {
// Navigate to page
onSelect(item.page);
clearQuery();
}
});

return (
<div className="option-list">
{filtered.length ? (
filtered.map((opt, idx) => (
<div
{...getItemProps(idx)}
className={`option-item ${getItemProps(idx).className}`}
key={idx}
>
<span className="icon">{opt.icon}</span>
<span>{opt.title}</span>
</div>
))
) : (
<div className="option-item selected">
<span className="icon">🌐</span>
<span>Search online for "{query}"</span>
{itemsToShow.map((opt, idx) => (
<div
{...getItemProps(idx)}
className={`option-item ${getItemProps(idx).className}`}
key={idx}
Comment on lines +58 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix className duplication and inefficient getItemProps calls.

There are two issues here:

  1. Duplicate className: Spreading {...getItemProps(idx)} applies the className from the hook (which already includes "option-item"), then explicitly setting className={...} overrides it with a duplicated "option-item" class. Since getItemProps returns className: "option-item selected" (or just "option-item"), the result is className="option-item option-item selected".

  2. Inefficient calls: getItemProps(idx) is called twice per render (lines 59 and 60), which is wasteful.

Apply this diff to fix both issues:

-      {itemsToShow.map((opt, idx) => (
-        <div
-          {...getItemProps(idx)}
-          className={`option-item ${getItemProps(idx).className}`}
-          key={idx}
-        >
+      {itemsToShow.map((opt, idx) => {
+        const itemProps = getItemProps(idx);
+        return (
+        <div
+          {...itemProps}
+          key={idx}
+        >
           <span className="icon">{opt.icon}</span>
           <span>{opt.title}</span>
         </div>
-      ))}
+      )})}

The getItemProps hook already returns the complete className with both "option-item" and the "selected" state, so the spread operator alone is sufficient.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/HomeOptions.jsx around lines 58 to 61, getItemProps(idx) is
called twice and its returned className is being overridden/duplicated by an
explicit className prop; call getItemProps(idx) once, store its result in a
local variable (e.g., const itemProps = getItemProps(idx)), spread only that
object into the div (remove the explicit className override) and keep the key
prop as before so the returned className (which already includes "option-item"
and "selected" when appropriate) is used without duplication and without
redundant calls.

>
<span className="icon">{opt.icon}</span>
<span>{opt.title}</span>
</div>
)}
))}
</div>
);
}
}
33 changes: 29 additions & 4 deletions src/components/OnlineSearchPage.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
import { useEffect } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused import.

useEffect isn’t used anymore.

-import { useEffect } from "react";
+// (unused) import removed

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/OnlineSearchPage.jsx around lines 1 to 1, the import statement
includes useEffect which is no longer used; remove useEffect from the import (or
delete the entire import line if nothing else is imported) to eliminate the
unused import and satisfy the linter.

// 1. Import the correct named function 'openUrl'
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import { useKeyboardNavigation } from "../hooks/useKeyboardNavigation";

export default function OnlineSearchPage({ query }) {
const { getItemProps } = useKeyboardNavigation([query], (item, idx) => {
console.log("Selected:", item);
const { getItemProps } = useKeyboardNavigation([query], async (item, idx) => {
await handleSearch(query);
});
Comment on lines +8 to 10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid stale closure: use the item passed by the hook.

Use item to ensure the searched query matches the selected entry.

-  const { getItemProps } = useKeyboardNavigation([query], async (item, idx) => {
-    await handleSearch(query);
-  });
+  const { getItemProps } = useKeyboardNavigation([query], async (item) => {
+    await handleSearch(item);
+  });
📝 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
const { getItemProps } = useKeyboardNavigation([query], async (item, idx) => {
await handleSearch(query);
});
const { getItemProps } = useKeyboardNavigation([query], async (item) => {
await handleSearch(item);
});
🤖 Prompt for AI Agents
In src/components/OnlineSearchPage.jsx around lines 8 to 10, the keyboard
navigation callback is closing over the external query variable instead of using
the item passed by the hook; update the callback to call handleSearch with the
item provided by useKeyboardNavigation (e.g., handleSearch(item) or
handleSearch(item.value/label depending on the item shape), and validate the
item exists and is a string before invoking handleSearch to avoid stale closures
and incorrect searches.


// 2. Removed the redundant useEffect hook.
// 'useKeyboardNavigation' already handles the Enter key.

const handleSearch = async (searchQuery) => {
if (!searchQuery.trim()) return;

try {
// Construct search URL (Google search)
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}`;

// 3. Call the correct 'openUrl' function
await openUrl(searchUrl);

// Hide the PathFinder window
await invoke('hide_window');
} catch (error) {
console.error("Failed to open browser:", error);
}
};
Comment on lines +15 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Handle permission errors and keep UX responsive.

If openUrl is denied by capabilities, this throws. Consider surfacing a toast and keeping the window visible on failure. Also verify opener scopes include https URLs. Docs detail URL scope configuration. (v2.tauri.app)

🤖 Prompt for AI Agents
In src/components/OnlineSearchPage.jsx around lines 15 to 30, the try/catch
around openUrl doesn't handle permission-denied failures or keep the UI
responsive: update the catch to detect capability/permission errors (e.g., check
error message/code), show a user-facing toast notification describing the
failure, and do NOT call invoke('hide_window') on failure so the PathFinder
window remains visible; additionally, before calling openUrl verify the opener
scope supports https URLs (ensure configuration or gate calls by testing the URL
scheme) and if scope is insufficient surface a clear toast telling the
user/guide them to update opener scopes.


return (
<div className="option-list">
<div
{...getItemProps(0)}
className={`option-item ${getItemProps(0).className}`}
onClick={() => handleSearch(query)}
>
<span className="icon">🌐</span>
<span>Search online for {query}</span>
<span>Search online for "{query}"</span>
</div>
Comment on lines 34 to 41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Drop duplicate className derivation.

getItemProps already includes className; avoid concatenating it again.

Included in the button refactor above.

🧰 Tools
🪛 Biome (2.1.2)

[error] 34-38: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 34-38: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🤖 Prompt for AI Agents
In src/components/OnlineSearchPage.jsx around lines 34 to 41, the div spreads
getItemProps(0) and then appends getItemProps(0).className again which
duplicates and calls getItemProps twice; remove the concatenation and rely on
the className provided by getItemProps (or call getItemProps once into a const
and use its className if you need to augment it), so change the element to use
the single getItemProps result for className without re-appending its className.

⚠️ Potential issue | 🟠 Major

Fix a11y: use a button, not an interactive div.

Addresses Biome errors: static element interactions and missing keyboard equivalence.

-      <div
-        {...getItemProps(0)}
-        className={`option-item ${getItemProps(0).className}`}
-        onClick={() => handleSearch(query)}
-      >
+      <button
+        {...getItemProps(0)}
+        type="button"
+        aria-label={`Search online for ${query}`}
+      >
         <span className="icon">🌐</span>
         <span>Search online for "{query}"</span>
-      </div>
+      </button>

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 34-38: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 34-38: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🤖 Prompt for AI Agents
In src/components/OnlineSearchPage.jsx around lines 34 to 41, the interactive
div used for the "Search online for {query}" item is not accessible; replace it
with a semantic <button type="button"> element (preserve/getItemProps values on
the button, merge className into className prop), remove any manual
role/keyboard handlers, keep the onClick calling handleSearch(query), and add a
concise aria-label if needed (e.g., `aria-label={`Search online for ${query}`}`)
so the control is keyboard-focusable and properly announced.

</div>
);
}
}