Improve files view behavior and Open In integration (#579)
* feat: open focused file in selected desktop app * fix: make file tree indicators match active open tabs * fix: limit preview functionality to markdown files only * feat: add Open In action to file editor toolbar. add antigravity to openin app list
This commit is contained in:
@@ -489,6 +489,181 @@ fn desktop_open_path(path: String, app: Option<String>) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct OpenCommandSpec {
|
||||||
|
program: &'static str,
|
||||||
|
args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn run_open_command_chain(specs: &[OpenCommandSpec]) -> Result<(), String> {
|
||||||
|
let mut failures: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for spec in specs {
|
||||||
|
match Command::new(spec.program).args(&spec.args).status() {
|
||||||
|
Ok(status) if status.success() => return Ok(()),
|
||||||
|
Ok(status) => failures.push(format!(
|
||||||
|
"{} {} exited with status {}",
|
||||||
|
spec.program,
|
||||||
|
spec.args.join(" "),
|
||||||
|
status
|
||||||
|
)),
|
||||||
|
Err(error) => failures.push(format!(
|
||||||
|
"{} {} failed: {}",
|
||||||
|
spec.program,
|
||||||
|
spec.args.join(" "),
|
||||||
|
error
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failures.is_empty() {
|
||||||
|
return Err("No launch strategies available".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(failures.join("; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn is_jetbrains_app_id(app_id: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
app_id,
|
||||||
|
"pycharm"
|
||||||
|
| "intellij"
|
||||||
|
| "webstorm"
|
||||||
|
| "phpstorm"
|
||||||
|
| "rider"
|
||||||
|
| "rustrover"
|
||||||
|
| "android-studio"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn cli_for_app_id(app_id: &str) -> Option<&'static str> {
|
||||||
|
match app_id {
|
||||||
|
"vscode" => Some("code"),
|
||||||
|
"cursor" => Some("cursor"),
|
||||||
|
"vscodium" => Some("codium"),
|
||||||
|
"windsurf" => Some("windsurf"),
|
||||||
|
"zed" => Some("zed"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn desktop_open_in_app(
|
||||||
|
project_path: String,
|
||||||
|
app_id: String,
|
||||||
|
app_name: String,
|
||||||
|
file_path: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let trimmed_project_path = project_path.trim();
|
||||||
|
if trimmed_project_path.is_empty() {
|
||||||
|
return Err("Project path is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed_app_id = app_id.trim().to_lowercase();
|
||||||
|
if trimmed_app_id.is_empty() {
|
||||||
|
return Err("App id is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed_app_name = app_name.trim();
|
||||||
|
if trimmed_app_name.is_empty() {
|
||||||
|
return Err("App name is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized_file_path = file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.trim())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let project = trimmed_project_path.to_string();
|
||||||
|
let app_name_owned = trimmed_app_name.to_string();
|
||||||
|
let file = normalized_file_path.map(|value| value.to_string());
|
||||||
|
let mut specs: Vec<OpenCommandSpec> = Vec::new();
|
||||||
|
|
||||||
|
if trimmed_app_id == "finder" {
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args: vec![project.clone()],
|
||||||
|
});
|
||||||
|
return run_open_command_chain(&specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(trimmed_app_id.as_str(), "terminal" | "iterm2" | "ghostty") {
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args: vec!["-a".to_string(), app_name_owned.clone(), project.clone()],
|
||||||
|
});
|
||||||
|
return run_open_command_chain(&specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cli) = cli_for_app_id(trimmed_app_id.as_str()) {
|
||||||
|
let mut cli_args = vec!["-n".to_string(), project.clone()];
|
||||||
|
if let Some(file_path) = file.as_ref() {
|
||||||
|
cli_args.push("-g".to_string());
|
||||||
|
cli_args.push(file_path.clone());
|
||||||
|
}
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: cli,
|
||||||
|
args: cli_args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_jetbrains_app_id(trimmed_app_id.as_str()) {
|
||||||
|
let mut args = vec![
|
||||||
|
"-na".to_string(),
|
||||||
|
app_name_owned.clone(),
|
||||||
|
"--args".to_string(),
|
||||||
|
project.clone(),
|
||||||
|
];
|
||||||
|
if let Some(file_path) = file.as_ref() {
|
||||||
|
args.push(file_path.clone());
|
||||||
|
}
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(file_path) = file.as_ref() {
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args: vec![
|
||||||
|
"-na".to_string(),
|
||||||
|
app_name_owned.clone(),
|
||||||
|
"--args".to_string(),
|
||||||
|
project.clone(),
|
||||||
|
file_path.clone(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args: vec!["-a".to_string(), app_name_owned.clone(), project.clone()],
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(file_path) = file {
|
||||||
|
specs.push(OpenCommandSpec {
|
||||||
|
program: "open",
|
||||||
|
args: vec!["-a".to_string(), app_name_owned, file_path],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return run_open_command_chain(&specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let _ = normalized_file_path;
|
||||||
|
Err("desktop_open_in_app is only supported on macOS".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct InstalledAppInfo {
|
struct InstalledAppInfo {
|
||||||
@@ -2655,6 +2830,7 @@ fn main() {
|
|||||||
desktop_new_window_at_url,
|
desktop_new_window_at_url,
|
||||||
desktop_clear_cache,
|
desktop_clear_cache,
|
||||||
desktop_open_path,
|
desktop_open_path,
|
||||||
|
desktop_open_in_app,
|
||||||
desktop_filter_installed_apps,
|
desktop_filter_installed_apps,
|
||||||
desktop_get_installed_apps,
|
desktop_get_installed_apps,
|
||||||
desktop_fetch_app_icons,
|
desktop_fetch_app_icons,
|
||||||
|
|||||||
@@ -10,57 +10,42 @@ import { toast } from '@/components/ui';
|
|||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, openDesktopPath, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
|
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
|
||||||
|
import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS, getOpenInAppById, type OpenInApp } from '@/lib/openInApps';
|
||||||
import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react';
|
import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react';
|
||||||
|
|
||||||
const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
|
const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
|
||||||
const TERMINAL_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
|
const TERMINAL_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
|
||||||
|
|
||||||
type OpenInAppOption = {
|
type OpenInAppOption = {
|
||||||
id: string;
|
id: OpenInApp['id'];
|
||||||
label: string;
|
label: OpenInApp['label'];
|
||||||
appName: string;
|
appName: OpenInApp['appName'];
|
||||||
fallbackIconDataUrl?: string;
|
fallbackIconDataUrl?: string;
|
||||||
iconDataUrl?: string;
|
iconDataUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPEN_IN_APPS: OpenInAppOption[] = [
|
const OPEN_IN_APP_OPTIONS: OpenInAppOption[] = OPEN_IN_APPS.map((app) => ({
|
||||||
{ id: 'finder', label: 'Finder', appName: 'Finder', fallbackIconDataUrl: FINDER_DEFAULT_ICON_DATA_URL },
|
...app,
|
||||||
{ id: 'terminal', label: 'Terminal', appName: 'Terminal', fallbackIconDataUrl: TERMINAL_DEFAULT_ICON_DATA_URL },
|
fallbackIconDataUrl: app.id === 'finder'
|
||||||
{ id: 'iterm2', label: 'iTerm2', appName: 'iTerm' },
|
? FINDER_DEFAULT_ICON_DATA_URL
|
||||||
{ id: 'ghostty', label: 'Ghostty', appName: 'Ghostty' },
|
: app.id === 'terminal'
|
||||||
{ id: 'vscode', label: 'VS Code', appName: 'Visual Studio Code' },
|
? TERMINAL_DEFAULT_ICON_DATA_URL
|
||||||
{ id: 'intellij', label: 'IntelliJ', appName: 'IntelliJ IDEA' },
|
: undefined,
|
||||||
{ id: 'visual-studio', label: 'Visual Studio', appName: 'Visual Studio' },
|
}));
|
||||||
{ id: 'cursor', label: 'Cursor', appName: 'Cursor' },
|
|
||||||
{ id: 'android-studio', label: 'Android Studio', appName: 'Android Studio' },
|
|
||||||
{ id: 'pycharm', label: 'PyCharm', appName: 'PyCharm' },
|
|
||||||
{ id: 'xcode', label: 'Xcode', appName: 'Xcode' },
|
|
||||||
{ id: 'sublime-text', label: 'Sublime', appName: 'Sublime Text' },
|
|
||||||
{ id: 'webstorm', label: 'WebStorm', appName: 'WebStorm' },
|
|
||||||
{ id: 'rider', label: 'Rider', appName: 'Rider' },
|
|
||||||
{ id: 'zed', label: 'Zed', appName: 'Zed' },
|
|
||||||
{ id: 'phpstorm', label: 'PhpStorm', appName: 'PhpStorm' },
|
|
||||||
{ id: 'eclipse', label: 'Eclipse', appName: 'Eclipse' },
|
|
||||||
{ id: 'windsurf', label: 'Windsurf', appName: 'Windsurf' },
|
|
||||||
{ id: 'vscodium', label: 'VSCodium', appName: 'VSCodium' },
|
|
||||||
{ id: 'rustrover', label: 'RustRover', appName: 'RustRover' },
|
|
||||||
{ id: 'trae', label: 'Trae', appName: 'Trae' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const DEFAULT_APP_ID = 'finder';
|
|
||||||
const ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']);
|
const ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']);
|
||||||
const getAlwaysAvailableApps = () => OPEN_IN_APPS.filter((app) => ALWAYS_AVAILABLE_APP_IDS.has(app.id));
|
const getAlwaysAvailableApps = () => OPEN_IN_APP_OPTIONS.filter((app) => ALWAYS_AVAILABLE_APP_IDS.has(app.id));
|
||||||
|
|
||||||
const getStoredAppId = (): string => {
|
const getStoredAppId = (): string => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return DEFAULT_APP_ID;
|
return DEFAULT_OPEN_IN_APP_ID;
|
||||||
}
|
}
|
||||||
const stored = window.localStorage.getItem('openInAppId');
|
const stored = window.localStorage.getItem('openInAppId');
|
||||||
if (stored && OPEN_IN_APPS.some((app) => app.id === stored)) {
|
if (stored && getOpenInAppById(stored)) {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
return DEFAULT_APP_ID;
|
return DEFAULT_OPEN_IN_APP_ID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppIcon = ({
|
const AppIcon = ({
|
||||||
@@ -102,10 +87,11 @@ const AppIcon = ({
|
|||||||
|
|
||||||
type OpenInAppButtonProps = {
|
type OpenInAppButtonProps = {
|
||||||
directory: string;
|
directory: string;
|
||||||
|
activeFilePath?: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) => {
|
export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenInAppButtonProps) => {
|
||||||
const [selectedAppId, setSelectedAppId] = React.useState(getStoredAppId);
|
const [selectedAppId, setSelectedAppId] = React.useState(getStoredAppId);
|
||||||
const [availableApps, setAvailableApps] = React.useState<OpenInAppOption[]>(getAlwaysAvailableApps);
|
const [availableApps, setAvailableApps] = React.useState<OpenInAppOption[]>(getAlwaysAvailableApps);
|
||||||
const [hasLoadedApps, setHasLoadedApps] = React.useState(false);
|
const [hasLoadedApps, setHasLoadedApps] = React.useState(false);
|
||||||
@@ -125,7 +111,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
const nextId = detail
|
const nextId = detail
|
||||||
&& typeof detail.openInAppId === 'string'
|
&& typeof detail.openInAppId === 'string'
|
||||||
&& detail.openInAppId.length > 0
|
&& detail.openInAppId.length > 0
|
||||||
&& OPEN_IN_APPS.some((app) => app.id === detail.openInAppId)
|
&& getOpenInAppById(detail.openInAppId)
|
||||||
? detail.openInAppId
|
? detail.openInAppId
|
||||||
: null;
|
: null;
|
||||||
if (!nextId) {
|
if (!nextId) {
|
||||||
@@ -163,7 +149,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
|
|
||||||
const allowed = new Set(installed.map((app) => app.name));
|
const allowed = new Set(installed.map((app) => app.name));
|
||||||
const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined]));
|
const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined]));
|
||||||
const filtered = OPEN_IN_APPS.filter(
|
const filtered = OPEN_IN_APP_OPTIONS.filter(
|
||||||
(app) => allowed.has(app.appName) || ALWAYS_AVAILABLE_APP_IDS.has(app.id)
|
(app) => allowed.has(app.appName) || ALWAYS_AVAILABLE_APP_IDS.has(app.id)
|
||||||
);
|
);
|
||||||
const withIcons = filtered.map((app) => ({
|
const withIcons = filtered.map((app) => ({
|
||||||
@@ -177,7 +163,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
const loadInstalledApps = React.useCallback(async (force?: boolean) => {
|
const loadInstalledApps = React.useCallback(async (force?: boolean) => {
|
||||||
if (isLoadingRef.current) return;
|
if (isLoadingRef.current) return;
|
||||||
if (hasLoadedApps && !force) return;
|
if (hasLoadedApps && !force) return;
|
||||||
const appNames = OPEN_IN_APPS.map((app) => app.appName);
|
const appNames = OPEN_IN_APP_OPTIONS.map((app) => app.appName);
|
||||||
if (retryTimeoutRef.current) {
|
if (retryTimeoutRef.current) {
|
||||||
clearTimeout(retryTimeoutRef.current);
|
clearTimeout(retryTimeoutRef.current);
|
||||||
retryTimeoutRef.current = null;
|
retryTimeoutRef.current = null;
|
||||||
@@ -271,9 +257,9 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
}, [isDesktopLocal, loadInstalledApps]);
|
}, [isDesktopLocal, loadInstalledApps]);
|
||||||
|
|
||||||
const selectedApp = React.useMemo(() => {
|
const selectedApp = React.useMemo(() => {
|
||||||
const known = OPEN_IN_APPS.find((app) => app.id === selectedAppId)
|
const known = OPEN_IN_APP_OPTIONS.find((app) => app.id === selectedAppId)
|
||||||
?? OPEN_IN_APPS.find((app) => app.id === DEFAULT_APP_ID)
|
?? OPEN_IN_APP_OPTIONS.find((app) => app.id === DEFAULT_OPEN_IN_APP_ID)
|
||||||
?? OPEN_IN_APPS[0];
|
?? OPEN_IN_APP_OPTIONS[0];
|
||||||
if (known) {
|
if (known) {
|
||||||
const iconDataUrl = availableApps.find((app) => app.appName === known.appName)?.iconDataUrl;
|
const iconDataUrl = availableApps.find((app) => app.appName === known.appName)?.iconDataUrl;
|
||||||
return iconDataUrl ? { ...known, iconDataUrl } : known;
|
return iconDataUrl ? { ...known, iconDataUrl } : known;
|
||||||
@@ -290,7 +276,10 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleOpen = async (app: OpenInAppOption) => {
|
const handleOpen = async (app: OpenInAppOption) => {
|
||||||
await openDesktopPath(directory, app.appName);
|
const opened = await openDesktopProjectInApp(directory, app.id, app.appName, activeFilePath);
|
||||||
|
if (!opened) {
|
||||||
|
await openDesktopPath(directory, app.appName);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = async (app: OpenInAppOption) => {
|
const handleSelect = async (app: OpenInAppOption) => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
|
|||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
|
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
@@ -506,6 +507,14 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
return worktreeDirectory || sessionDirectory || draftDirectory;
|
return worktreeDirectory || sessionDirectory || draftDirectory;
|
||||||
}, [draftDirectory, sessionDirectory, worktreeDirectory]);
|
}, [draftDirectory, sessionDirectory, worktreeDirectory]);
|
||||||
|
|
||||||
|
const selectedFilePath = useFilesViewTabsStore((state) => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return state.byRoot[directory]?.selectedPath ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
const actionDirectory = React.useMemo(() => {
|
const actionDirectory = React.useMemo(() => {
|
||||||
return normalize(openDirectory || activeProject?.path || '');
|
return normalize(openDirectory || activeProject?.path || '');
|
||||||
}, [activeProject?.path, openDirectory]);
|
}, [activeProject?.path, openDirectory]);
|
||||||
@@ -1084,7 +1093,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<OpenInAppButton directory={openDirectory} className="mr-1" />
|
<OpenInAppButton directory={openDirectory} activeFilePath={selectedFilePath} className="mr-1" />
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={isDesktopServicesOpen}
|
open={isDesktopServicesOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
@@ -305,13 +305,20 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
const inFlightDirsRef = React.useRef<Set<string>>(new Set());
|
const inFlightDirsRef = React.useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const EMPTY_PATHS: string[] = React.useMemo(() => [], []);
|
const EMPTY_PATHS: string[] = React.useMemo(() => [], []);
|
||||||
|
const EMPTY_CONTEXT_TABS: Array<{ mode: string; targetPath: string | null }> = React.useMemo(() => [], []);
|
||||||
const expandedPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.expandedPaths ?? EMPTY_PATHS) : EMPTY_PATHS));
|
const expandedPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.expandedPaths ?? EMPTY_PATHS) : EMPTY_PATHS));
|
||||||
const openPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.openPaths ?? EMPTY_PATHS) : EMPTY_PATHS));
|
|
||||||
const selectedPath = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.selectedPath ?? null) : null));
|
const selectedPath = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.selectedPath ?? null) : null));
|
||||||
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
|
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
|
||||||
const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath);
|
const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath);
|
||||||
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
|
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
|
||||||
const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath);
|
const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath);
|
||||||
|
const contextTabs = useUIStore((state) => (root ? (state.contextPanelByDirectory[root]?.tabs ?? EMPTY_CONTEXT_TABS) : EMPTY_CONTEXT_TABS));
|
||||||
|
const openContextFilePaths = React.useMemo(() => new Set(
|
||||||
|
contextTabs
|
||||||
|
.map((tab) => (tab.mode === 'file' ? tab.targetPath : null))
|
||||||
|
.filter((targetPath): targetPath is string => typeof targetPath === 'string' && targetPath.length > 0)
|
||||||
|
.map((targetPath) => normalizePath(targetPath))
|
||||||
|
), [contextTabs]);
|
||||||
|
|
||||||
// Context menu state
|
// Context menu state
|
||||||
const [contextMenuPath, setContextMenuPath] = React.useState<string | null>(null);
|
const [contextMenuPath, setContextMenuPath] = React.useState<string | null>(null);
|
||||||
@@ -552,7 +559,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
// --- Git status helpers (matching FilesView) ---
|
// --- Git status helpers (matching FilesView) ---
|
||||||
|
|
||||||
const getFileStatus = React.useCallback((path: string): FileStatus | null => {
|
const getFileStatus = React.useCallback((path: string): FileStatus | null => {
|
||||||
if (openPaths.includes(path)) return 'open';
|
if (openContextFilePaths.has(path)) return 'open';
|
||||||
|
|
||||||
if (gitStatus?.files) {
|
if (gitStatus?.files) {
|
||||||
const relative = path.startsWith(root + '/') ? path.slice(root.length + 1) : path;
|
const relative = path.startsWith(root + '/') ? path.slice(root.length + 1) : path;
|
||||||
@@ -564,7 +571,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [openPaths, gitStatus, root]);
|
}, [openContextFilePaths, gitStatus, root]);
|
||||||
|
|
||||||
const getFolderBadge = React.useCallback((dirPath: string): { modified: number; added: number } | null => {
|
const getFolderBadge = React.useCallback((dirPath: string): { modified: number; added: number } | null => {
|
||||||
if (!gitStatus?.files) return null;
|
if (!gitStatus?.files) return null;
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
RiFolderAddLine,
|
RiFolderAddLine,
|
||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
RiEditLine,
|
RiEditLine,
|
||||||
RiEyeLine,
|
|
||||||
RiFileCopyLine,
|
RiFileCopyLine,
|
||||||
|
RiFileTransferFill,
|
||||||
} from '@remixicon/react';
|
} from '@remixicon/react';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
@@ -74,6 +74,8 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|||||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
|
import { openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop';
|
||||||
|
import { getDefaultOpenInApp, getOpenInAppById, OPEN_DIRECTORY_APP_IDS, type OpenInApp } from '@/lib/openInApps';
|
||||||
|
|
||||||
type FileNode = {
|
type FileNode = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -88,6 +90,37 @@ type SelectedLineRange = {
|
|||||||
end: number;
|
end: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSelectedOpenInApp = (): OpenInApp => {
|
||||||
|
const stored = typeof window !== 'undefined' ? window.localStorage.getItem('openInAppId') : null;
|
||||||
|
const selected = getOpenInAppById(stored);
|
||||||
|
if (selected) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
return getDefaultOpenInApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParentDirectoryPath = (path: string): string => {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (!normalized) return '';
|
||||||
|
if (normalized === '/' || /^[A-Za-z]:\/$/.test(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSlash = normalized.lastIndexOf('/');
|
||||||
|
if (lastSlash < 0) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
if (lastSlash === 0) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = normalized.slice(0, lastSlash);
|
||||||
|
if (/^[A-Za-z]:$/.test(parent)) {
|
||||||
|
return `${parent}/`;
|
||||||
|
}
|
||||||
|
return parent;
|
||||||
|
};
|
||||||
|
|
||||||
const sortNodes = (items: FileNode[]) =>
|
const sortNodes = (items: FileNode[]) =>
|
||||||
items.slice().sort((a, b) => {
|
items.slice().sort((a, b) => {
|
||||||
if (a.type !== b.type) {
|
if (a.type !== b.type) {
|
||||||
@@ -413,6 +446,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
||||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||||
const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit');
|
const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit');
|
||||||
|
const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit');
|
||||||
|
|
||||||
const lightTheme = React.useMemo(
|
const lightTheme = React.useMemo(
|
||||||
() => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? getDefaultTheme(false),
|
() => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? getDefaultTheme(false),
|
||||||
@@ -527,9 +561,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const [copiedContent, setCopiedContent] = React.useState(false);
|
const [copiedContent, setCopiedContent] = React.useState(false);
|
||||||
const [copiedPath, setCopiedPath] = React.useState(false);
|
const [copiedPath, setCopiedPath] = React.useState(false);
|
||||||
|
|
||||||
// Markdown view mode (global, not per-file)
|
|
||||||
const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit');
|
|
||||||
|
|
||||||
const canCreateFile = Boolean(files.writeFile);
|
const canCreateFile = Boolean(files.writeFile);
|
||||||
const canCreateFolder = Boolean(files.createDirectory);
|
const canCreateFolder = Boolean(files.createDirectory);
|
||||||
const canRename = Boolean(files.rename);
|
const canRename = Boolean(files.rename);
|
||||||
@@ -543,6 +574,38 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
});
|
});
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
|
const handleOpenInSelectedApp = React.useCallback(async () => {
|
||||||
|
if (!selectedFile?.path || !root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedApp = getSelectedOpenInApp();
|
||||||
|
const fileDirectory = getParentDirectoryPath(selectedFile.path) || root;
|
||||||
|
|
||||||
|
if (OPEN_DIRECTORY_APP_IDS.has(selectedApp.id)) {
|
||||||
|
const openedDirectory = await openDesktopPath(fileDirectory, selectedApp.appName);
|
||||||
|
if (!openedDirectory) {
|
||||||
|
toast.error(`Failed to open in ${selectedApp.appName}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openedInApp = await openDesktopProjectInApp(root, selectedApp.id, selectedApp.appName, selectedFile.path);
|
||||||
|
if (openedInApp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openedFile = await openDesktopPath(selectedFile.path, selectedApp.appName);
|
||||||
|
if (openedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openedDirectory = await openDesktopPath(fileDirectory, selectedApp.appName);
|
||||||
|
if (!openedDirectory) {
|
||||||
|
toast.error(`Failed to open in ${selectedApp.appName}`);
|
||||||
|
}
|
||||||
|
}, [root, selectedFile?.path]);
|
||||||
|
|
||||||
const handleOpenDialog = React.useCallback((type: 'createFile' | 'createFolder' | 'rename' | 'delete', data: { path: string; name?: string; type?: 'file' | 'directory' }) => {
|
const handleOpenDialog = React.useCallback((type: 'createFile' | 'createFolder' | 'rename' | 'delete', data: { path: string; name?: string; type?: 'file' | 'directory' }) => {
|
||||||
setActiveDialog(type);
|
setActiveDialog(type);
|
||||||
setDialogData(data);
|
setDialogData(data);
|
||||||
@@ -776,37 +839,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
}
|
}
|
||||||
}, [loadDirectory, root, showGitignored, showHidden]);
|
}, [loadDirectory, root, showGitignored, showHidden]);
|
||||||
|
|
||||||
const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode';
|
|
||||||
|
|
||||||
// Load markdown view mode preference from localStorage on mount
|
|
||||||
React.useEffect(() => {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(MD_VIEWER_MODE_KEY);
|
|
||||||
if (stored === 'preview') {
|
|
||||||
setMdViewMode('preview');
|
|
||||||
} else if (stored === 'edit') {
|
|
||||||
setMdViewMode('edit');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore localStorage errors
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Save markdown view mode preference to localStorage
|
|
||||||
const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
|
||||||
setMdViewMode(mode);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(MD_VIEWER_MODE_KEY, mode);
|
|
||||||
} catch {
|
|
||||||
// Ignore localStorage errors
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Get the view mode for a markdown file (from state, default to 'edit')
|
|
||||||
const getMdViewMode = React.useCallback((): 'preview' | 'edit' => {
|
|
||||||
return mdViewMode;
|
|
||||||
}, [mdViewMode]);
|
|
||||||
|
|
||||||
const handleDialogSubmit = React.useCallback(async (e?: React.FormEvent) => {
|
const handleDialogSubmit = React.useCallback(async (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (!dialogData || !activeDialog) return;
|
if (!dialogData || !activeDialog) return;
|
||||||
@@ -1614,6 +1646,34 @@ const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
|||||||
setTextViewMode('edit');
|
setTextViewMode('edit');
|
||||||
}, [selectedFile?.path]);
|
}, [selectedFile?.path]);
|
||||||
|
|
||||||
|
const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode';
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(MD_VIEWER_MODE_KEY);
|
||||||
|
if (stored === 'preview') {
|
||||||
|
setMdViewMode('preview');
|
||||||
|
} else if (stored === 'edit') {
|
||||||
|
setMdViewMode('edit');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
||||||
|
setMdViewMode(mode);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MD_VIEWER_MODE_KEY, mode);
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getMdViewMode = React.useCallback((): 'preview' | 'edit' => {
|
||||||
|
return mdViewMode;
|
||||||
|
}, [mdViewMode]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!pendingFileNavigation || !root) {
|
if (!pendingFileNavigation || !root) {
|
||||||
return;
|
return;
|
||||||
@@ -2196,25 +2256,21 @@ const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void handleOpenInSelectedApp()}
|
||||||
|
className="h-5 w-5 p-0 text-muted-foreground opacity-70 hover:opacity-100"
|
||||||
|
title="Open in selected app"
|
||||||
|
aria-label="Open in selected app"
|
||||||
|
>
|
||||||
|
<RiFileTransferFill className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{canEdit && !isSelectedImage && (
|
{canEdit && !isSelectedImage && (
|
||||||
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canUseShikiFileView && canEdit && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTextViewMode((prev) => (prev === 'view' ? 'edit' : 'view'))}
|
|
||||||
className={cn(
|
|
||||||
'h-5 w-5 p-0 transition-opacity',
|
|
||||||
textViewMode === 'edit' ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-70 hover:opacity-100'
|
|
||||||
)}
|
|
||||||
title={textViewMode === 'view' ? 'Switch to edit mode' : 'Switch to highlighted view'}
|
|
||||||
>
|
|
||||||
{textViewMode === 'view' ? <RiEditLine className="size-4" /> : <RiEyeLine className="size-4" />}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSelectedImage && (
|
{!isSelectedImage && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -2367,7 +2423,7 @@ const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
|||||||
<div className="h-full overflow-auto p-3">
|
<div className="h-full overflow-auto p-3">
|
||||||
{fileContent.length > 500 * 1024 && (
|
{fileContent.length > 500 * 1024 && (
|
||||||
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
||||||
⚠️ This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
@@ -2637,25 +2693,21 @@ const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void handleOpenInSelectedApp()}
|
||||||
|
className="h-6 w-6 p-0 text-muted-foreground opacity-70 hover:opacity-100"
|
||||||
|
title="Open in selected app"
|
||||||
|
aria-label="Open in selected app"
|
||||||
|
>
|
||||||
|
<RiFileTransferFill className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{canEdit && !isSelectedImage && (
|
{canEdit && !isSelectedImage && (
|
||||||
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canUseShikiFileView && canEdit && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTextViewMode((prev) => (prev === 'view' ? 'edit' : 'view'))}
|
|
||||||
className={cn(
|
|
||||||
'h-6 w-6 p-0 transition-opacity',
|
|
||||||
textViewMode === 'edit' ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-70 hover:opacity-100'
|
|
||||||
)}
|
|
||||||
title={textViewMode === 'view' ? 'Switch to edit mode' : 'Switch to highlighted view'}
|
|
||||||
>
|
|
||||||
{textViewMode === 'view' ? <RiEditLine className="size-4" /> : <RiEyeLine className="size-4" />}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSelectedImage && (
|
{!isSelectedImage && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -434,6 +434,40 @@ export const openDesktopPath = async (path: string, app?: string | null): Promis
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openDesktopProjectInApp = async (
|
||||||
|
projectPath: string,
|
||||||
|
appId: string,
|
||||||
|
appName: string,
|
||||||
|
filePath?: string | null,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedProjectPath = projectPath?.trim();
|
||||||
|
const trimmedAppId = appId?.trim();
|
||||||
|
const trimmedAppName = appName?.trim();
|
||||||
|
const trimmedFilePath = typeof filePath === 'string' ? filePath.trim() : '';
|
||||||
|
|
||||||
|
if (!trimmedProjectPath || !trimmedAppId || !trimmedAppName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||||
|
await tauri?.core?.invoke?.('desktop_open_in_app', {
|
||||||
|
projectPath: trimmedProjectPath,
|
||||||
|
appId: trimmedAppId,
|
||||||
|
appName: trimmedAppName,
|
||||||
|
filePath: trimmedFilePath.length > 0 ? trimmedFilePath : undefined,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to open project in app (tauri)', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
|
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
|
||||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export type OpenInApp = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
appName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OPEN_IN_APPS: OpenInApp[] = [
|
||||||
|
{ id: 'finder', label: 'Finder', appName: 'Finder' },
|
||||||
|
{ id: 'terminal', label: 'Terminal', appName: 'Terminal' },
|
||||||
|
{ id: 'iterm2', label: 'iTerm2', appName: 'iTerm' },
|
||||||
|
{ id: 'ghostty', label: 'Ghostty', appName: 'Ghostty' },
|
||||||
|
{ id: 'vscode', label: 'VS Code', appName: 'Visual Studio Code' },
|
||||||
|
{ id: 'intellij', label: 'IntelliJ', appName: 'IntelliJ IDEA' },
|
||||||
|
{ id: 'visual-studio', label: 'Visual Studio', appName: 'Visual Studio' },
|
||||||
|
{ id: 'cursor', label: 'Cursor', appName: 'Cursor' },
|
||||||
|
{ id: 'android-studio', label: 'Android Studio', appName: 'Android Studio' },
|
||||||
|
{ id: 'pycharm', label: 'PyCharm', appName: 'PyCharm' },
|
||||||
|
{ id: 'xcode', label: 'Xcode', appName: 'Xcode' },
|
||||||
|
{ id: 'sublime-text', label: 'Sublime', appName: 'Sublime Text' },
|
||||||
|
{ id: 'webstorm', label: 'WebStorm', appName: 'WebStorm' },
|
||||||
|
{ id: 'rider', label: 'Rider', appName: 'Rider' },
|
||||||
|
{ id: 'zed', label: 'Zed', appName: 'Zed' },
|
||||||
|
{ id: 'phpstorm', label: 'PhpStorm', appName: 'PhpStorm' },
|
||||||
|
{ id: 'eclipse', label: 'Eclipse', appName: 'Eclipse' },
|
||||||
|
{ id: 'windsurf', label: 'Windsurf', appName: 'Windsurf' },
|
||||||
|
{ id: 'vscodium', label: 'VSCodium', appName: 'VSCodium' },
|
||||||
|
{ id: 'rustrover', label: 'RustRover', appName: 'RustRover' },
|
||||||
|
{ id: 'antigravity', label: 'Antigravity', appName: 'Antigravity' },
|
||||||
|
{ id: 'trae', label: 'Trae', appName: 'Trae' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_OPEN_IN_APP_ID = 'finder';
|
||||||
|
export const OPEN_DIRECTORY_APP_IDS = new Set(['finder', 'terminal', 'iterm2', 'ghostty']);
|
||||||
|
|
||||||
|
export const getOpenInAppById = (id: string | null | undefined): OpenInApp | null => {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return OPEN_IN_APPS.find((app) => app.id === id) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultOpenInApp = (): OpenInApp => {
|
||||||
|
return getOpenInAppById(DEFAULT_OPEN_IN_APP_ID) ?? OPEN_IN_APPS[0];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user