Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Tauri 2 + Vue 3 + TypeScript + Sass + vue-router + vue-i18n + Arco Design Vue.
- **Close to tray** (can be disabled in Settings).
- **Launch at login** (Settings toggle, registered for real via the autostart plugin).
- **i18n**: Simplified Chinese / English; JSON locale files are discovered, hot-reloaded, and precompiled by `@intlify/unplugin-vue-i18n` through Vite.
- **WSL2 instances (experimental)**: create DSH instances inside a WSL2 distro — Node.js / pnpm / the pinned version are installed inside the distro automatically and the instance's DSH_HOME lives under the distro's `~/.dsh-launcher/homes`. Once created, HOME works like a local instance (profiles CRUD, plugin install/enable/disable/uninstall, skills, MCP, icons, logs, modpack export/import, embedded terminal); files are accessed through the `\\wsl$\` UNC share and a stopped distro is booted automatically.

## Interface

Expand All @@ -47,6 +48,8 @@ Tauri 2 + Vue 3 + TypeScript + Sass + vue-router + vue-i18n + Arco Design Vue.
- **Instance settings → Plugins**: filter by profile, enable/disable plugins, multi-select batch enable/disable (`@deepseek-ai/*` core plugins hidden).
- **Settings**: language, close to tray, launch at login, DSH_HOME management.

> For WSL instances, plugin and modpack dependency installs run pnpm inside the distro (pulling Linux platform binaries); reading/writing files from the Windows side through UNC requires the distro to be running, which the launcher starts automatically.

## Development

Prerequisites: Node ≥ 20, pnpm, stable Rust (MSVC toolchain), WebView2.
Expand Down
3 changes: 3 additions & 0 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Tauri 2 + Vue 3 + TypeScript + Sass + vue-router + vue-i18n + Arco Design Vue。
- **关闭最小化到托盘**(可在设置关闭)。
- **开机自启**(设置页开关,经 autostart 插件真正注册)。
- **i18n**:简体中文 / English,JSON 语言文件由 `@intlify/unplugin-vue-i18n` 经 Vite 发现、热重载并预编译。
- **WSL2 实例(实验性)**:在 WSL2 发行版内创建 DSH 实例——自动安装发行版内 Node.js / pnpm / 指定版本,实例的 DSH_HOME 位于发行版 `~/.dsh-launcher/homes` 下;创建后可像本地实例一样操作 HOME(Profile 增删改、插件安装/启停/卸载、Skills、MCP、图标、日志、整合包导入导出、内置终端),文件访问经 `\\wsl$\` UNC 共享,发行版未运行时自动启动。

## 界面

Expand All @@ -47,6 +48,8 @@ Tauri 2 + Vue 3 + TypeScript + Sass + vue-router + vue-i18n + Arco Design Vue。
- **实例设置 → 插件页**:筛选 Profile 查看插件、启用/禁用、多选批量启用/禁用(`@deepseek-ai/*` 核心插件不显示)。
- **设置页**:语言、关闭到托盘、开机自启、DSH_HOME 管理。

> WSL 实例的插件安装/整合包依赖安装会在发行版内执行 pnpm(拉取 Linux 平台二进制);从 Windows 侧经 UNC 读写文件要求发行版处于运行状态,启动器会自动拉起。

## 开发

前置:Node ≥ 20、pnpm、Rust stable(含 MSVC 工具链)、WebView2。
Expand Down
44 changes: 21 additions & 23 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,16 +402,6 @@ pub fn copy_instance(
if !cfg.versions.iter().any(|v| v.id == source.version_id) {
return Err("DSH 版本不存在".to_string());
}
// WSL 实例(issue #19):复制为新的专属 HOME 需要在发行版内创建文件,
// 暂不支持;共享源 HOME 的纯记录复制不受限。
if input.new_home
&& cfg
.homes
.iter()
.any(|h| h.id == source.home_id && h.wsl.is_some())
{
return Err("WSL 实例暂不支持复制到新的专属 HOME".to_string());
}

// Resolve the DSH_HOME: reuse the source's, or create a dedicated one.
let home_id = if input.new_home {
Expand Down Expand Up @@ -464,17 +454,18 @@ pub fn copy_instance(
};
// A local icon is stored per instance id; copy the file for the clone,
// falling back to the launcher default when it cannot be carried over.
// WSL source homes map through \\wsl$\ (issue #19 follow-up).
if source.icon.as_deref() == Some("local") {
let src_home = cfg
.homes
.iter()
.find(|h| h.id == source.home_id)
.map(|h| h.path.clone());
.map(crate::wsl::home_fs_path);
let dst_home = cfg
.homes
.iter()
.find(|h| h.id == inst.home_id)
.map(|h| h.path.clone());
.map(crate::wsl::home_fs_path);
let copied = match (src_home, dst_home) {
(Some(src_home), Some(dst_home)) => {
let src_icon = crate::icons::local_icon_path(&src_home, &source.id);
Expand Down Expand Up @@ -503,7 +494,9 @@ pub fn list_profiles(state: State<'_, AppState>, home_id: String) -> Result<Vec<
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
let profiles_dir = home.path.join("profiles");
// WSL homes map through \\wsl$\ (issue #19 follow-up): the fs APIs below
// operate on the Windows-visible path.
let profiles_dir = crate::wsl::home_fs_path(home).join("profiles");
let mut out = Vec::new();
if let Ok(entries) = std::fs::read_dir(&profiles_dir) {
for entry in entries.flatten() {
Expand Down Expand Up @@ -544,7 +537,10 @@ pub fn list_profile_infos(
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
let profiles_dir = home.path.join("profiles");
// WSL homes map through \\wsl$\ (issue #19 follow-up): the fs APIs below
// operate on the Windows-visible path.
let home_dir = crate::wsl::home_fs_path(home);
let profiles_dir = home_dir.join("profiles");
let mut out = Vec::new();
if let Ok(entries) = std::fs::read_dir(&profiles_dir) {
for entry in entries.flatten() {
Expand All @@ -554,7 +550,7 @@ pub fn list_profile_infos(
continue;
}
if entry.path().is_dir() {
let kind = match process::profile_kind(&home.path, &name) {
let kind = match process::profile_kind(&home_dir, &name) {
process::InstanceKind::Web => "web",
process::InstanceKind::Tui => "tui",
process::InstanceKind::Other => "other",
Expand Down Expand Up @@ -588,7 +584,7 @@ pub fn create_profile(
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
home.path.join("profiles")
crate::wsl::home_fs_path(home).join("profiles")
};

let temp_dir = profiles_dir.join("__temp__");
Expand Down Expand Up @@ -624,7 +620,7 @@ pub fn copy_profile(
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
home.path.join("profiles")
crate::wsl::home_fs_path(home).join("profiles")
};

let from = profiles_dir.join(&source);
Expand Down Expand Up @@ -676,7 +672,7 @@ pub fn rename_profile(
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
home.path.join("profiles")
crate::wsl::home_fs_path(home).join("profiles")
};

let from = profiles_dir.join(&old_name);
Expand Down Expand Up @@ -725,7 +721,7 @@ pub fn delete_profile(
.iter()
.find(|h| h.id == home_id)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
home.path.join("profiles")
crate::wsl::home_fs_path(home).join("profiles")
};

let target = profiles_dir.join(&name);
Expand Down Expand Up @@ -890,8 +886,8 @@ fn resolve_instance_paths(
.find(|v| v.id == inst.version_id)
.ok_or_else(|| "版本不存在".to_string())?;
Ok((
home.path.clone(),
version.dir.clone(),
crate::wsl::home_fs_path(home),
crate::wsl::version_fs_path(version),
version.version.clone(),
))
}
Expand Down Expand Up @@ -1271,7 +1267,7 @@ fn read_tail(path: &std::path::Path, n: usize) -> Vec<String> {

/// Opens the DSH_HOME directory of one instance in the file manager.
#[tauri::command]
pub fn open_instance_directory(
pub async fn open_instance_directory(
state: State<'_, AppState>,
instance_id: String,
) -> Result<String, String> {
Expand All @@ -1290,7 +1286,9 @@ pub fn open_instance_directory(
(h.path.clone(), h.wsl.clone())
};
// WSL home (issue #19): open the distro's \\wsl$\ UNC share instead.
// Ensure the distro is running first so the share is reachable.
if let Some(distro) = wsl {
crate::wsl::ensure_distro_running(&state, &distro).await?;
let unc = crate::wsl::unc_path(&distro, &home.to_string_lossy());
crate::log_info!(
"在文件管理器中打开实例 {instance_id} 的 WSL DSH_HOME {}",
Expand Down Expand Up @@ -1354,7 +1352,7 @@ pub async fn create_launch_shortcut(
.homes
.iter()
.find(|h| h.id == inst.home_id)
.map(|h| h.path.clone())
.map(crate::wsl::home_fs_path)
.ok_or_else(|| "DSH_HOME 不存在".to_string())?;
(inst.name.clone(), inst.icon.clone(), home)
};
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fn instance_home(state: &AppState, instance_id: &str) -> Result<PathBuf, String>
cfg.homes
.iter()
.find(|h| h.id == inst.home_id)
.map(|h| h.path.clone())
.map(crate::wsl::home_fs_path)
.ok_or_else(|| "DSH_HOME 不存在".to_string())
}

Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub struct AppState {
/// from `terminals` (the settings-page shell): different lifecycle,
/// different status wiring.
pub tui_sessions: tokio::sync::Mutex<HashMap<String, tui::TuiSession>>,
/// WSL distros recently verified running (`wsl.rs::ensure_distro_running`
/// TTL cache): distro → last successful boot/probe timestamp.
pub distro_ready: tokio::sync::Mutex<HashMap<String, std::time::Instant>>,
}

/// Extracts a `dsh-launcher://…` deep link from process arguments (Windows
Expand Down Expand Up @@ -151,6 +154,7 @@ pub fn run() {
last_focused_instance: StdMutex::new(None),
terminals: tokio::sync::Mutex::new(HashMap::new()),
tui_sessions: tokio::sync::Mutex::new(HashMap::new()),
distro_ready: tokio::sync::Mutex::new(HashMap::new()),
});

// System tray with dynamic menu.
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fn home_path_of(state: &AppState, home_id: &str) -> Result<PathBuf, String> {
.homes
.iter()
.find(|h| h.id == home_id)
.map(|h| h.path.clone())
.map(crate::wsl::home_fs_path)
.ok_or_else(|| "DSH_HOME 不存在".to_string())
}

Expand Down
Loading