Skip to content
Open
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Global shortcuts:
- `J` or `ArrowDown`: scroll the active file view down one line when the file tree is hidden
- `K` or `ArrowUp`: scroll the active file view up one line when the file tree is hidden
- `Shift+L`: toggle file tree
- `Y`: copy the current file's absolute path
- `Y`: copy the current file's absolute path when the file tree is hidden
- `R`: reload the current file
- `Shift+T`: toggle table of contents for Markdown
- `Shift+P`: switch Markdown raw/preview pane
Expand All @@ -69,6 +69,7 @@ File tree shortcuts:
- `S`: sort by size descending
- `H` or `ArrowLeft`: go to parent directory
- `L`, `ArrowRight`, `Enter`: open or confirm selection
- `Y`: copy the selected file or directory absolute path, or the current directory path when no row is selected
- `Ctrl+M`: same as `Enter` in the filter field

Video preview:
Expand Down Expand Up @@ -129,6 +130,17 @@ The repository is set up for `nix develop`, which provides Bun, Cargo, Tauri-rel
nix develop
```

### Linux GDK / GTK environment constraints

On Linux, `chilla` depends on the GTK/WebKitGTK runtime prepared by `flake.nix`. In practice, that means:

- Prefer running the app from `nix develop`, `task dev`, or `nix run .` so the wrapped GTK/GIO/GStreamer environment is applied.
- Do not mix host GTK-related overrides into the dev shell. In particular, `GTK_PATH`, `GI_TYPELIB_PATH`, `GTK_IM_MODULE`, `QT_IM_MODULE`, and `XMODIFIERS` are intentionally unset to avoid loading incompatible host modules.
- The Linux WebKitGTK renderer is currently run with `WEBKIT_DISABLE_DMABUF_RENDERER=1` because DMA-BUF rendering is not treated as stable for this app's environment.
- If Linux media playback behaves differently from macOS, assume the Linux path is constrained by the WebKitGTK + GStreamer stack rather than the frontend alone.

If you launch Tauri commands outside the Nix shell, you are responsible for providing a compatible GTK/WebKitGTK/GStreamer runtime yourself.

### Common commands

```bash
Expand Down
9 changes: 9 additions & 0 deletions bun.lock

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

69 changes: 69 additions & 0 deletions e2e/file-browser-keyboard.pw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, test } from "@playwright/test";

const harnessPath = "/?harness=file-browser";

test.describe("file browser keyboard navigation", () => {
test("moves focus and selection with j/k and confirms with l", async ({ page }) => {
await page.goto(harnessPath);

await page.waitForSelector('[data-testid="selected-path"]');
await expect(page.getByTestId("selected-path")).toHaveText(
"/workspace/bravo.md",
);
await expect(page.getByTestId("selected-index")).toHaveText("2");
await expect(page.locator(".file-browser__button:focus")).toHaveAttribute(
"data-path",
"/workspace/bravo.md",
);

await page.keyboard.press("j");
await expect(page.getByTestId("selected-path")).toHaveText(
"/workspace/delta.md",
);
await expect(page.locator(".file-browser__button:focus")).toHaveAttribute(
"data-path",
"/workspace/delta.md",
);

await page.keyboard.press("j");
await expect(page.getByTestId("selected-path")).toHaveText(
"/workspace/echo.txt",
);
await expect(page.locator(".file-browser__button:focus")).toHaveAttribute(
"data-path",
"/workspace/echo.txt",
);

await page.keyboard.press("k");
await expect(page.getByTestId("selected-path")).toHaveText(
"/workspace/delta.md",
);
await expect(page.locator(".file-browser__button:focus")).toHaveAttribute(
"data-path",
"/workspace/delta.md",
);

await page.keyboard.press("l");
await expect(page.getByTestId("confirmed-path")).toHaveText(
"/workspace/delta.md",
);
});

test("h triggers parent navigation while preserving list keyboard handling", async ({
page,
}) => {
await page.goto(harnessPath);
await expect(page.locator(".file-browser__button:focus")).toHaveAttribute(
"data-path",
"/workspace/bravo.md",
);

await page.keyboard.press("h");
await expect(page.getByTestId("parent-count")).toHaveText("1");

await page.keyboard.press("j");
await expect(page.getByTestId("selected-path")).toHaveText(
"/workspace/delta.md",
);
});
});
14 changes: 13 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,23 @@
postFixup = lib.optionalString pkgs.stdenv.isLinux ''
wrapProgram $out/bin/chilla \
--prefix LD_LIBRARY_PATH : "${linuxRuntimeLibraryPath}" \
--set GIO_EXTRA_MODULES "${linuxGioModulePath}" \
--set XDG_DATA_DIRS "${linuxXdgDataDirs}" \
--set WEBKIT_DISABLE_DMABUF_RENDERER 1 \
--set GST_PLUGIN_SCANNER "${linuxGStreamerPluginScanner}" \
--set GST_PLUGIN_SYSTEM_PATH "${linuxGStreamerPluginPath}" \
--prefix GST_PLUGIN_SYSTEM_PATH_1_0 : "${linuxGStreamerPluginPath}" \
--set GST_PLUGIN_PATH "${linuxGStreamerPluginPath}" \
--set GST_PLUGIN_PATH_1_0 "${linuxGStreamerPluginPath}"
--set GST_PLUGIN_PATH_1_0 "${linuxGStreamerPluginPath}" \
--unset GTK_PATH \
--unset GI_TYPELIB_PATH \
--unset GTK_IM_MODULE \
--unset GTK_IM_MODULE_FILE \
--unset QT_IM_MODULE \
--unset XMODIFIERS \
--unset GIO_MODULE_DIR \
--unset GTK_EXE_PREFIX \
--unset GTK_DATA_PREFIX
'';
};

