mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor(config): unify config path resolution across app and launcher
Share config discovery logic between main and launcher so XDG/home and SubMiner/subminer precedence stay consistent. Add regression tests for resolution order and keep config path/show behavior stable.
This commit is contained in:
89
src/config/path-resolution.test.ts
Normal file
89
src/config/path-resolution.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { resolveConfigBaseDirs, resolveConfigDir, resolveConfigFilePath } from './path-resolution';
|
||||
|
||||
function existsSyncFrom(paths: string[]): (candidate: string) => boolean {
|
||||
const normalized = new Set(paths.map((entry) => path.normalize(entry)));
|
||||
return (candidate: string): boolean => normalized.has(path.normalize(candidate));
|
||||
}
|
||||
|
||||
test('resolveConfigBaseDirs trims xdg value and deduplicates fallback dir', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const baseDirs = resolveConfigBaseDirs(' /home/tester/.config ', homeDir);
|
||||
assert.deepEqual(baseDirs, [path.join(homeDir, '.config')]);
|
||||
});
|
||||
|
||||
test('resolveConfigDir prefers xdg SubMiner config when present', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const xdgConfigHome = '/tmp/xdg-config';
|
||||
const configDir = path.join(xdgConfigHome, 'SubMiner');
|
||||
const existsSync = existsSyncFrom([path.join(configDir, 'config.jsonc')]);
|
||||
|
||||
const resolved = resolveConfigDir({
|
||||
xdgConfigHome,
|
||||
homeDir,
|
||||
existsSync,
|
||||
});
|
||||
|
||||
assert.equal(resolved, configDir);
|
||||
});
|
||||
|
||||
test('resolveConfigDir falls back to lowercase subminer candidate', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const configDir = path.join(homeDir, '.config', 'subminer');
|
||||
const existsSync = existsSyncFrom([path.join(configDir, 'config.json')]);
|
||||
|
||||
const resolved = resolveConfigDir({
|
||||
xdgConfigHome: '/tmp/missing-xdg',
|
||||
homeDir,
|
||||
existsSync,
|
||||
});
|
||||
|
||||
assert.equal(resolved, configDir);
|
||||
});
|
||||
|
||||
test('resolveConfigDir falls back to existing directory when file is missing', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const configDir = path.join(homeDir, '.config', 'subminer');
|
||||
const existsSync = existsSyncFrom([configDir]);
|
||||
|
||||
const resolved = resolveConfigDir({
|
||||
xdgConfigHome: '/tmp/missing-xdg',
|
||||
homeDir,
|
||||
existsSync,
|
||||
});
|
||||
|
||||
assert.equal(resolved, configDir);
|
||||
});
|
||||
|
||||
test('resolveConfigFilePath prefers jsonc before json', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const xdgConfigHome = '/tmp/xdg-config';
|
||||
const existsSync = existsSyncFrom([
|
||||
path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'),
|
||||
path.join(xdgConfigHome, 'SubMiner', 'config.json'),
|
||||
]);
|
||||
|
||||
const resolved = resolveConfigFilePath({
|
||||
xdgConfigHome,
|
||||
homeDir,
|
||||
existsSync,
|
||||
});
|
||||
|
||||
assert.equal(resolved, path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'));
|
||||
});
|
||||
|
||||
test('resolveConfigFilePath keeps legacy fallback output path', () => {
|
||||
const homeDir = '/home/tester';
|
||||
const xdgConfigHome = '/tmp/xdg-config';
|
||||
const existsSync = existsSyncFrom([]);
|
||||
|
||||
const resolved = resolveConfigFilePath({
|
||||
xdgConfigHome,
|
||||
homeDir,
|
||||
existsSync,
|
||||
});
|
||||
|
||||
assert.equal(resolved, path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'));
|
||||
});
|
||||
76
src/config/path-resolution.ts
Normal file
76
src/config/path-resolution.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import path from 'node:path';
|
||||
|
||||
type ExistsSync = (candidate: string) => boolean;
|
||||
|
||||
type ConfigPathOptions = {
|
||||
xdgConfigHome?: string;
|
||||
homeDir: string;
|
||||
existsSync: ExistsSync;
|
||||
appNames?: readonly string[];
|
||||
defaultAppName?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_APP_NAMES = ['SubMiner', 'subminer'] as const;
|
||||
const DEFAULT_FILE_NAMES = ['config.jsonc', 'config.json'] as const;
|
||||
|
||||
export function resolveConfigBaseDirs(
|
||||
xdgConfigHome: string | undefined,
|
||||
homeDir: string,
|
||||
): string[] {
|
||||
const fallbackBaseDir = path.join(homeDir, '.config');
|
||||
const primaryBaseDir = xdgConfigHome?.trim() || fallbackBaseDir;
|
||||
return Array.from(new Set([primaryBaseDir, fallbackBaseDir]));
|
||||
}
|
||||
|
||||
function getAppNames(options: ConfigPathOptions): readonly string[] {
|
||||
return options.appNames ?? DEFAULT_APP_NAMES;
|
||||
}
|
||||
|
||||
function getDefaultAppName(options: ConfigPathOptions): string {
|
||||
return options.defaultAppName ?? DEFAULT_APP_NAMES[0];
|
||||
}
|
||||
|
||||
export function resolveConfigDir(options: ConfigPathOptions): string {
|
||||
const baseDirs = resolveConfigBaseDirs(options.xdgConfigHome, options.homeDir);
|
||||
const appNames = getAppNames(options);
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
for (const fileName of DEFAULT_FILE_NAMES) {
|
||||
if (options.existsSync(path.join(dir, fileName))) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
if (options.existsSync(dir)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], getDefaultAppName(options));
|
||||
}
|
||||
|
||||
export function resolveConfigFilePath(options: ConfigPathOptions): string {
|
||||
const baseDirs = resolveConfigBaseDirs(options.xdgConfigHome, options.homeDir);
|
||||
const appNames = getAppNames(options);
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
for (const fileName of DEFAULT_FILE_NAMES) {
|
||||
const candidate = path.join(baseDir, appName, fileName);
|
||||
if (options.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], getDefaultAppName(options), DEFAULT_FILE_NAMES[0]);
|
||||
}
|
||||
41
src/main.ts
41
src/main.ts
@@ -184,6 +184,7 @@ import {
|
||||
DEFAULT_KEYBINDINGS,
|
||||
generateConfigTemplate,
|
||||
} from './config';
|
||||
import { resolveConfigDir } from './config/path-resolution';
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
||||
@@ -252,41 +253,11 @@ function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
||||
sendMpvCommandRuntime(client, ['set_property', 'slang', JELLYFIN_LANG_PREF]);
|
||||
}
|
||||
|
||||
function resolveConfigDir(): string {
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
||||
const baseDirs = Array.from(
|
||||
new Set([
|
||||
xdgConfigHome || path.join(os.homedir(), '.config'),
|
||||
path.join(os.homedir(), '.config'),
|
||||
]),
|
||||
);
|
||||
const appNames = ['SubMiner', 'subminer'];
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
if (
|
||||
fs.existsSync(path.join(dir, 'config.jsonc')) ||
|
||||
fs.existsSync(path.join(dir, 'config.json'))
|
||||
) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
if (fs.existsSync(dir)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], 'SubMiner');
|
||||
}
|
||||
|
||||
const CONFIG_DIR = resolveConfigDir();
|
||||
const CONFIG_DIR = resolveConfigDir({
|
||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||
homeDir: os.homedir(),
|
||||
existsSync: fs.existsSync,
|
||||
});
|
||||
const USER_DATA_PATH = CONFIG_DIR;
|
||||
const DEFAULT_MPV_LOG_PATH = process.env.SUBMINER_MPV_LOG?.trim() || DEFAULT_MPV_LOG_FILE;
|
||||
const DEFAULT_IMMERSION_DB_PATH = path.join(USER_DATA_PATH, 'immersion.sqlite');
|
||||
|
||||
Reference in New Issue
Block a user