130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
/**
|
|
* Keybinds persistence via JSONC file in XDG_CONFIG_HOME
|
|
*
|
|
* Handles copying keybinds.jsonc from package to user config directory
|
|
* and loading/saving keybind configurations.
|
|
*/
|
|
|
|
import { copyFile } from "fs/promises";
|
|
import path from "path";
|
|
import { parseJSONC } from "./jsonc";
|
|
import { getConfigFilePath, ensureConfigDir } from "./config-dir";
|
|
import type { KeybindsResolved } from "../context/KeybindContext";
|
|
|
|
const KEYBINDS_SOURCE = path.join(
|
|
process.cwd(),
|
|
"src",
|
|
"config",
|
|
"keybinds.jsonc",
|
|
);
|
|
const KEYBINDS_FILE = "keybinds.jsonc";
|
|
|
|
/** Default keybinds (yazi-style) — mirrors src/config/keybinds.jsonc so the
|
|
* app works before a user keybinds file is copied into place. */
|
|
const DEFAULT_KEYBINDS: KeybindsResolved = {
|
|
// movement
|
|
"move-down": ["j", "down"],
|
|
"move-up": ["k", "up"],
|
|
"page-down": ["ctrl-d"],
|
|
"page-up": ["ctrl-u"],
|
|
"full-down": ["ctrl-f"],
|
|
"full-up": ["ctrl-b"],
|
|
"jump-down": ["J"],
|
|
"jump-up": ["K"],
|
|
"goto-top": [["g", "g"]],
|
|
"goto-bottom": ["G"],
|
|
// pane swipe
|
|
"swipe-prev": ["h", "left"],
|
|
"swipe-next": ["l", "right"],
|
|
// open / select
|
|
open: ["return", "enter"],
|
|
"open-interactive": ["shift-return"],
|
|
"toggle-select": ["space"],
|
|
"visual-mode": ["v"],
|
|
"toggle-all": ["ctrl-a"],
|
|
"invert-all": ["ctrl-r"],
|
|
escape: ["escape", "ctrl-["],
|
|
// tabs
|
|
"tab-prev": ["["],
|
|
"tab-next": ["]"],
|
|
"tab-goto-1": ["1"],
|
|
"tab-goto-2": ["2"],
|
|
"tab-goto-3": ["3"],
|
|
"tab-goto-4": ["4"],
|
|
"tab-goto-5": ["5"],
|
|
"tab-goto-6": ["6"],
|
|
// command palette / help / quit
|
|
// q opens the palette (type q + Enter to quit there); Q is the quick quit.
|
|
command: [":", "q"],
|
|
quit: ["Q", "ctrl-c"],
|
|
help: ["~", "f1"],
|
|
// list ops
|
|
search: ["s"],
|
|
"search-scope-toggle": ["tab"],
|
|
filter: ["f"],
|
|
sort: [","],
|
|
"toggle-hidden": ["."],
|
|
refresh: ["r"],
|
|
// a subscribes the focused show/episode result in place (x unsubscribes)
|
|
subscribe: ["a"],
|
|
unsubscribe: ["x"],
|
|
// downloads
|
|
download: ["d"],
|
|
"delete-download": ["D"],
|
|
"whitelist-toggle": ["w"],
|
|
// audio transport (preserved; shifted single keys, no collisions)
|
|
"audio-toggle": ["P"],
|
|
"audio-next": ["N"],
|
|
"audio-prev": ["B"],
|
|
"audio-seek-forward": ["shift-."], // > = shift+.
|
|
"audio-seek-backward": ["shift-,"], // < = shift+,
|
|
};
|
|
|
|
/** Copy keybinds.jsonc to user config directory on first run */
|
|
export async function copyKeybindsIfNeeded(): Promise<void> {
|
|
try {
|
|
const targetPath = getConfigFilePath(KEYBINDS_FILE);
|
|
|
|
const targetFile = Bun.file(targetPath);
|
|
if (await targetFile.exists()) return;
|
|
|
|
await ensureConfigDir();
|
|
await copyFile(KEYBINDS_SOURCE, targetPath);
|
|
} catch {
|
|
// Silently ignore errors
|
|
}
|
|
}
|
|
|
|
/** Load keybinds from JSONC file */
|
|
export async function loadKeybindsFromFile(): Promise<KeybindsResolved> {
|
|
try {
|
|
const filePath = getConfigFilePath(KEYBINDS_FILE);
|
|
const file = Bun.file(filePath);
|
|
|
|
if (!(await file.exists())) return DEFAULT_KEYBINDS;
|
|
|
|
const raw = await file.text();
|
|
const parsed = parseJSONC(raw);
|
|
|
|
if (!parsed || typeof parsed !== "object") return DEFAULT_KEYBINDS;
|
|
|
|
// Merge so partial user configs inherit defaults for missing keys.
|
|
return { ...DEFAULT_KEYBINDS, ...parsed } as KeybindsResolved;
|
|
} catch {
|
|
return DEFAULT_KEYBINDS;
|
|
}
|
|
}
|
|
|
|
/** Save keybinds to JSONC file */
|
|
export async function saveKeybindsToFile(
|
|
keybinds: KeybindsResolved,
|
|
): Promise<void> {
|
|
try {
|
|
await ensureConfigDir();
|
|
const filePath = getConfigFilePath(KEYBINDS_FILE);
|
|
await Bun.write(filePath, JSON.stringify(keybinds, null, 2));
|
|
} catch {
|
|
// Silently ignore write errors
|
|
}
|
|
}
|