Skip to main content

spool/desktop/
helpers.rs

1//! 桌面 facade 里用到的小工具:csv/filepath 解析、session 索引加载、continue/delete 命令、终端启动。
2
3use super::dto::DesktopSessionItem;
4use crate::daemon::LifecycleReadOptions;
5use crate::lifecycle_store::{LifecycleStore, lifecycle_root_from_config};
6use crate::session_sources::{load_provider_sessions, raw_session_id};
7use std::fs;
8use std::path::Path;
9
10use super::dto::DesktopDaemonRequest;
11
12pub fn parse_file_list(value: &str) -> Vec<String> {
13    value
14        .split([',', '\n'])
15        .map(str::trim)
16        .filter(|item| !item.is_empty())
17        .map(ToString::to_string)
18        .collect()
19}
20
21pub fn parse_csv_items(value: &str) -> Vec<String> {
22    value
23        .split(',')
24        .map(str::trim)
25        .filter(|item| !item.is_empty())
26        .map(ToString::to_string)
27        .collect()
28}
29
30pub(super) fn lifecycle_read_options(
31    daemon: Option<&DesktopDaemonRequest>,
32) -> LifecycleReadOptions {
33    match daemon {
34        Some(DesktopDaemonRequest {
35            enabled: true,
36            daemon_bin: Some(daemon_bin),
37        }) => LifecycleReadOptions::with_daemon(daemon_bin.as_path()),
38        _ => LifecycleReadOptions::default(),
39    }
40}
41
42pub(super) fn store_from_config_path(config_path: &Path) -> LifecycleStore {
43    let config_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
44    LifecycleStore::new(lifecycle_root_from_config(config_dir).as_path())
45}
46
47pub(super) fn load_session_index(_config_path: &Path) -> anyhow::Result<Vec<DesktopSessionItem>> {
48    let mut sessions = load_provider_sessions(None)?;
49    sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
50    Ok(sessions)
51}
52
53pub(super) fn load_session_index_filtered(
54    _config_path: &Path,
55    provider_filter: Option<&str>,
56) -> anyhow::Result<Vec<DesktopSessionItem>> {
57    let mut sessions = load_provider_sessions(provider_filter)?;
58    sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
59    Ok(sessions)
60}
61
62pub(super) fn build_continue_command(session: &DesktopSessionItem) -> Option<String> {
63    let cwd_prefix = session
64        .cwd
65        .as_deref()
66        .map(shell_cd_prefix)
67        .unwrap_or_default();
68    let raw_id = raw_session_id(&session.session_id);
69    match session.provider.as_str() {
70        "claude" => Some(format!("{cwd_prefix}claude -r \"{raw_id}\"")),
71        "codex" => Some(format!("{cwd_prefix}codex resume \"{raw_id}\"")),
72        _ => None,
73    }
74}
75
76pub(super) fn delete_session_file(session: &DesktopSessionItem) -> anyhow::Result<()> {
77    let Some(path) = session.source_path.as_deref() else {
78        anyhow::bail!("当前会话没有可删除的本地文件")
79    };
80    let file_path = Path::new(path);
81    if !file_path.exists() {
82        anyhow::bail!("本地会话文件不存在:{}", file_path.display())
83    }
84    if !file_path.is_file() {
85        anyhow::bail!("本地会话路径不是文件:{}", file_path.display())
86    }
87    fs::remove_file(file_path)?;
88    Ok(())
89}
90
91fn shell_cd_prefix(cwd: &str) -> String {
92    format!("cd '{}' && ", cwd.replace('\'', "'\\''"))
93}
94
95pub(super) fn launch_terminal_command(command: &str) -> anyhow::Result<()> {
96    #[cfg(target_os = "macos")]
97    {
98        let script = format!(
99            "tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
100            command.replace('\\', "\\\\").replace('"', "\\\"")
101        );
102        let output = std::process::Command::new("osascript")
103            .arg("-e")
104            .arg(script)
105            .output()?;
106        if !output.status.success() {
107            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
108            let detail = if stderr.is_empty() {
109                format!("osascript exited with status {}", output.status)
110            } else {
111                stderr
112            };
113            anyhow::bail!("无法通过 Terminal 启动命令 `{command}`:{detail}");
114        }
115        Ok(())
116    }
117
118    #[cfg(not(target_os = "macos"))]
119    {
120        let _ = command;
121        anyhow::bail!("continue session is only implemented for macOS right now")
122    }
123}