Skip to content

Commit 848f68f

Browse files
committed
Add font width measuring
For precise display in Brief mode
1 parent e2aa011 commit 848f68f

18 files changed

Lines changed: 684 additions & 4 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Migration from bincode v1 to bincode2 v2
2+
3+
**Date**: 2025-12-31
4+
5+
## Context
6+
7+
The original `bincode` crate became unmaintained in late 2024/early 2025 due to a doxxing and harassment incident. The maintainer ceased all development and published v3.0.0 as a "tombstone" release that intentionally fails to compile.
8+
9+
## Decision
10+
11+
Migrated to `bincode2` v2, a maintained fork by Pravega, which provides:
12+
- Drop-in replacement with minimal code changes
13+
- Ongoing maintenance and security updates
14+
- Compatible API with bincode v1
15+
- Active development
16+
17+
## Alternatives considered
18+
19+
1. **Stay on bincode v1**: No work needed, but no security updates or bug fixes
20+
2. **postcard**: Different API, would require more code changes
21+
3. **rkyv**: Zero-copy deserialization, but more complex API and higher migration effort
22+
4. **bincode v2.0.1**: Original but unmaintained version
23+
24+
## Changes made
25+
26+
1. **Cargo.toml**: Changed `bincode = "1"` to `bincode2 = "2"`
27+
2. **src-tauri/src/font_metrics/mod.rs**: Updated two function calls:
28+
- `bincode::deserialize()``bincode2::deserialize()`
29+
- `bincode::serialize()``bincode2::serialize()`
30+
3. **docs/features/font-metrics.md**: Updated documentation to mention bincode2
31+
32+
## Impact
33+
34+
- **Breaking change for cached data**: Existing font metrics cache files may need to be regenerated
35+
- **Location**: `~/Library/Application Support/com.rusty-commander.app/font-metrics/`
36+
- **Mitigation**: The app will automatically regenerate metrics if deserialization fails
37+
38+
## Testing
39+
40+
- ✅ All Rust tests pass (26/26)
41+
- ✅ Build succeeds
42+
- ✅ All checks pass (rustfmt, clippy, cargo-audit, cargo-deny, cargo-udeps)
43+
44+
## References
45+
46+
- bincode2 crate: https://crates.io/crates/bincode2
47+
- Original bincode situation: https://crates.io/crates/bincode

docs/features/font-metrics.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Font metrics for accurate column widths
2+
3+
## Overview
4+
5+
The file explorer uses a font metrics system to calculate accurate character widths for optimal column sizing in Brief
6+
mode. This ensures filenames are never truncated unnecessarily while avoiding excessive column widths.
7+
8+
## How it works
9+
10+
### Measurement phase (first app run)
11+
12+
1. **Frontend measurement**: On first app start, the system measures ~67,000 characters using the Canvas API
13+
- Coverage: Basic Multilingual Plane (BMP) + common emoji
14+
- Includes CJK, Cyrillic, Arabic, Indic scripts, Latin Extended
15+
- Takes ~100-300ms, runs in background using `requestIdleCallback`
16+
17+
2. **Binary storage**: Measurements are serialized using bincode2 and saved to disk
18+
- Location: `~/Library/Application Support/com.veszelovszki.rusty-commander/font-metrics/system-400-12.bin`
19+
- Size: ~426KB (500KB theoretical max)
20+
- Load time: ~5ms
21+
22+
### Width calculation (every directory load)
23+
24+
1. **Rust calculates max width**: During `list_directory_start()`, Rust:
25+
- Iterates through all filenames
26+
- Sums character widths using cached metrics
27+
- Returns the maximum width alongside `listingId` and `totalCount`
28+
29+
2. **Frontend uses width**: BriefList receives `maxFilenameWidth` and:
30+
- Uses it for column width when available
31+
- Falls back to estimation (`containerWidth / 3`) if metrics unavailable
32+
- Automatically adapts columns to actual content
33+
34+
## Performance
35+
36+
- **Measurement**: ~100-300ms (one-time, on first run)
37+
- **Load from disk**: ~5ms (on subsequent runs)
38+
- **Width calculation**: ~1-10ms per directory (depends on file count)
39+
- **Total impact**: Negligible for directories under 100k files
40+
41+
## Code organization
42+
43+
```
44+
src-tauri/src/
45+
└── font_metrics/
46+
└── mod.rs # Core metrics storage and calculation
47+
48+
src/lib/
49+
└── font-metrics/
50+
├── index.ts # Public API
51+
└── measure.ts # Canvas measurement
52+
```
53+
54+
## Font configuration
55+
56+
Currently hardcoded to match CSS:
57+
58+
- **Font family**: system font stack (`-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif`)
59+
- **Font weight**: 400 (normal)
60+
- **Font size**: 12px (`--font-size-sm`)
61+
- **Font ID**: `system-400-12`
62+
63+
When font settings become user-configurable, the frontend will automatically re-measure and the cache key will be
64+
updated.
65+
66+
## Example
67+
68+
For a directory with files:
69+
70+
- `README.md` (9 chars × 7.2px avg = 65px)
71+
- `package.json` (12 chars × 7.2px avg = 86px)
72+
- `中文文件.txt` (7 chars × 12px avg = 84px)
73+
74+
The system calculates the max width as 86px, ensuring all filenames fit without truncation while keeping columns
75+
compact.
76+
77+
## Limitations
78+
79+
- Only supports a single font configuration at a time
80+
- Unmeasured characters (rare Unicode) fall back to average width
81+
- Column width is fixed for the entire directory (not per-column)