Expand Down
16 changes: 16 additions & 0 deletions impl-plans/active/file-viewer-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ export interface StartupContext {
**Notes**: The current app is centered on a single Markdown startup path. The feature will be implemented by adding startup context plus separate directory-listing/file-preview commands, while preserving the existing Markdown editor flow for Markdown mode. Image and video previews are part of the file-view contract.

### Session: 2026-03-21 JST
**Tasks Completed**: Refactored directory listing contract to keep selection frontend-owned
**Tasks In Progress**: TASK-002 Rust viewer service, TASK-003 frontend file view mode
**Blockers**: Runtime keyboard behavior still needs manual validation in the app
**Notes**: Removed `selected_path` / `selected_index` from the `list_directory` Tauri contract so the backend acts as a stateless page query service over `path`, `offset`, `limit`, `query`, and `sort`. Frontend selection is now owned locally in `WorkspaceShell`, including page-boundary keyboard navigation. Verified with `bun run typecheck`, `bun test`, `CARGO_TERM_QUIET=true cargo check -p chilla`, and `CARGO_TERM_QUIET=true cargo test -p chilla`.
**Tasks Completed**: Extended the directory-entry contract with file size and modified-time metadata; added frontend file-tree sort modes and keyboard shortcuts for name, mtime, and size.
**Tasks In Progress**: TASK-002 Rust viewer service, TASK-003 frontend file view mode, TASK-004 verification
**Blockers**: None
Expand All @@ -228,3 +232,15 @@ export interface StartupContext {
**Tasks In Progress**: TASK-003 frontend file view mode, TASK-004 verification
**Blockers**: None
**Notes**: The play overlay remains keyboard reachable, and the autoplay request id is now consumed once per preview load so repeated reactive updates do not retrigger stale playback requests.

### Session: 2026-03-21 JST
**Tasks Completed**: Tuned large-directory file-tree rendering by virtualizing the visible rows, moving scrolling into a dedicated viewport, and removing per-row truncation measurement observers that scaled with entry count.
**Tasks In Progress**: TASK-003 frontend file view mode, TASK-004 verification
**Blockers**: None
**Notes**: The file tree now renders only the visible slice plus overscan, which keeps navigation responsive for large directories while preserving existing keyboard and focus behavior.

### Session: 2026-03-21 JST
**Tasks Completed**: Moved file-tree sort and filter evaluation to Rust, changed the directory-list contract to return a paged window with total counts and selection metadata, and updated the frontend file browser to request new windows as scroll/keyboard navigation moves across large directories.
**Tasks In Progress**: TASK-003 frontend file view mode, TASK-004 verification
**Blockers**: None
**Notes**: Large directories such as `/nix/store` no longer require sending the entire entry list over Tauri in one response. The frontend now treats the Rust response as a movable window over the full sorted directory.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"solid-js": "^1.9.3"
},
"devDependencies": {
"@types/bun": "^1.3.3",
"@playwright/test": "^1.58.2",
"@tauri-apps/cli": "^2",
"@types/bun": "^1.3.3",
"typescript": "~5.6.2",
"vite": "^6.0.3",
"vite-plugin-solid": "^2.11.0"
Expand Down
40 changes: 40 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { execSync } from "node:child_process";
import { defineConfig } from "@playwright/test";

function resolveChromiumExecutablePath(): string | undefined {
if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE) {
return process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE;
}

try {
const value = execSync("which chromium", {
stdio: ["ignore", "pipe", "ignore"],
})
.toString()
.trim();
return value === "" ? undefined : value;
} catch {
return undefined;
}
}

const chromiumExecutablePath = resolveChromiumExecutablePath();

export default defineConfig({
testDir: "./e2e",
testMatch: "**/*.pw.ts",
timeout: 30_000,
use: {
baseURL: "http://localhost:1420",
headless: true,
launchOptions: chromiumExecutablePath
? { executablePath: chromiumExecutablePath }
: undefined,
},
webServer: {
command: "bun run dev -- --host localhost",
url: "http://localhost:1420",
reuseExistingServer: true,
timeout: 120_000,
},
});
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ syntect = { version = "5.3", default-features = false, features = [
"html",
"dump-load",
] }
tauri = { version = "2", features = ["protocol-asset"] }
tauri = { version = "2", features = ["custom-protocol", "protocol-asset"] }
tauri-plugin-opener = "2.5.3"
thiserror = "2"
tree_magic_mini = "3.2.2"
15 changes: 12 additions & 3 deletions src-tauri/src/commands/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
document::types::{DocumentSnapshot, HeadingNode},
markdown::render_markdown,
syntax_highlight::SyntaxUiTheme,
viewer::types::{DirectorySnapshot, FilePreview, StartupContext},
viewer::types::{DirectorySnapshot, DirectorySort, FilePreview, StartupContext},
};

fn format_command_error(error: impl std::fmt::Display) -> String {
Expand Down Expand Up @@ -48,12 +48,21 @@ pub fn get_startup_context(state: State<'_, AppState>) -> Result<StartupContext,
#[tauri::command]
pub fn list_directory(
path: String,
selected_path: Option<String>,
offset: Option<usize>,
limit: Option<usize>,
query: Option<String>,
sort: Option<DirectorySort>,
state: State<'_, AppState>,
) -> Result<DirectorySnapshot, String> {
state
.viewer_service()
.list_directory(Path::new(&path), selected_path.as_deref().map(Path::new))
.list_directory(
Path::new(&path),
offset,
limit,
query.as_deref(),
sort,
)
.map_err(format_command_error)
}

Expand Down
Loading