Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# 22+: the test script passes --no-experimental-webstorage, which Node 20 rejects as an
# unknown option. Node 20 reached end of life in April 2026.
node-version: 24
cache: npm
cache-dependency-path: cardinal/package-lock.json

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
- Desktop app lives in `cardinal/` (React UI in `src/`, Tauri/native glue in `src-tauri/`, build output in `cardinal/dist/`).
- Workspace crates (root `Cargo.toml`): `lsf/` (CLI), `cardinal-sdk/` (shared types), `fswalk/`, `fs-icon/`, `namepool/`, `query-segmentation/`, `search-cache/`, `search-cancel/`, `cardinal-syntax/`, `slab-mmap/` (mmap-backed slab for the cache), `was/` (CLI that streams FSEvents via the SDK).
- Tests sit next to code; cross-crate cases belong in each crate’s `tests/` directory. Generated outputs (`target/`, `cardinal/dist/`, vendor bundles) stay out of commits.
- Toolchain pinned via `rust-toolchain.toml` (`nightly-2025-05-09`); install with `rustup toolchain install nightly-2025-05-09`.
- Toolchain pinned via `rust-toolchain.toml` (`nightly-2025-12-11`); install with `rustup toolchain install nightly-2025-12-11`. Let the rustup shim pick it up rather than putting a toolchain's `bin/` on `PATH`: an older rustc still builds here from cache, but the proc-macro dylibs it emits are rejected by recent macOS dyld with `mis-aligned LINKEDIT string pool` as soon as anything has to be rebuilt.
- Keep `target/` out of iCloud-synced folders (`CARGO_TARGET_DIR`). If iCloud evicts a fingerprint file, cargo blocks forever in `read()` waiting for a download that never lands.