src-tauri/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ rayon = "1.11.0"
3535
uuid = { version = "1.19.0", features = ["v4"] }
3636
tauri-plugin-clipboard-manager = "2"
3737
notify-debouncer-full = "0.6.0"
38+
bincode2 = "2"
3839

3940
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
4041
tauri-plugin-window-state = "2"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//! Tauri commands for font metrics operations.
2+
3+
use crate::font_metrics;
4+
use std::collections::HashMap;
5+
6+
/// Stores font metrics received from the frontend.
7+
///
8+
/// # Arguments
9+
/// * `app` - Tauri app handle for accessing app data directory
10+
/// * `font_id` - Font identifier (e.g., "system-400-12")
11+
/// * `widths` - Map of code point → width in pixels
12+
#[tauri::command]
13+
pub fn store_font_metrics<R: tauri::Runtime>(
14+
app: tauri::AppHandle<R>,
15+
font_id: String,
16+
widths: HashMap<u32, f32>,
17+
) -> Result<(), String> {
18+
// Store in memory
19+
font_metrics::store_metrics(font_id.clone(), widths.clone())?;
20+
21+
// Save to disk
22+
font_metrics::save_to_disk(&app, &font_id, &widths)?;
23+
24+
eprintln!("[FONT_METRICS] Stored metrics for font: {}", font_id);
25+
Ok(())
26+
}
27+
28+
/// Checks if font metrics are available for a font ID.
29+
///
30+
/// # Arguments
31+
/// * `font_id` - Font identifier to check
32+
#[tauri::command]
33+
pub fn has_font_metrics(font_id: String) -> bool {
34+
font_metrics::has_metrics(&font_id)
35+
}

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Tauri commands module.
22
33
pub mod file_system;
4+
pub mod font_metrics;
45
pub mod icons;
56
#[cfg(target_os = "macos")]
67
pub mod sync_status;

src-tauri/src/file_system/operations.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ pub struct ListingStartResult {
275275
pub listing_id: String,
276276
/// Total number of entries in the directory
277277
pub total_count: usize,
278+
/// Maximum filename width in pixels (for Brief mode columns)
279+
/// None if font metrics are not available
280+
pub max_filename_width: Option<f32>,
278281
}
279282

280283
/// Starts a new directory listing.
@@ -319,15 +322,23 @@ pub fn list_directory_start(path: &Path, include_hidden: bool) -> Result<Listing
319322
listing_id.clone(),
320323
CachedListing {
321324
path: path.to_path_buf(),
322-
entries: all_entries,
325+
entries: all_entries.clone(), // Clone to allow reuse below
323326
},
324327
);
325328
}
326329

330+
// Calculate max filename width if font metrics are available
331+
let max_filename_width = {
332+
let font_id = "system-400-12"; // Default font for now
333+
let filenames: Vec<&str> = all_entries.iter().map(|e| e.name.as_str()).collect();
334+
crate::font_metrics::calculate_max_width(&filenames, font_id)
335+
};
336+
327337
benchmark::log_event("list_directory_start RETURNING");
328338
Ok(ListingStartResult {
329339
listing_id,
330340
total_count,
341+
max_filename_width,
331342
})
332343
}
333344

0 commit comments

Comments
 (0)