`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>
121 lines
5.2 KiB
Rust
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)?)?)
|
|
}
|