Add syntax highlighting, OSD, search, perf optimizations, and UX improvements

- troika-three-text MSDF rendering for resolution-independent code text
- highlight.js syntax highlighting with Catppuccin Mocha colors
- Lazy text pool: max 25 concurrent code meshes, created on demand
- LOD throttled to every 3 frames, OSD every 10
- 45° tiled watermark (repo/path/filename) behind code
- OSD: breadcrumb, file stats, zoom level
- Search: / or Ctrl+F to find and fly to files
- Keybindings: WASD pan, Q/E rotate, Z/C zoom, Space overview, ? help modal
- Mouse wheel zoom vs trackpad pan detection via event frequency
- Zip GBK filename encoding fallback for Chinese filenames
- Docker volume persistence for SQLite cache
This commit is contained in:
2026-04-06 16:30:28 +01:00
parent 7232d4cc37
commit 4aec9510e4
9 changed files with 781 additions and 277 deletions

1
server/Cargo.lock generated
View File

@@ -900,6 +900,7 @@ name = "repo-vis-server"
version = "0.1.0"
dependencies = [
"axum",
"encoding_rs",
"hex",
"rusqlite",
"serde",

View File

@@ -14,6 +14,7 @@ walkdir = "2"
sha2 = "0.10"
hex = "0.4"
zip = "2"
encoding_rs = "0.8"
tempfile = "3"
tracing = "0.1"
tracing-subscriber = "0.3"

View File

@@ -183,9 +183,40 @@ async fn scan_zip(
let mut archive =
zip::ZipArchive::new(cursor).map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid zip: {e}")))?;
archive
.extract(tmp.path())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Extract failed: {e}")))?;
// Extract manually to handle non-UTF-8 filenames (GBK from Windows zips)
for i in 0..archive.len() {
let mut file = archive
.by_index_raw(i)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Zip entry error: {e}")))?;
let raw_name = file.name_raw();
// Try UTF-8 first, then GBK fallback
let name = match std::str::from_utf8(raw_name) {
Ok(s) => s.to_string(),
Err(_) => {
let (decoded, _, _) = encoding_rs::GBK.decode(raw_name);
decoded.into_owned()
}
};
// Sanitize: skip entries that try to escape
if name.contains("..") {
continue;
}
let out_path = tmp.path().join(&name);
if file.is_dir() {
std::fs::create_dir_all(&out_path).ok();
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent).ok();
}
let mut outfile = std::fs::File::create(&out_path)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Create file: {e}")))?;
std::io::copy(&mut file, &mut outfile)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Write file: {e}")))?;
}
}
let entries: Vec<_> = std::fs::read_dir(tmp.path())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?