## Build, Test, and Development Commands
- `cargo check --workspace` — fast compile validation for all crates.
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.2.0 — 2026-08-01
- Add an accessible name to the search input, so assistive technologies can announce it. Thanks to [@dkattan](https://github.com/dkattan) ([#220](https://github.com/cardisoft/cardinal/pull/220)).
- Add a file-type dropdown next to the search bar. Picking a type writes it into the query (`type:image`), so the control and the search bar never disagree — and the syntax stays visible instead of hidden behind a menu.
- Add the `type:email` category (`.eml`, `.emlx`, `.emlxpart`, `.msg`, `.mbox`), with `mail`/`message` as synonyms.
- Rename "folder scope" to plain "Search in" across all 15 languages.
- Add a context column for `content:` searches, showing the matching text inside each file with the searched term highlighted.
- Derive the highlighted content terms from the query parser itself, so negated terms (`!content:`) no longer highlight and `content:"Bearer "` keeps its trailing space.
- Hydrate result rows in parallel, so icon and content-snippet reads no longer queue up behind each other.

## 0.1.23 — 2026-03-25
- Reduce power consumption by expanding the default ignored paths to cover more macOS cache, log, metadata, and runtime directories.
- Further reduce background work by making the filesystem event watcher honor ignored paths.
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<img src="cardinal/mac-icon_1024x1024.png" alt="Cardinal icon" width="120" height="120">
<h1>Cardinal</h1>
<p>Fastest and most accurate file search app for macOS.</p>
<p>
<a href="https://github.com/franciscoxc/cardinal/releases/download/v0.2.0/Cardinal_0.2.0_aarch64.dmg"><img src="https://img.shields.io/badge/Download-Cardinal%200.2.0%20for%20macOS-D62828?style=for-the-badge&logo=apple&logoColor=white" alt="Download Cardinal 0.2.0 for macOS"></a>
</p>
<p>
Signed and notarized · Apple silicon · macOS 12+
</p>
<p>
<a href="#using-cardinal">Using Cardinal</a> ·
<a href="#building-cardinal">Building Cardinal</a>
Expand All @@ -17,13 +23,9 @@

### Download

Use homebrew:

```bash
brew install --cask cardinal-search
```
[**Download Cardinal 0.2.0 for macOS**](https://github.com/franciscoxc/cardinal/releases/download/v0.2.0/Cardinal_0.2.0_aarch64.dmg) — signed with a Developer ID and notarized by Apple, so it opens without Gatekeeper warnings.

You can also grab the latest packaged builds from [GitHub Releases](https://github.com/cardisoft/cardinal/releases/).
Every build lives in [Releases](https://github.com/franciscoxc/cardinal/releases). Open the DMG, drag Cardinal to Applications, and grant Full Disk Access when macOS asks — Cardinal needs it to index and watch your files.

### i18n support

Expand Down
Binary file modified cardinal/app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/mac-icon_1024x1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions cardinal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"test": "vitest --watch=false",
"test:watch": "vitest --watch",
"test": "NODE_OPTIONS=--no-experimental-webstorage vitest --watch=false",
"test:watch": "NODE_OPTIONS=--no-experimental-webstorage vitest --watch",
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check ."
Expand Down
2 changes: 1 addition & 1 deletion cardinal/src-tauri/Cargo.lock

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

2 changes: 1 addition & 1 deletion cardinal/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cardinal"
version = "0.1.23"
version = "0.2.0"
description = "Cardinal is a cross platform file searching tool"
authors = ["ldm2993593805@163.com"]
edition = "2024"
Expand Down
Binary file modified cardinal/src-tauri/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square107x107Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square142x142Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square150x150Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square284x284Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square30x30Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square310x310Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square44x44Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square71x71Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/Square89x89Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/StoreLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cardinal/src-tauri/icons/icon.icns
Binary file not shown.
Binary file modified cardinal/src-tauri/icons/icon.ico
Binary file not shown.
Binary file modified cardinal/src-tauri/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion cardinal/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ use objc2::{
use objc2_app_kit::{NSPasteboard, NSPasteboardItem, NSPasteboardTypeString, NSPasteboardWriting};
use objc2_foundation::{NSArray, NSString, NSURL};
use parking_lot::Mutex;
use rayon::prelude::*;
use search_cache::{
SearchOptions, SearchOutcome, SearchQuery, SearchResultNode, SlabIndex, SlabNodeMetadata,
content_snippet, content_terms_of_query,
};
use search_cancel::CancellationToken;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -217,13 +219,18 @@ pub struct NodeInfo {
pub path: String,
pub metadata: Option<NodeInfoMetadata>,
pub icon: Option<String>,
#[serde(rename = "contentContext")]
pub content_context: Option<String>,
}

#[derive(Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SearchResponse {
pub results: Vec<SlabIndex>,
pub highlights: Vec<String>,
/// `content:` terms of this query, so the UI highlights in a snippet exactly what the search
/// looked for inside files.
pub content_terms: Vec<String>,
pub status_code: u8,
}

Expand Down Expand Up @@ -291,6 +298,7 @@ pub async fn search(
search_activity::note_search_activity();

let options = options.unwrap_or_default();
let content_terms = query.as_deref().map(content_terms_of_query).unwrap_or_default();
let cancellation_token = CancellationToken::new_search();
let (result_tx, result_rx) = bounded(1);
if let Err(e) = state.search_tx.send(SearchJob {
Expand Down Expand Up @@ -325,6 +333,7 @@ pub async fn search(
SearchResponse {
results,
highlights,
content_terms,
status_code,
}
})
Expand All @@ -335,18 +344,26 @@ pub async fn search(
pub fn get_nodes_info(
results: Vec<SlabIndex>,
include_icons: Option<bool>,
content_terms: Option<Vec<String>>,
case_insensitive: Option<bool>,
state: State<'_, SearchState>,
) -> Vec<NodeInfo> {
if results.is_empty() {
return Vec::new();
}

let include_icons = include_icons.unwrap_or(true);
let content_terms = content_terms.unwrap_or_default();
let case_insensitive = case_insensitive.unwrap_or_default();
let nodes = state.request_nodes(results);

// Rows are independent, and each one may read a file for its icon and its content snippet.
nodes
.into_iter()
.into_par_iter()
.map(|SearchResultNode { path, metadata }| {
let content_context = content_terms
.iter()
.find_map(|term| content_snippet(&path, term, case_insensitive));
let path = path.to_string_lossy().into_owned();
let icon = if include_icons {
fs_icon::icon_of_path_ns(&path).map(|data| {
Expand All @@ -362,6 +379,7 @@ pub fn get_nodes_info(
path,
icon,
metadata: metadata.as_ref().map(NodeInfoMetadata::from_metadata),
content_context,
}
})
.collect()
Expand Down
6 changes: 3 additions & 3 deletions cardinal/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Cardinal",
"version": "0.1.23",
"identifier": "com.cardinal.one",
"version": "0.2.0",
"identifier": "com.franxc.cardinal",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
Expand All @@ -26,7 +26,7 @@
"targets": "all",
"category": "Utilities",
"macOS": {
"signingIdentity": "-",
"signingIdentity": "Developer ID Application: Francisco Carrasco (993439K8XB)",
"minimumSystemVersion": "12.0"
},
"icon": [
Expand Down
77 changes: 77 additions & 0 deletions cardinal/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,57 @@ main,
font-size: 0.78rem;
}

/* Native <select>: the popup, keyboard handling and RTL layout come from the platform. Only the
default macOS chrome is stripped so it reads as part of the toolbar. */
.file-type-select {
appearance: none;
max-width: 9rem;
height: 1.8rem;
padding: 0 1.1rem 0 0.5rem;
border: none;
border-radius: 0.55rem;
background-color: transparent;
/* Chevron drawn inline so no asset is needed; currentColor keeps it on-theme. */
background-image:
linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position:
right 0.5rem top 55%,
right 0.26rem top 55%;
background-size:
0.24rem 0.24rem,
0.24rem 0.24rem;
background-repeat: no-repeat;
color: var(--color-muted);
font: inherit;
font-size: 0.78rem;
line-height: 1;
text-overflow: ellipsis;
cursor: pointer;
}

.file-type-select:hover {
background-color: var(--search-toggle-hover-bg);
}

/* Active filter reads like the case toggle when it is on.
ponytail-keep: matched on a class, not `:not([value=''])`. React sets a controlled select's
value as a DOM property and never writes the attribute, so the attribute selector matches on
first paint and never updates — the accent colour stays on after switching back to "All". */
.file-type-select.is-active {
color: var(--color-accent);
}

.file-type-select:focus-visible {
outline: 2px solid rgba(var(--color-accent-rgb), 0.5);
outline-offset: 2px;
}

.file-type-select option {
color: var(--color-text);
background-color: var(--color-bg);
}

.search-option input:focus-visible + .search-option__display {
outline: 2px solid rgba(var(--color-accent-rgb), 0.5);
outline-offset: 2px;
Expand Down Expand Up @@ -461,6 +512,7 @@ button:active:not(:disabled) {
--columns-total: calc(
var(--w-filename) + var(--w-path) + var(--w-size) + var(--w-modified) + var(--w-created)
);
--columns-total-with-context: calc(var(--columns-total) + var(--w-context));
}

.scroll-area {
Expand Down Expand Up @@ -597,6 +649,14 @@ button:active:not(:disabled) {
min-width: var(--columns-total);
}

.scroll-area--with-context .columns {
grid-template-columns:
var(--w-context) var(--w-filename) var(--w-path) var(--w-size) var(--w-modified)
var(--w-created);
width: var(--columns-total-with-context);
min-width: var(--columns-total-with-context);
}

/* === Header Row === */
.header-row-container {
overflow: hidden;
Expand Down Expand Up @@ -628,6 +688,16 @@ button:active:not(:disabled) {
var(--virtual-scrollbar-width);
}

.scroll-area--with-context .header-row {
width: calc(var(--columns-total-with-context) + var(--virtual-scrollbar-width));
}

.scroll-area--with-context .header-row.columns {
grid-template-columns:
var(--w-context) var(--w-filename) var(--w-path) var(--w-size) var(--w-modified)
var(--w-created) var(--virtual-scrollbar-width);
}

.header {
font-weight: 600;
color: var(--color-header);
Expand Down Expand Up @@ -933,6 +1003,13 @@ button:active:not(:disabled) {
padding: 0 var(--cell-hpad);
}

.context-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--cell-hpad);
}

.filename-text {
flex: 1;
min-width: 0;
Expand Down
Loading