Electron updater now uses Electron release metadata only Removed legacy Tauri package and migration workflow Replaced Tauri shim usage with the desktop bridge
152 lines
4.2 KiB
TypeScript
152 lines
4.2 KiB
TypeScript
import { clsx, type ClassValue } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { isDesktopShell } from "@/lib/desktop";
|
|
import { matchesFuzzyQuery } from "@/lib/search/fuzzySearch";
|
|
import type { I18nKey } from "@/lib/i18n";
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
|
|
/**
|
|
* Detects if the current platform is macOS.
|
|
* Uses navigator.userAgent in browser environments.
|
|
*/
|
|
export const isMacOS = (): boolean => {
|
|
if (typeof navigator === 'undefined') return false;
|
|
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
|
};
|
|
|
|
export const isWindows = (): boolean => {
|
|
if (typeof navigator === 'undefined') return false;
|
|
return /Windows/.test(navigator.userAgent || '');
|
|
};
|
|
|
|
export const getRevealLabelKey = (): I18nKey => {
|
|
if (isMacOS()) return 'common.revealPath.finder';
|
|
if (isWindows()) return 'common.revealPath.fileExplorer';
|
|
return 'common.revealPath.fileManager';
|
|
};
|
|
|
|
/**
|
|
* Checks if the platform-appropriate modifier key is pressed.
|
|
* On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey).
|
|
* Browser intercepts Cmd shortcuts, so we only use Cmd in the desktop app.
|
|
*/
|
|
export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => {
|
|
return isMacOS() && isDesktopShell() ? e.metaKey : e.ctrlKey;
|
|
};
|
|
|
|
/**
|
|
* Returns the platform-appropriate modifier key label.
|
|
* On macOS desktop app: "⌘", on other platforms or web: "Ctrl"
|
|
* Browser intercepts Cmd shortcuts, so we only show Cmd in the desktop app.
|
|
*/
|
|
export const getModifierLabel = (): string => {
|
|
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
|
|
};
|
|
|
|
export const truncatePathMiddle = (
|
|
value: string,
|
|
options?: { maxLength?: number }
|
|
): string => {
|
|
const source = value ?? "";
|
|
const maxLength = Math.max(16, options?.maxLength ?? 45);
|
|
if (source.length <= maxLength) {
|
|
return source;
|
|
}
|
|
|
|
const segments = source.split('/');
|
|
if (segments.length <= 1) {
|
|
return source;
|
|
}
|
|
|
|
const fileName = segments.pop() ?? '';
|
|
if (!fileName) {
|
|
return source;
|
|
}
|
|
|
|
const prefixBudget = Math.max(0, maxLength - (fileName.length + 2));
|
|
if (prefixBudget <= 0) {
|
|
return `…/${fileName}`;
|
|
}
|
|
|
|
let prefix = '';
|
|
for (const segment of segments) {
|
|
if (!segment) {
|
|
continue;
|
|
}
|
|
const candidate = prefix ? `${prefix}/${segment}` : segment;
|
|
if (candidate.length > prefixBudget) {
|
|
break;
|
|
}
|
|
prefix = candidate;
|
|
}
|
|
|
|
if (!prefix) {
|
|
const first = segments[0] ?? '';
|
|
prefix = first ? first.slice(0, prefixBudget) : '';
|
|
}
|
|
|
|
return prefix ? `${prefix}…/${fileName}` : `…/${fileName}`;
|
|
};
|
|
|
|
const normalizePath = (value: string) => {
|
|
if (!value) return "";
|
|
if (value === "/") return "/";
|
|
return value.replace(/\/+$/, "");
|
|
};
|
|
|
|
export function formatPathForDisplay(path: string | null | undefined, homeDirectory?: string | null): string {
|
|
if (!path) {
|
|
return "";
|
|
}
|
|
|
|
const normalizedPath = normalizePath(path);
|
|
if (normalizedPath === "/") {
|
|
return "/";
|
|
}
|
|
|
|
const normalizedHome = homeDirectory ? normalizePath(homeDirectory) : undefined;
|
|
|
|
if (normalizedHome && normalizedHome !== "/") {
|
|
if (normalizedPath === normalizedHome) {
|
|
return "~";
|
|
}
|
|
if (normalizedPath.startsWith(`${normalizedHome}/`)) {
|
|
const relative = normalizedPath.slice(normalizedHome.length + 1);
|
|
return relative ? `~/${relative}` : "~";
|
|
}
|
|
}
|
|
|
|
return normalizedPath;
|
|
}
|
|
|
|
export function formatDirectoryName(path: string | null | undefined, homeDirectory?: string | null): string {
|
|
if (!path) {
|
|
return "/";
|
|
}
|
|
|
|
const normalizedPath = normalizePath(path);
|
|
if (!normalizedPath || normalizedPath === "/") {
|
|
return "/";
|
|
}
|
|
|
|
const normalizedHome = homeDirectory ? normalizePath(homeDirectory) : undefined;
|
|
if (normalizedHome && normalizedHome !== "/" && normalizedPath === normalizedHome) {
|
|
return "~";
|
|
}
|
|
|
|
const segments = normalizedPath.split("/");
|
|
const name = segments.pop() || normalizedPath;
|
|
return name || "/";
|
|
}
|
|
|
|
/**
|
|
* Fuzzy search using Fuse.js with typo tolerance.
|
|
* Returns true if query fuzzy-matches target (e.g. "coude" matches "claude")
|
|
*/
|
|
export function fuzzyMatch(target: string, query: string): boolean {
|
|
return matchesFuzzyQuery(target, query);
|
|
}
|