Files
shell-rs/src/main.rs
T
XuzandClaude Opus 5.5 87c5cfb120 堡垒机用 ssh:// 链接打开临时连接(兼容 Xshell 的调用方式)
`ShellRS ssh://用户[:密码]@地址[:端口]` 或 `ShellRS -url … -newtab 名称`
开一个终端标签连过去。只看第一个参数判断是不是链接,外部 CLI 的命令不受
影响;Windows 发布版也认。

- 临时连接不保存:HostStore 把它放在单独的 transient 里,按 id 找得到,
  但不在 hosts() 里,不写库、不进最近连接;终端和 SFTP 标签都关掉就忘掉。
- 链接里的密码只在 TransientSecretStore 的内存里(SecretRef::Transient),
  不进钥匙串。
- ShellRS 已在运行时,Request::Activate 带上链接交给它打开;不带链接时消息
  和以前一字不差,旧版收到也只是调出窗口,所以不升协议版本。
- 链接有问题时弹通知说明原因,文字里不带链接原文。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:24:42 +08:00

121 lines
5.2 KiB
Rust

// A release build on Windows is a GUI program, so opening it does not bring
// up a console window as well. Debug builds keep theirs for the logs.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
use gpui_kit::component::{Root, notification::Notification};
use gpui_kit::*;
use shellrs::host::{HostDatabase, HostStore};
use shellrs::settings::SettingsStore;
fn main() {
let args = shellrs::cli::command_line_arguments().unwrap_or_default();
// `ShellRS ssh://user@host:port` or `ShellRS -url … -newtab 名称`: a
// bastion host opening ShellRS the way it opens Xshell. On every build,
// a Windows release build above all, where bastion hosts run it.
let link = shellrs::cli::link_arguments(&args);
// `shellrs list`, `shellrs exec …`: the command, not the app. Not in a
// Windows release build, which has nowhere to print: there the command
// is `shellrs-cli.exe`, which 设置 → 外部 CLI puts on the PATH.
#[cfg(any(not(windows), debug_assertions))]
if link.is_none() && !args.is_empty() {
std::process::exit(shellrs::cli::main(args));
}
// One ShellRS per data directory. Each keeps the hosts in memory and
// writes its changes through, so a second one would write over the
// first one's; opened again, ShellRS brings the running one forward
// and hands it the link.
if shellrs::cli::activate_running_app(&shellrs::app::cli_socket_path(), link.clone()) {
return;
}
let app = gpui_kit::application().with_assets(shellrs::app::AppAssets);
// macOS: the Dock icon of a ShellRS whose window was closed, which only
// hid it (see `hide_when_closed`).
app.on_reopen(|cx| {
if let Some(window) = cx.windows().first().copied() {
window
.update(cx, |_, window, cx| shellrs::app::bring_forward(window, cx))
.ok();
}
});
app.run(|cx| {
shellrs::init(cx);
shellrs::app::show_logo_when_unbundled(cx);
cx.activate(true);
let options = shellrs::workspace::window_options(cx);
cx.spawn(async move |cx| {
cx.open_window(options, |window, cx| {
window.activate_window();
window.set_window_title("ShellRS");
shellrs::app::hide_when_closed(window, cx);
let (store, problem) = open_store();
let store = cx.new(|_| store);
let (settings, settings_problem) = open_settings();
let settings = cx.new(|_| settings);
let workspace =
cx.new(|cx| shellrs::workspace::Workspace::new(store, settings, window, cx));
// Not pushed here: the window has no `Root` to show it yet.
for problem in [problem, settings_problem].into_iter().flatten() {
shellrs::workspace::notify_once_open(Notification::error(problem), window, cx);
}
if let Some(link) = link {
shellrs::workspace::open_link_once_open(&workspace, link, window, cx);
}
cx.new(|cx| Root::new(workspace, window, cx))
})
.expect("failed to open window");
})
.detach();
});
// On Windows the event loop ends and `run` returns: an update that is
// to be installed now starts its setup, which waits for this process.
shellrs::update::start_handed_over();
}
/// Load the hosts saved on disk. If the database cannot be opened the
/// window still comes up, on a store that forgets everything when it closes;
/// the returned message says so.
///
/// Either way the store gets the system keychain, which is where passwords
/// live. A machine without one still runs; the host form says so and
/// disables its password field. Pasted and generated private keys go
/// beside the database.
fn open_store() -> (HostStore, Option<SharedString>) {
let secrets = shellrs::secrets::system_secret_store();
match load_store() {
// Keys are kept only with a database to remember them: a store
// that forgets everything would leave them behind unused.
Ok(store) => (
store
.with_secrets(secrets)
.with_key_dir(shellrs::app::keys_dir()),
None,
),
Err(error) => {
eprintln!("shellrs: 无法打开本地数据库:{error}");
(
HostStore::empty().with_secrets(secrets),
Some(format!("无法打开本地数据库,本次运行的改动不会被保存:{error}").into()),
)
}
}
}
/// Load the settings file. Without a data directory the settings still
/// work, only for this run; the returned message says so.
fn open_settings() -> (SettingsStore, Option<SharedString>) {
match shellrs::app::settings_path() {
Ok(path) => SettingsStore::load(path),
Err(error) => (
SettingsStore::in_memory(),
Some(format!("无法保存设置,本次运行的改动不会被保存:{error}").into()),
),
}
}
fn load_store() -> anyhow::Result<HostStore> {
let path = shellrs::app::database_path()?;
Ok(HostStore::load(HostDatabase::open(&path)?)?)
}