fix(launcher): restore Matroska thumbnails in Linux rofi picker (#210)

This commit is contained in:
2026-08-18 20:30:58 -07:00
committed by GitHub
parent 4ed878270f
commit 2a77ba9dd4
26 changed files with 648 additions and 119 deletions
@@ -8,6 +8,18 @@ import {
resolveManagedLinuxRuntimePluginPaths,
} from './linux-runtime-plugin-assets';
const THUMBNAILER_RELATIVE_PATH = path.join(
'thumbnailers',
'subminer-ffmpegthumbnailer.thumbnailer',
);
function writeThumbnailer(rootDir: string, content = '[Thumbnailer Entry]\n'): string {
const thumbnailerPath = path.join(rootDir, THUMBNAILER_RELATIVE_PATH);
fs.mkdirSync(path.dirname(thumbnailerPath), { recursive: true });
fs.writeFileSync(thumbnailerPath, content);
return thumbnailerPath;
}
async function withTempDir<T>(fn: (dir: string) => Promise<T> | T): Promise<T> {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-linux-plugin-assets-test-'));
try {
@@ -48,6 +60,7 @@ test('resolveManagedLinuxRuntimePluginPaths resolves XDG data target paths', ()
pluginEntrypointPath: '/tmp/xdg-data/SubMiner/plugin/subminer/main.lua',
pluginConfigPath: '/tmp/xdg-data/SubMiner/plugin/subminer.conf',
themePath: '/tmp/xdg-data/SubMiner/themes/subminer.rasi',
thumbnailerPath: '/tmp/xdg-data/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
});
});
@@ -79,6 +92,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
await withTempDir(async (tempDir) => {
const sourceRoot = path.join(tempDir, 'source', 'plugin');
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
const targetRoot = path.join(tempDir, 'xdg-data', 'SubMiner', 'plugin');
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
@@ -94,6 +108,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
pluginDirSource: path.join(sourceRoot, 'subminer'),
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
themeSourcePath,
thumbnailerSourcePath,
}),
});
@@ -117,6 +132,19 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
),
'/* theme */\n',
);
assert.equal(
fs.readFileSync(
path.join(
tempDir,
'xdg-data',
'SubMiner',
'thumbnailers',
'subminer-ffmpegthumbnailer.thumbnailer',
),
'utf8',
),
'[Thumbnailer Entry]\n',
);
});
});
@@ -124,6 +152,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
await withTempDir(async (tempDir) => {
const sourceRoot = path.join(tempDir, 'source', 'plugin');
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
const xdgDataHome = path.join(tempDir, 'xdg-data');
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
@@ -143,6 +172,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
pluginDirSource: path.join(sourceRoot, 'subminer'),
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
themeSourcePath,
thumbnailerSourcePath,
}),
});
@@ -169,6 +199,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
test('ensureLinuxRuntimePluginAssets installs managed theme without resolving plugin sources when plugin assets already exist', async () => {
await withTempDir(async (tempDir) => {
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
const xdgDataHome = path.join(tempDir, 'xdg-data');
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
@@ -183,6 +214,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme without resolving pl
xdgDataHome,
resolveBundledAssets: () => ({
themeSourcePath,
thumbnailerSourcePath,
}),
});
@@ -250,6 +282,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin assets without reso
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
'/* existing theme */\n',
);
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
const result = await ensureLinuxRuntimePluginAssets({
platform: 'linux',
@@ -258,6 +291,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin assets without reso
resolveBundledAssets: () => ({
pluginDirSource: path.join(sourceRoot, 'subminer'),
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
thumbnailerSourcePath,
}),
});
@@ -332,6 +366,7 @@ test('ensureLinuxRuntimePluginAssets returns already-present when managed assets
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
'/* theme */\n',
);
writeThumbnailer(path.join(xdgDataHome, 'SubMiner'));
const result = await ensureLinuxRuntimePluginAssets({
platform: 'linux',
@@ -369,6 +404,7 @@ test('ensureLinuxRuntimePluginAssets leaves no final target tree on failed insta
await withTempDir(async (tempDir) => {
const sourceRoot = path.join(tempDir, 'source', 'plugin');
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
const xdgDataHome = path.join(tempDir, 'xdg-data');
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
@@ -385,6 +421,7 @@ test('ensureLinuxRuntimePluginAssets leaves no final target tree on failed insta
pluginDirSource: path.join(sourceRoot, 'subminer'),
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
themeSourcePath,
thumbnailerSourcePath,
}),
copyFile: async () => {
throw new Error('copy failed');
@@ -10,6 +10,7 @@ export interface ManagedLinuxRuntimePluginPaths {
pluginEntrypointPath: string;
pluginConfigPath: string;
themePath: string;
thumbnailerPath: string;
}
export interface EnsureLinuxRuntimePluginAssetsResult {
@@ -23,6 +24,7 @@ interface RuntimePluginAssetSources {
pluginDirSource?: string;
pluginConfigSource?: string;
themeSourcePath?: string;
thumbnailerSourcePath?: string;
}
interface RuntimePluginDirentLike {
@@ -72,6 +74,11 @@ export function resolveManagedLinuxRuntimePluginPaths(options: {
pluginEntrypointPath: pathModule.join(pluginDir, 'main.lua'),
pluginConfigPath: pathModule.join(rootDir, 'subminer.conf'),
themePath: pathModule.join(dataDir, 'themes', 'subminer.rasi'),
thumbnailerPath: pathModule.join(
dataDir,
'thumbnailers',
'subminer-ffmpegthumbnailer.thumbnailer',
),
};
}
@@ -95,11 +102,12 @@ async function copyDirectoryRecursive(
}
}
function resolveBundledThemePath(options: {
function resolveBundledAssetPath(options: {
dirname: string;
appPath: string;
resourcesPath: string;
existsSync: (candidate: string) => boolean;
relativePath: string;
}): string | null {
const roots = [
path.join(options.resourcesPath, 'assets'),
@@ -111,7 +119,7 @@ function resolveBundledThemePath(options: {
];
for (const root of roots) {
const candidate = path.join(root, 'themes', 'subminer.rasi');
const candidate = path.join(root, options.relativePath);
if (options.existsSync(candidate)) return candidate;
}
@@ -129,16 +137,25 @@ function resolveBundledAssetsDefault(
existsSync,
});
const themeSourcePath = resolveBundledThemePath({
const themeSourcePath = resolveBundledAssetPath({
dirname: __dirname,
appPath: process.execPath,
resourcesPath,
existsSync,
relativePath: path.join('themes', 'subminer.rasi'),
});
const thumbnailerSourcePath = resolveBundledAssetPath({
dirname: __dirname,
appPath: process.execPath,
resourcesPath,
existsSync,
relativePath: path.join('thumbnailers', 'subminer-ffmpegthumbnailer.thumbnailer'),
});
return {
...(pluginAssets ?? {}),
...(themeSourcePath ? { themeSourcePath } : {}),
...(thumbnailerSourcePath ? { thumbnailerSourcePath } : {}),
};
}
@@ -178,7 +195,8 @@ export async function ensureLinuxRuntimePluginAssets(
const pluginAssetsExist =
existsSync(managedPaths.pluginEntrypointPath) && existsSync(managedPaths.pluginConfigPath);
const themeExists = existsSync(managedPaths.themePath);
if (pluginAssetsExist && themeExists) {
const thumbnailerExists = existsSync(managedPaths.thumbnailerPath);
if (pluginAssetsExist && themeExists && thumbnailerExists) {
return {
ok: true,
status: 'already-present',
@@ -193,6 +211,7 @@ export async function ensureLinuxRuntimePluginAssets(
const shouldInstallPluginAssets = !pluginAssetsExist;
const shouldInstallTheme = !themeExists;
const shouldInstallThumbnailer = !thumbnailerExists;
if (
shouldInstallPluginAssets &&
(!bundledAssets.pluginDirSource || !bundledAssets.pluginConfigSource)
@@ -210,6 +229,13 @@ export async function ensureLinuxRuntimePluginAssets(
error: 'Bundled Linux runtime theme asset was not found.',
};
}
if (shouldInstallThumbnailer && !bundledAssets.thumbnailerSourcePath) {
return {
ok: false,
status: 'failed',
error: 'Bundled Linux rofi thumbnailer asset was not found.',
};
}
const stagingSuffix = `${process.pid}-${Date.now()}`;
const stagedPluginDir = pathModule.join(managedPaths.rootDir, `.subminer-stage-${stagingSuffix}`);
@@ -221,9 +247,14 @@ export async function ensureLinuxRuntimePluginAssets(
pathModule.dirname(managedPaths.themePath),
`.subminer.rasi-stage-${stagingSuffix}`,
);
const stagedThumbnailerPath = pathModule.join(
pathModule.dirname(managedPaths.thumbnailerPath),
`.subminer-ffmpegthumbnailer.thumbnailer-stage-${stagingSuffix}`,
);
let pluginDirInstalled = false;
let pluginConfigInstalled = false;
let themeInstalled = false;
let thumbnailerInstalled = false;
try {
if (shouldInstallPluginAssets) {
@@ -249,6 +280,14 @@ export async function ensureLinuxRuntimePluginAssets(
await mkdir(pathModule.dirname(managedPaths.themePath), { recursive: true });
await copyFile(themeSourcePath, stagedThemePath);
}
if (shouldInstallThumbnailer) {
const thumbnailerSourcePath = bundledAssets.thumbnailerSourcePath;
if (!thumbnailerSourcePath) {
throw new Error('Bundled Linux rofi thumbnailer asset was not found.');
}
await mkdir(pathModule.dirname(managedPaths.thumbnailerPath), { recursive: true });
await copyFile(thumbnailerSourcePath, stagedThumbnailerPath);
}
if (shouldInstallPluginAssets) {
await rm(managedPaths.pluginDir, { recursive: true, force: true });
await rm(managedPaths.pluginConfigPath, { force: true });
@@ -262,6 +301,11 @@ export async function ensureLinuxRuntimePluginAssets(
await rename(stagedThemePath, managedPaths.themePath);
themeInstalled = true;
}
if (shouldInstallThumbnailer) {
await rm(managedPaths.thumbnailerPath, { force: true });
await rename(stagedThumbnailerPath, managedPaths.thumbnailerPath);
thumbnailerInstalled = true;
}
return {
ok: true,
@@ -278,9 +322,13 @@ export async function ensureLinuxRuntimePluginAssets(
if (themeInstalled) {
await rm(managedPaths.themePath, { force: true }).catch(() => {});
}
if (thumbnailerInstalled) {
await rm(managedPaths.thumbnailerPath, { force: true }).catch(() => {});
}
await rm(stagedPluginDir, { recursive: true, force: true }).catch(() => {});
await rm(stagedPluginConfigPath, { force: true }).catch(() => {});
await rm(stagedThemePath, { force: true }).catch(() => {});
await rm(stagedThumbnailerPath, { force: true }).catch(() => {});
return {
ok: false,
status: 'failed',
+106 -2
View File
@@ -13,7 +13,7 @@ import {
} from './support-assets';
type SupportAssetsResultWithComponent = SupportAssetsUpdateResult & {
component?: 'theme' | 'plugin';
component?: 'theme' | 'thumbnailer' | 'plugin';
};
function sha256(data: Buffer): string {
@@ -22,18 +22,30 @@ function sha256(data: Buffer): string {
function makeSupportAssetsArchive(options?: {
themeContent?: string;
thumbnailerContent?: string;
includeThumbnailer?: boolean;
pluginVersion?: string | null;
pluginMainContent?: string;
extraPluginFiles?: Array<{ relativePath: string; content: string }>;
}): { archive: Buffer; tempDir: string } {
const themeContent = options?.themeContent ?? 'new theme\n';
const thumbnailerContent = options?.thumbnailerContent ?? '[Thumbnailer Entry]\n';
const pluginVersion = options && 'pluginVersion' in options ? options.pluginVersion : '0.12.0';
const pluginMainContent = options?.pluginMainContent ?? 'new plugin\n';
const extraPluginFiles = options?.extraPluginFiles ?? [];
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-support-assets-test-'));
fs.mkdirSync(path.join(tempDir, 'assets/themes'), { recursive: true });
if (options?.includeThumbnailer !== false) {
fs.mkdirSync(path.join(tempDir, 'assets/thumbnailers'), { recursive: true });
}
fs.mkdirSync(path.join(tempDir, 'plugin/subminer'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'assets/themes/subminer.rasi'), themeContent);
if (options?.includeThumbnailer !== false) {
fs.writeFileSync(
path.join(tempDir, 'assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
thumbnailerContent,
);
}
fs.writeFileSync(path.join(tempDir, 'plugin/subminer/main.lua'), pluginMainContent);
if (pluginVersion !== null) {
fs.writeFileSync(
@@ -93,7 +105,7 @@ test('detectSupportAssetDataDirs only returns Linux support-asset locations', ()
);
});
test('buildProtectedSupportAssetsCommand installs both theme and plugin assets', () => {
test('buildProtectedSupportAssetsCommand installs theme, thumbnailer, and plugin assets', () => {
const command = buildProtectedSupportAssetsCommand(
"https://example.test/subminer assets.tar.gz?sig='abc'",
'ABCDEF1234',
@@ -110,11 +122,28 @@ test('buildProtectedSupportAssetsCommand installs both theme and plugin assets',
command,
/printf '%s %s\\n' 'abcdef1234' "\$tmp\/subminer-assets\.tar\.gz" \| sha256sum -c -/,
);
const requiredAssetChecks = [
'test -f "$tmp/assets/themes/subminer.rasi"',
'test -f "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"',
'test -f "$tmp/plugin/subminer/main.lua"',
'test -f "$tmp/plugin/subminer/version.lua"',
];
const firstSudoIndex = command.indexOf('sudo ');
assert.notEqual(firstSudoIndex, -1);
for (const check of requiredAssetChecks) {
const checkIndex = command.indexOf(check);
assert.notEqual(checkIndex, -1);
assert.ok(checkIndex < firstSudoIndex);
}
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/themes/);
assert.match(
command,
/sudo cp "\$tmp\/assets\/themes\/subminer\.rasi" '\/usr\/local\/share\/SubMiner'\\''s data'\/themes\/subminer\.rasi/,
);
assert.match(
command,
/sudo cp "\$tmp\/assets\/thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer" .*thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer/,
);
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/plugin/);
assert.match(command, /sudo rm -rf .*plugin\/subminer\.next/);
assert.match(command, /sudo cp -R "\$tmp\/plugin\/subminer" .*plugin\/subminer\.next/);
@@ -209,6 +238,12 @@ test('updateSupportAssetsFromRelease installs missing plugin into a root with a
path: dataDir,
message: 'Updated theme.',
},
{
status: 'updated',
component: 'thumbnailer',
path: dataDir,
message: 'Installed rofi thumbnailer.',
},
{
status: 'updated',
component: 'plugin',
@@ -220,6 +255,13 @@ test('updateSupportAssetsFromRelease installs missing plugin into a root with a
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
'new theme\n',
);
assert.equal(
fs.readFileSync(
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
'utf8',
),
'[Thumbnailer Entry]\n',
);
assert.equal(
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
'new plugin\n',
@@ -345,8 +387,13 @@ test('updateSupportAssetsFromRelease skips identical theme and up-to-date plugin
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
fs.mkdirSync(path.join(dataDir, 'thumbnailers'), { recursive: true });
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'same theme\n');
fs.writeFileSync(
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
'[Thumbnailer Entry]\n',
);
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'same plugin\n');
fs.writeFileSync(
path.join(dataDir, 'plugin/subminer/version.lua'),
@@ -371,6 +418,12 @@ test('updateSupportAssetsFromRelease skips identical theme and up-to-date plugin
path: dataDir,
message: 'Theme already up to date.',
},
{
status: 'skipped',
component: 'thumbnailer',
path: dataDir,
message: 'Rofi thumbnailer already up to date.',
},
{
status: 'skipped',
component: 'plugin',
@@ -396,8 +449,13 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
fs.mkdirSync(path.join(dataDir, 'thumbnailers'), { recursive: true });
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'old theme\n');
fs.writeFileSync(
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
'[Old Thumbnailer]\n',
);
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'old plugin\n');
fs.writeFileSync(
path.join(dataDir, 'plugin/subminer/version.lua'),
@@ -406,6 +464,7 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/stale.lua'), 'stale\n');
const { archive, tempDir } = makeSupportAssetsArchive({
themeContent: 'new theme\n',
thumbnailerContent: '[Thumbnailer Entry]\n',
pluginVersion: '0.12.0',
pluginMainContent: 'new plugin main\n',
extraPluginFiles: [{ relativePath: 'fresh.lua', content: 'fresh\n' }],
@@ -424,6 +483,12 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
path: dataDir,
message: 'Updated theme.',
},
{
status: 'updated',
component: 'thumbnailer',
path: dataDir,
message: 'Updated rofi thumbnailer.',
},
{
status: 'updated',
component: 'plugin',
@@ -435,6 +500,13 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
'new theme\n',
);
assert.equal(
fs.readFileSync(
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
'utf8',
),
'[Thumbnailer Entry]\n',
);
assert.equal(
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
'new plugin main\n',
@@ -479,6 +551,12 @@ test('updateSupportAssetsFromRelease returns protected commands for managed root
path: dataDir,
command: true,
},
{
status: 'protected',
component: 'thumbnailer',
path: dataDir,
command: true,
},
{
status: 'protected',
component: 'plugin',
@@ -488,6 +566,10 @@ test('updateSupportAssetsFromRelease returns protected commands for managed root
],
);
assert.match(results[0]?.command ?? '', /themes\/subminer\.rasi/);
assert.match(
results[0]?.command ?? '',
/thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer/,
);
assert.match(results[0]?.command ?? '', /plugin\/subminer/);
} finally {
fs.chmodSync(dataDir, originalMode);
@@ -522,3 +604,25 @@ test('updateSupportAssetsFromRelease returns missing-asset when release plugin v
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('updateSupportAssetsFromRelease rejects archives without the rofi thumbnailer', async () => {
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'managed theme\n');
const { archive, tempDir } = makeSupportAssetsArchive({ includeThumbnailer: false });
try {
const results = await runLinuxSupportAssetUpdate({ archive, xdgDataHome });
assert.deepEqual(results, [
{
status: 'missing-asset',
message: 'Support asset archive is missing the rofi thumbnailer.',
},
]);
} finally {
fs.rmSync(xdgDataHome, { recursive: true, force: true });
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
+65 -3
View File
@@ -9,13 +9,17 @@ import { compareSemverLike, findReleaseAsset } from './release-assets';
const execFileAsync = promisify(execFile);
const THEME_RELATIVE_PATH = path.join('themes', 'subminer.rasi');
const THUMBNAILER_RELATIVE_PATH = path.join(
'thumbnailers',
'subminer-ffmpegthumbnailer.thumbnailer',
);
const PLUGIN_ENTRYPOINT_RELATIVE_PATH = path.join('plugin', 'subminer', 'main.lua');
const PLUGIN_VERSION_RELATIVE_PATH = path.join('plugin', 'subminer', 'version.lua');
const PLUGIN_DIR_RELATIVE_PATH = path.join('plugin', 'subminer');
export interface SupportAssetsUpdateResult {
status: 'updated' | 'skipped' | 'protected' | 'hash-mismatch' | 'missing-asset';
component?: 'theme' | 'plugin';
component?: 'theme' | 'thumbnailer' | 'plugin';
path?: string;
command?: string;
message?: string;
@@ -69,11 +73,12 @@ async function readInstalledPluginVersion(pluginDir: string): Promise<string | n
async function detectManagedSupportAssetDataDirs(dataDirs: string[]): Promise<string[]> {
const managedDataDirs: string[] = [];
for (const dataDir of dataDirs) {
const [hasTheme, hasPlugin] = await Promise.all([
const [hasTheme, hasThumbnailer, hasPlugin] = await Promise.all([
pathExists(path.join(dataDir, THEME_RELATIVE_PATH)),
pathExists(path.join(dataDir, THUMBNAILER_RELATIVE_PATH)),
pathExists(path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH)),
]);
if (hasTheme || hasPlugin) {
if (hasTheme || hasThumbnailer || hasPlugin) {
managedDataDirs.push(dataDir);
}
}
@@ -163,8 +168,14 @@ export function buildProtectedSupportAssetsCommand(
`curl -fSL ${shellQuote(assetUrl)} -o "$tmp/subminer-assets.tar.gz"`,
`printf '%s %s\\n' ${quotedExpectedSha256} "$tmp/subminer-assets.tar.gz" | sha256sum -c -`,
'tar -xzf "$tmp/subminer-assets.tar.gz" -C "$tmp"',
'test -f "$tmp/assets/themes/subminer.rasi"',
'test -f "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"',
'test -f "$tmp/plugin/subminer/main.lua"',
'test -f "$tmp/plugin/subminer/version.lua"',
`sudo mkdir -p ${quotedDir}/themes`,
`sudo cp "$tmp/assets/themes/subminer.rasi" ${quotedDir}/themes/subminer.rasi`,
`sudo mkdir -p ${quotedDir}/thumbnailers`,
`sudo cp "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer" ${quotedDir}/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer`,
`sudo mkdir -p ${quotedDir}/plugin`,
`sudo rm -rf ${quotedStagedPluginDir} ${quotedBackupPluginDir}`,
`sudo cp -R "$tmp/plugin/subminer" ${quotedStagedPluginDir}`,
@@ -216,6 +227,12 @@ export async function updateSupportAssetsFromRelease(options: {
dataDir,
'Support asset path is not a directory.',
),
makeSupportAssetResult(
'skipped',
'thumbnailer',
dataDir,
'Support asset path is not a directory.',
),
makeSupportAssetResult(
'skipped',
'plugin',
@@ -244,6 +261,13 @@ export async function updateSupportAssetsFromRelease(options: {
'Theme install requires a manual command.',
command,
),
makeSupportAssetResult(
'protected',
'thumbnailer',
dataDir,
'Rofi thumbnailer install requires a manual command.',
command,
),
makeSupportAssetResult(
'protected',
'plugin',
@@ -284,6 +308,17 @@ export async function updateSupportAssetsFromRelease(options: {
}
const themeBytes = await fs.promises.readFile(themeSourcePath);
const thumbnailerSourcePath = path.join(tempDir, 'assets', THUMBNAILER_RELATIVE_PATH);
if (!(await pathExists(thumbnailerSourcePath))) {
return [
{
status: 'missing-asset',
message: 'Support asset archive is missing the rofi thumbnailer.',
},
];
}
const thumbnailerBytes = await fs.promises.readFile(thumbnailerSourcePath);
const sourcePluginDir = path.join(tempDir, PLUGIN_DIR_RELATIVE_PATH);
const sourcePluginEntrypoint = path.join(tempDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
if (!(await pathExists(sourcePluginEntrypoint))) {
@@ -328,6 +363,33 @@ export async function updateSupportAssetsFromRelease(options: {
);
}
const targetThumbnailerPath = path.join(dataDir, THUMBNAILER_RELATIVE_PATH);
const existingThumbnailerBytes = await readFileIfExists(targetThumbnailerPath);
if (
existingThumbnailerBytes &&
Buffer.compare(existingThumbnailerBytes, thumbnailerBytes) === 0
) {
results.push(
makeSupportAssetResult(
'skipped',
'thumbnailer',
dataDir,
'Rofi thumbnailer already up to date.',
),
);
} else {
await fs.promises.mkdir(path.dirname(targetThumbnailerPath), { recursive: true });
await fs.promises.writeFile(targetThumbnailerPath, thumbnailerBytes);
results.push(
makeSupportAssetResult(
'updated',
'thumbnailer',
dataDir,
existingThumbnailerBytes ? 'Updated rofi thumbnailer.' : 'Installed rofi thumbnailer.',
),
);
}
const targetPluginDir = path.join(dataDir, PLUGIN_DIR_RELATIVE_PATH);
const targetPluginEntrypoint = path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
const installedPluginVersion = await readInstalledPluginVersion(targetPluginDir);