mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(macos): add default terminal integration (#818)
* feat(macos): add default terminal integration * fix(macos): route external opens through the layout pull Five holes in the LaunchServices path, all on the way from a URL to a tab. The `ssh:` arm handed the raw URL back to `parse_quick_connect`, which reads a bare `user@host:port` typed into Quick Connect. Everything a URL carries past the authority landed in the wrong field: `ssh://h:2200/` parsed its port as `2200/` and was dropped on the floor, `ssh://h/srv` became the host `h/srv`, and the percent escapes `url` was added for were never decoded. Read the authority off the parsed URL instead. `x-man-page://3/printf` is Apple's sectioned form, and taking the host as the page name ran `man 3`, which asks the user what page they wanted. Section and page are now both carried. A window that is pulling its layout is one `Adopt::IfEmpty` will not adopt into, so a tab inserted while the pull is out comes back as the whole workspace — the failure `then_open` already exists to avoid. Both the script/man path and the SSH path inserted straight into a freshly restored window, so `then_open` becomes a list of parked requests and carries a command or an SSH link as well as a folder. A cold `ssh://` link also went through `open_at` directly, claiming a fresh workspace and leaving the restored one detached and unannounced; it takes the shared restore now. `new_tab_running` wrote the command whether or not a tab opened, so a failed spawn typed a script path and a newline into whatever pane was focused before — a shell mid-line, or an agent. Left alone deliberately: an `ssh://` link still connects without a confirmation, which is a product call rather than a defect. Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
@@ -72,6 +72,39 @@ cat > "$APP/Contents/Info.plist" <<PLIST
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
<key>NSPrincipalClass</key><string>NSApplication</string>
|
||||
<!-- LaunchServices has no global default-terminal setting. These declare
|
||||
the specific document types tty7 can open, and Settings lets users
|
||||
select tty7 as their handler for them. -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>Folder</string>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
<key>LSItemContentTypes</key><array><string>public.directory</string></array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>Shell Script</string>
|
||||
<key>CFBundleTypeRole</key><string>Shell</string>
|
||||
<key>LSItemContentTypes</key><array><string>public.shell-script</string></array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>Unix Executable</string>
|
||||
<key>CFBundleTypeRole</key><string>Shell</string>
|
||||
<key>LSItemContentTypes</key><array><string>public.unix-executable</string></array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key><string>SSH</string>
|
||||
<key>CFBundleURLSchemes</key><array><string>ssh</string></array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key><string>Man Page</string>
|
||||
<key>CFBundleURLSchemes</key><array><string>x-man-page</string></array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- tty7 is a terminal workbench: panes are forked from the bundled
|
||||
executable, so macOS attributes a child process's protected-resource
|
||||
requests to tty7.app. Without these usage strings a program you run in
|
||||
|
||||
Generated
+2
@@ -9848,6 +9848,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
@@ -9862,6 +9863,7 @@ dependencies = [
|
||||
"tty7-core",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows 0.58.0",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
@@ -66,6 +66,13 @@ smallvec.workspace = true
|
||||
serde = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
tempfile = "3"
|
||||
# LaunchServices sends files and custom URL schemes through gpui's unified
|
||||
# open-URL callback. `url` keeps the decoding and SSH authority parsing here
|
||||
# standards-compliant rather than treating percent escapes as literal paths,
|
||||
# and `percent-encoding` — already in the tree under `url` — spells the
|
||||
# userinfo and man-page names back out.
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
# SSH profile ids in the connection manager UI (`ui::ssh_connect`,
|
||||
# `ui::settings`, the command palette). The profiles themselves — and the
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//! macOS default-terminal integration.
|
||||
//!
|
||||
//! LaunchServices has no system-wide "default terminal" switch. Instead it
|
||||
//! remembers a handler per document type and URL scheme, which is exactly the
|
||||
//! narrow promise tty7 can make: folders, runnable local files, SSH links, and
|
||||
//! man-page links.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const BUNDLE_ID: &str = "com.github.tty7";
|
||||
#[cfg(target_os = "macos")]
|
||||
const URL_SCHEMES: &[&str] = &["ssh", "x-man-page"];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ExternalOpen {
|
||||
Folder(PathBuf),
|
||||
Runnable(PathBuf),
|
||||
Ssh(tty7_core::core::ssh_profile::QuickConnect),
|
||||
/// `man [section] page`, from `x-man-page://page` or the sectioned
|
||||
/// `x-man-page://section/page` that Apple's own man-page links use.
|
||||
ManPage {
|
||||
section: Option<String>,
|
||||
page: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parses the strings supplied by gpui's `Application::on_open_urls`. Finder
|
||||
/// represents document opens as `file:` URLs on macOS, so paths and custom
|
||||
/// schemes deliberately share this one entry point.
|
||||
pub fn parse_open_url(raw: &str) -> Result<ExternalOpen, String> {
|
||||
let url = url::Url::parse(raw).map_err(|error| format!("invalid URL: {error}"))?;
|
||||
match url.scheme() {
|
||||
"file" => {
|
||||
let path = url
|
||||
.to_file_path()
|
||||
.map_err(|_| "the file URL does not name a local path".to_string())?;
|
||||
if path.is_dir() {
|
||||
Ok(ExternalOpen::Folder(path))
|
||||
} else {
|
||||
Ok(ExternalOpen::Runnable(path))
|
||||
}
|
||||
}
|
||||
"ssh" => quick_connect_from(&url).map(ExternalOpen::Ssh),
|
||||
"x-man-page" => man_page_from(&url),
|
||||
scheme => Err(format!("unsupported URL scheme: {scheme}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the authority `Url` has already validated rather than handing the raw
|
||||
/// string to [`tty7_core::core::ssh_profile::parse_quick_connect`]. That parser
|
||||
/// takes a bare `user@host:port` typed into Quick Connect, so everything a URL
|
||||
/// may carry past the authority lands in the wrong field: `ssh://h:22/` parses
|
||||
/// its port as `22/` and is dropped, and `ssh://h/srv` becomes the host
|
||||
/// `h/srv`.
|
||||
fn quick_connect_from(
|
||||
url: &url::Url,
|
||||
) -> Result<tty7_core::core::ssh_profile::QuickConnect, String> {
|
||||
let host = match url.host() {
|
||||
// `Host`'s own `Display` brackets an IPv6 address for use in a URL.
|
||||
// `QuickConnect` holds the bare form and brackets it again when it
|
||||
// writes one out, so unwrap it here.
|
||||
Some(url::Host::Ipv6(address)) => address.to_string(),
|
||||
Some(host) => host.to_string(),
|
||||
None => return Err("the SSH URL has no host".to_string()),
|
||||
};
|
||||
if host.is_empty() {
|
||||
return Err("the SSH URL has no host".to_string());
|
||||
}
|
||||
let user = decode(url.username(), "user name")?;
|
||||
Ok(tty7_core::core::ssh_profile::QuickConnect {
|
||||
user: (!user.is_empty()).then_some(user),
|
||||
host,
|
||||
// Port 0 is what `Url` gives back for `:0`, and no SSH server listens
|
||||
// there; the Quick Connect parser rejects it the same way.
|
||||
port: url.port().filter(|port| *port != 0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Apple writes these two ways: `x-man-page://ls`, and `x-man-page://3/printf`
|
||||
/// where the authority is the *section*. Taking the host as the page name
|
||||
/// turns the second form into `man 3`, which asks the user what page they
|
||||
/// wanted.
|
||||
fn man_page_from(url: &url::Url) -> Result<ExternalOpen, String> {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(host) = url.host_str() {
|
||||
parts.push(decode(host, "page name")?);
|
||||
}
|
||||
for segment in url.path().split('/') {
|
||||
parts.push(decode(segment, "page name")?);
|
||||
}
|
||||
parts.retain(|part| !part.is_empty());
|
||||
let mut parts = parts.into_iter();
|
||||
let first = parts
|
||||
.next()
|
||||
.ok_or_else(|| "the man-page URL has no page name".to_string())?;
|
||||
Ok(match parts.next() {
|
||||
Some(page) => ExternalOpen::ManPage {
|
||||
section: Some(first),
|
||||
page,
|
||||
},
|
||||
None => ExternalOpen::ManPage {
|
||||
section: None,
|
||||
page: first,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn decode(raw: &str, what: &str) -> Result<String, String> {
|
||||
percent_encoding::percent_decode_str(raw)
|
||||
.decode_utf8()
|
||||
.map(|decoded| decoded.into_owned())
|
||||
.map_err(|_| format!("the URL has a {what} that is not UTF-8"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_as_default_terminal() -> Result<(), String> {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::CFString;
|
||||
|
||||
// LaunchServices is a subframework of CoreServices. Linking the parent is
|
||||
// portable across both the full Xcode SDK and Command Line Tools SDK; the
|
||||
// latter has no standalone `LaunchServices.framework` linker path.
|
||||
#[link(name = "CoreServices", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn LSSetDefaultRoleHandlerForContentType(
|
||||
content_type: core_foundation::string::CFStringRef,
|
||||
role: u32,
|
||||
handler: core_foundation::string::CFStringRef,
|
||||
) -> i32;
|
||||
fn LSSetDefaultHandlerForURLScheme(
|
||||
scheme: core_foundation::string::CFStringRef,
|
||||
handler: core_foundation::string::CFStringRef,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
// This is the conventional macOS definition of "default terminal":
|
||||
// iTerm2 makes the same `public.unix-executable` / `Shell` association. A
|
||||
// folder is a Viewer/Editor item rather than something a terminal executes,
|
||||
// so asking LaunchServices to assign its Shell role is invalid (-50).
|
||||
const ROLE_SHELL: u32 = 0x0000_0008;
|
||||
let handler = CFString::new(BUNDLE_ID);
|
||||
let executable = CFString::new("public.unix-executable");
|
||||
let status = unsafe {
|
||||
LSSetDefaultRoleHandlerForContentType(
|
||||
executable.as_concrete_TypeRef(),
|
||||
ROLE_SHELL,
|
||||
handler.as_concrete_TypeRef(),
|
||||
)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(format!(
|
||||
"could not set the Unix executable handler (LaunchServices status {status})"
|
||||
));
|
||||
}
|
||||
for scheme in URL_SCHEMES {
|
||||
let scheme = CFString::new(scheme);
|
||||
let status = unsafe {
|
||||
LSSetDefaultHandlerForURLScheme(
|
||||
scheme.as_concrete_TypeRef(),
|
||||
handler.as_concrete_TypeRef(),
|
||||
)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(format!(
|
||||
"could not set the {scheme} URL handler (LaunchServices status {status})"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn set_as_default_terminal() -> Result<(), String> {
|
||||
Err("setting a default terminal is only available on macOS".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ExternalOpen, parse_open_url};
|
||||
|
||||
#[test]
|
||||
fn parses_finder_file_urls_and_percent_decodes_paths() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("a folder");
|
||||
std::fs::create_dir(&path).unwrap();
|
||||
assert_eq!(
|
||||
parse_open_url(&url::Url::from_file_path(&path).unwrap().to_string()).unwrap(),
|
||||
ExternalOpen::Folder(path)
|
||||
);
|
||||
}
|
||||
|
||||
fn ssh(raw: &str) -> tty7_core::core::ssh_profile::QuickConnect {
|
||||
let ExternalOpen::Ssh(ssh) = parse_open_url(raw).unwrap() else {
|
||||
panic!("expected SSH request from {raw:?}");
|
||||
};
|
||||
ssh
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ssh_authority_and_port() {
|
||||
let parsed = ssh("ssh://me@example.test:2200");
|
||||
assert_eq!(parsed.user.as_deref(), Some("me"));
|
||||
assert_eq!(parsed.host, "example.test");
|
||||
assert_eq!(parsed.port, Some(2200));
|
||||
}
|
||||
|
||||
/// The authority is read off the parsed URL, so what follows it cannot
|
||||
/// leak into the host or the port the way it does when the raw string is
|
||||
/// re-parsed as a bare `user@host:port`.
|
||||
#[test]
|
||||
fn ssh_paths_and_trailing_slashes_stay_out_of_the_authority() {
|
||||
let trailing = ssh("ssh://me@example.test:2200/");
|
||||
assert_eq!(trailing.host, "example.test");
|
||||
assert_eq!(trailing.port, Some(2200));
|
||||
|
||||
let with_path = ssh("ssh://example.test/srv/app");
|
||||
assert_eq!(with_path.host, "example.test");
|
||||
assert_eq!(with_path.port, None);
|
||||
|
||||
// `Url` lowercases the scheme, so the arm fires whatever case the
|
||||
// link was written in.
|
||||
assert_eq!(ssh("SSH://example.test").host, "example.test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_decodes_the_user_and_unwraps_ipv6() {
|
||||
assert_eq!(
|
||||
ssh("ssh://user%40corp@example.test").user.as_deref(),
|
||||
Some("user@corp")
|
||||
);
|
||||
let numeric = ssh("ssh://[fe80::1]:2200");
|
||||
assert_eq!(numeric.host, "fe80::1");
|
||||
assert_eq!(numeric.port, Some(2200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_without_a_host_is_rejected() {
|
||||
assert!(parse_open_url("ssh://").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_man_page_host_or_path() {
|
||||
assert_eq!(
|
||||
parse_open_url("x-man-page://printf").unwrap(),
|
||||
ExternalOpen::ManPage {
|
||||
section: None,
|
||||
page: "printf".into()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_open_url("x-man-page:/ls").unwrap(),
|
||||
ExternalOpen::ManPage {
|
||||
section: None,
|
||||
page: "ls".into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Apple's sectioned form puts the section in the authority. Reading the
|
||||
/// host as the page name ran `man 3` and asked what page was wanted.
|
||||
#[test]
|
||||
fn a_sectioned_man_page_keeps_its_page_name() {
|
||||
assert_eq!(
|
||||
parse_open_url("x-man-page://3/printf").unwrap(),
|
||||
ExternalOpen::ManPage {
|
||||
section: Some("3".into()),
|
||||
page: "printf".into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_man_page_url_with_no_name_is_rejected() {
|
||||
assert!(parse_open_url("x-man-page://").is_err());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod agent_prompt;
|
||||
pub mod aumid;
|
||||
pub mod cli_install;
|
||||
pub mod config;
|
||||
pub mod default_terminal;
|
||||
pub mod explorer_context_menu;
|
||||
pub mod keychain;
|
||||
pub mod rate_meter;
|
||||
|
||||
+89
-33
@@ -441,6 +441,47 @@ fn set_dock_icon_for_bare_binary() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delivers Finder document opens and LaunchServices URL opens after gpui has
|
||||
/// created the application. The native callback queues requests; UI state is
|
||||
/// then changed on gpui's application loop.
|
||||
fn handle_external_opens(urls: Vec<String>, cx: &mut App) {
|
||||
use crate::core::default_terminal::{ExternalOpen, parse_open_url};
|
||||
|
||||
for raw in urls {
|
||||
let request = match parse_open_url(&raw) {
|
||||
Ok(request) => request,
|
||||
Err(error) => {
|
||||
log::warn!("ignored external open request {raw:?}: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match request {
|
||||
ExternalOpen::Folder(path) => crate::ui::windows::open_from_cli(cx, Some(path)),
|
||||
ExternalOpen::Runnable(path) => {
|
||||
let Some(parent) = path.parent().map(std::path::Path::to_path_buf) else {
|
||||
log::warn!(
|
||||
"ignored runnable without a parent directory: {}",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let command =
|
||||
crate::core::shell_quote::quote_for_shell(&path.to_string_lossy(), None);
|
||||
crate::ui::windows::run_local_command(cx, parent, command);
|
||||
}
|
||||
ExternalOpen::Ssh(ssh) => crate::ui::windows::quick_connect_from_url(cx, ssh),
|
||||
ExternalOpen::ManPage { section, page } => {
|
||||
let mut command = String::from("man");
|
||||
for argument in section.iter().chain(std::iter::once(&page)) {
|
||||
command.push(' ');
|
||||
command.push_str(&crate::core::shell_quote::quote_for_shell(argument, None));
|
||||
}
|
||||
crate::ui::windows::run_local_command(cx, std::env::temp_dir(), command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
|
||||
{
|
||||
@@ -583,46 +624,61 @@ fn main() {
|
||||
log::error!("failed to ensure daemon is running: {e}");
|
||||
}
|
||||
|
||||
gpui_platform::application()
|
||||
// `Application`, not the in-loop `App`, owns the native delegate. Register
|
||||
// before `run` so macOS can deliver Finder and URL events from launch.
|
||||
let (external_open_tx, external_open_rx) = smol::channel::unbounded();
|
||||
let application = gpui_platform::application()
|
||||
.with_assets(Assets)
|
||||
// The window-close path decides whether this process survives: with
|
||||
// the tray icon on, closing the last window retires to the tray, and
|
||||
// without it the close handler quits explicitly. The platform default
|
||||
// would quit on the last window unconditionally, which is exactly the
|
||||
// orphaned-daemon trap the tray is meant to prevent.
|
||||
.with_quit_mode(QuitMode::Explicit)
|
||||
.run(move |cx| {
|
||||
gpui_component::init(cx);
|
||||
register_bundled_fonts(cx);
|
||||
cx.activate(true);
|
||||
#[cfg(target_os = "macos")]
|
||||
set_dock_icon_for_bare_binary();
|
||||
crate::ui::i18n::set_locale(&gui_language);
|
||||
// The load above is reused rather than re-read: reading the same
|
||||
// file twice at launch would report the same failure twice.
|
||||
cx.set_global(config);
|
||||
crate::ui::theme::refresh_system_appearance(cx);
|
||||
crate::core::session::WorkspaceStore::init(cx);
|
||||
crate::ui::windows::WindowRegistry::init(cx);
|
||||
crate::ui::presets::load_registry(cx);
|
||||
crate::ui::theme::apply_cursor_hide_mode(cx);
|
||||
spawn_config_watcher(cx);
|
||||
crate::core::update::spawn_check(cx);
|
||||
cx.background_executor()
|
||||
.spawn(async {
|
||||
crate::core::agent_hooks::refresh_hooks_at_launch();
|
||||
})
|
||||
.detach();
|
||||
keymap::init(cx);
|
||||
crate::ui::local_link::LocalLink::install(cx);
|
||||
|
||||
let reopen = crate::ui::windows::restore_target(cx, open_path.as_deref());
|
||||
crate::ui::windows::open_at(cx, reopen.map(|(id, _)| id), open_path);
|
||||
crate::ui::windows::announce_detached_at_launch(cx, reopen);
|
||||
if config_outcome.failed() {
|
||||
notify_config_load_failed(cx, config_outcome, true);
|
||||
.with_quit_mode(QuitMode::Explicit);
|
||||
application.on_open_urls(move |urls| {
|
||||
let _ = external_open_tx.try_send(urls);
|
||||
});
|
||||
application.run(move |cx| {
|
||||
// gpui invokes this callback without an `App` context. Bridge it
|
||||
// back onto the application loop instead of touching UI state on
|
||||
// AppKit's delegate call stack.
|
||||
cx.spawn(async move |cx| {
|
||||
while let Ok(urls) = external_open_rx.recv().await {
|
||||
let _ = cx.update(|cx| handle_external_opens(urls, cx));
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
gpui_component::init(cx);
|
||||
register_bundled_fonts(cx);
|
||||
cx.activate(true);
|
||||
#[cfg(target_os = "macos")]
|
||||
set_dock_icon_for_bare_binary();
|
||||
crate::ui::i18n::set_locale(&gui_language);
|
||||
// The load above is reused rather than re-read: reading the same
|
||||
// file twice at launch would report the same failure twice.
|
||||
cx.set_global(config);
|
||||
crate::ui::theme::refresh_system_appearance(cx);
|
||||
crate::core::session::WorkspaceStore::init(cx);
|
||||
crate::ui::windows::WindowRegistry::init(cx);
|
||||
crate::ui::presets::load_registry(cx);
|
||||
crate::ui::theme::apply_cursor_hide_mode(cx);
|
||||
spawn_config_watcher(cx);
|
||||
crate::core::update::spawn_check(cx);
|
||||
cx.background_executor()
|
||||
.spawn(async {
|
||||
crate::core::agent_hooks::refresh_hooks_at_launch();
|
||||
})
|
||||
.detach();
|
||||
keymap::init(cx);
|
||||
crate::ui::local_link::LocalLink::install(cx);
|
||||
|
||||
let reopen = crate::ui::windows::restore_target(cx, open_path.as_deref());
|
||||
crate::ui::windows::open_at(cx, reopen.map(|(id, _)| id), open_path);
|
||||
crate::ui::windows::announce_detached_at_launch(cx, reopen);
|
||||
if config_outcome.failed() {
|
||||
notify_config_load_failed(cx, config_outcome, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The watcher tick, from a reloaded file to the keys the app dispatches on.
|
||||
|
||||
@@ -3552,6 +3552,41 @@ impl Tty7App {
|
||||
self.new_tab_with_cwd(Some(cwd), None, window, cx);
|
||||
}
|
||||
|
||||
/// Opens a new local shell in `cwd`, then types a command only after the
|
||||
/// pane exists. LaunchServices uses this for a script/executable selected
|
||||
/// in Finder and for `x-man-page:` requests.
|
||||
///
|
||||
/// A tab that did not open takes the command with it. `new_tab_with_cwd`
|
||||
/// returns early when the spawn fails or the workspace cannot host a local
|
||||
/// shell, and writing anyway would type the command into whatever pane was
|
||||
/// focused before — a shell the user is mid-line in, or an agent.
|
||||
pub(crate) fn new_tab_running(
|
||||
&mut self,
|
||||
cwd: std::path::PathBuf,
|
||||
command: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let before = self.tabs.len();
|
||||
self.new_tab_with_cwd(Some(cwd), None, window, cx);
|
||||
if self.tabs.len() == before {
|
||||
log::warn!("no tab opened for {command:?}; not writing it to another pane");
|
||||
return;
|
||||
}
|
||||
if let Some(terminal) = self.focused_leaf(window, cx) {
|
||||
terminal.read(cx).run_command_line(&command);
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the terminal that a newly-created window already opened. This keeps
|
||||
/// a cold LaunchServices request to one tab rather than creating the
|
||||
/// window's default shell and then a second shell for the requested item.
|
||||
pub(crate) fn run_in_active_terminal(&self, command: &str, window: &Window, cx: &App) {
|
||||
if let Some(terminal) = self.focused_leaf(window, cx) {
|
||||
terminal.read(cx).run_command_line(command);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_tab_with_shell(
|
||||
&mut self,
|
||||
shell: Option<ShellSpec>,
|
||||
|
||||
@@ -635,6 +635,17 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsAboutDesc1 => {
|
||||
"A terminal workbench: persistent sessions, remote work, agents."
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminal => "Default terminal",
|
||||
L10nKey::SettingsDefaultTerminalDesc => {
|
||||
"Make tty7 the macOS default terminal for Unix executables, SSH links, and man-page links. tty7 can also open folders and scripts, but does not replace Finder's folder handler. Apps that choose their own terminal may ignore this setting."
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSet => "Set as Default Terminal",
|
||||
L10nKey::SettingsDefaultTerminalSetSuccess => {
|
||||
"tty7 is now the default handler for supported terminal files and links."
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSetFailed => {
|
||||
"Could not set tty7 as the default terminal: {error}"
|
||||
}
|
||||
L10nKey::SettingsVersion => "Version",
|
||||
L10nKey::SettingsUpdates => "Updates",
|
||||
L10nKey::SettingsUpdateAndRelaunch => "Update and relaunch",
|
||||
|
||||
@@ -642,6 +642,17 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAboutDesc1 => {
|
||||
"ターミナルワークベンチ: 常駐セッション、リモート作業、エージェント"
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminal => "デフォルトのターミナル",
|
||||
L10nKey::SettingsDefaultTerminalDesc => {
|
||||
"tty7 を Unix 実行ファイル、SSH リンク、man ページリンク用の macOS のデフォルトターミナルにします。tty7 はフォルダとスクリプトも開けますが、Finder のフォルダハンドラは置き換えません。独自のターミナルを指定するアプリはこの設定を無視することがあります。"
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSet => "デフォルトのターミナルに設定",
|
||||
L10nKey::SettingsDefaultTerminalSetSuccess => {
|
||||
"tty7 を対応するターミナルファイルとリンクのデフォルトハンドラに設定しました。"
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSetFailed => {
|
||||
"tty7 をデフォルトのターミナルに設定できませんでした: {error}"
|
||||
}
|
||||
L10nKey::SettingsVersion => "バージョン",
|
||||
L10nKey::SettingsUpdates => "アップデート",
|
||||
L10nKey::SettingsUpdateAndRelaunch => "更新して再起動",
|
||||
|
||||
@@ -508,6 +508,11 @@ l10n_keys! {
|
||||
KeybindForkSessionDown,
|
||||
KeybindForkSessionUp,
|
||||
SettingsAboutDesc1,
|
||||
SettingsDefaultTerminal,
|
||||
SettingsDefaultTerminalDesc,
|
||||
SettingsDefaultTerminalSet,
|
||||
SettingsDefaultTerminalSetSuccess,
|
||||
SettingsDefaultTerminalSetFailed,
|
||||
SettingsVersion,
|
||||
SettingsUpdates,
|
||||
SettingsUpdateAndRelaunch,
|
||||
|
||||
@@ -563,6 +563,15 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::KeybindForkSessionDown => "向下 Fork 会话",
|
||||
L10nKey::KeybindForkSessionUp => "向上 Fork 会话",
|
||||
L10nKey::SettingsAboutDesc1 => "终端工作台:常驻会话、远程工作、agent。",
|
||||
L10nKey::SettingsDefaultTerminal => "默认终端",
|
||||
L10nKey::SettingsDefaultTerminalDesc => {
|
||||
"将 tty7 设为 Unix 可执行文件、SSH 链接和 man 页面链接的 macOS 默认终端。tty7 仍可打开文件夹和脚本,但不会替换 Finder 的文件夹处理程序。自行指定终端的应用可能不会遵循此设置。"
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSet => "设为默认终端",
|
||||
L10nKey::SettingsDefaultTerminalSetSuccess => {
|
||||
"tty7 已成为受支持终端文件和链接的默认处理程序。"
|
||||
}
|
||||
L10nKey::SettingsDefaultTerminalSetFailed => "无法将 tty7 设为默认终端:{error}",
|
||||
L10nKey::SettingsVersion => "版本",
|
||||
L10nKey::SettingsUpdates => "更新",
|
||||
L10nKey::SettingsUpdateAndRelaunch => "更新并重新启动",
|
||||
|
||||
@@ -7033,6 +7033,40 @@ impl Tty7App {
|
||||
.text_color(muted_fg)
|
||||
.child(t(L10nKey::SettingsAboutDesc1)),
|
||||
)
|
||||
.when(cfg!(target_os = "macos"), |this| {
|
||||
this.child(self.section_rule(cx)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(foreground)
|
||||
.child(t(L10nKey::SettingsDefaultTerminal)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted_fg)
|
||||
.child(t(L10nKey::SettingsDefaultTerminalDesc)),
|
||||
)
|
||||
.child(
|
||||
Button::new("set-default-terminal")
|
||||
.label(t(L10nKey::SettingsDefaultTerminalSet))
|
||||
.small()
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
let message = match crate::core::default_terminal::set_as_default_terminal() {
|
||||
Ok(()) => t(L10nKey::SettingsDefaultTerminalSetSuccess).to_string(),
|
||||
Err(error) => t_fmt(
|
||||
L10nKey::SettingsDefaultTerminalSetFailed,
|
||||
&[("error", &error)],
|
||||
),
|
||||
};
|
||||
window.push_notification(message, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(self.section_rule(cx))
|
||||
.child(self.section_header(t(L10nKey::SettingsUpdates), cx))
|
||||
.child(
|
||||
|
||||
+158
-25
@@ -896,14 +896,19 @@ struct WsState {
|
||||
/// Leaving it standing after the run ends is what makes a *first* failure
|
||||
/// wait the cap: the count would still be carrying an outage that is over.
|
||||
rehydrate_attempts: u32,
|
||||
/// A folder the launch asked for, waiting for this window's layout.
|
||||
/// Tabs a launch or a LaunchServices open asked for, waiting for this
|
||||
/// window's layout.
|
||||
///
|
||||
/// Held here rather than opened straight away because a window with a tab
|
||||
/// in it is one `Adopt::IfEmpty` will not adopt into: the pull would land,
|
||||
/// decline the layout, and push the single tab back as the whole workspace.
|
||||
/// Parking it also means a pull that has to be retried still gets the
|
||||
/// folder opened, on whichever attempt finally lands.
|
||||
then_open: Option<std::path::PathBuf>,
|
||||
///
|
||||
/// A list because more than one can be waiting: a launch parks the folder
|
||||
/// it was given, and the `x-man-page:` or script Finder sent to the same
|
||||
/// cold start parks its own tab behind it.
|
||||
then_open: Vec<ParkedOpen>,
|
||||
/// A name the user typed for a workspace this window is about to create.
|
||||
///
|
||||
/// It has to travel with the create rather than follow it as a rename: the
|
||||
@@ -938,7 +943,7 @@ impl Default for WsState {
|
||||
owed_over: Vec::new(),
|
||||
not_rebuilt: Vec::new(),
|
||||
rehydrate_attempts: 0,
|
||||
then_open: None,
|
||||
then_open: Vec::new(),
|
||||
chosen_name: None,
|
||||
said_why_empty: false,
|
||||
}
|
||||
@@ -1606,21 +1611,98 @@ pub(crate) fn hydrate_window_then_open(
|
||||
.windows
|
||||
.entry(client_ws)
|
||||
.or_default()
|
||||
.then_open = Some(path);
|
||||
.then_open
|
||||
.push(ParkedOpen::Folder(path));
|
||||
hydrate(cx, client_ws, Adopt::IfEmpty);
|
||||
}
|
||||
|
||||
/// Opens the folder a launch parked here, now that the layout it waited for is
|
||||
/// Parks a tab request while this window's layout is still on its way, and
|
||||
/// says whether it did. A caller told `false` has a settled window and should
|
||||
/// open the tab itself; one told `true` has handed the request to the pull,
|
||||
/// which opens it once the layout is up.
|
||||
///
|
||||
/// Inserting into a window mid-pull is the failure `then_open` exists to
|
||||
/// avoid: `Adopt::IfEmpty` declines a window that has a tab, and pushes that
|
||||
/// one tab back as the whole workspace.
|
||||
pub(crate) fn park_command_while_pulling(
|
||||
cx: &mut App,
|
||||
client_ws: WorkspaceId,
|
||||
cwd: &std::path::Path,
|
||||
command: &str,
|
||||
) -> bool {
|
||||
if !layout_is_pending(cx, client_ws) {
|
||||
return false;
|
||||
}
|
||||
let parked = parked_for(cx, client_ws);
|
||||
// Opening a window for this request parks its folder on the way past, in
|
||||
// `for_workspace_at`. The command belongs in that tab, not in a second one
|
||||
// beside it running the same shell.
|
||||
match parked
|
||||
.iter_mut()
|
||||
.find(|open| matches!(open, ParkedOpen::Folder(folder) if folder == cwd))
|
||||
{
|
||||
Some(open) => {
|
||||
*open = ParkedOpen::Command {
|
||||
cwd: cwd.to_path_buf(),
|
||||
command: command.to_owned(),
|
||||
}
|
||||
}
|
||||
None => parked.push(ParkedOpen::Command {
|
||||
cwd: cwd.to_path_buf(),
|
||||
command: command.to_owned(),
|
||||
}),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// [`park_command_while_pulling`] for an `ssh://` link, which opens a tab of
|
||||
/// its own and so races the same pull.
|
||||
pub(crate) fn park_ssh_while_pulling(
|
||||
cx: &mut App,
|
||||
client_ws: WorkspaceId,
|
||||
ssh: &tty7_core::core::ssh_profile::QuickConnect,
|
||||
) -> bool {
|
||||
if !layout_is_pending(cx, client_ws) {
|
||||
return false;
|
||||
}
|
||||
parked_for(cx, client_ws).push(ParkedOpen::Ssh(ssh.clone()));
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether this window's layout is still on its way: a pull is out, one is
|
||||
/// owed a retry, or tabs are already waiting on one.
|
||||
fn layout_is_pending(cx: &mut App, client_ws: WorkspaceId) -> bool {
|
||||
cx.default_global::<TreeSync>()
|
||||
.windows
|
||||
.get(&client_ws)
|
||||
.is_some_and(|state| {
|
||||
matches!(state.sync, SyncPhase::Unprimed { priming: true, .. })
|
||||
|| state.rehydrate.is_some()
|
||||
|| !state.then_open.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
fn parked_for(cx: &mut App, client_ws: WorkspaceId) -> &mut Vec<ParkedOpen> {
|
||||
&mut cx
|
||||
.default_global::<TreeSync>()
|
||||
.windows
|
||||
.entry(client_ws)
|
||||
.or_default()
|
||||
.then_open
|
||||
}
|
||||
|
||||
/// Opens the tabs a launch parked here, now that the layout they waited for is
|
||||
/// up. Does nothing for the windows — every other one — that parked nothing.
|
||||
fn open_parked_path(cx: &mut App, client_ws: WorkspaceId) {
|
||||
let Some(path) = cx
|
||||
let parked = cx
|
||||
.default_global::<TreeSync>()
|
||||
.windows
|
||||
.get_mut(&client_ws)
|
||||
.and_then(|state| state.then_open.take())
|
||||
else {
|
||||
.map(|state| std::mem::take(&mut state.then_open))
|
||||
.unwrap_or_default();
|
||||
if parked.is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let Some(handle) = crate::ui::windows::WindowRegistry::window_for(cx, client_ws) else {
|
||||
return;
|
||||
};
|
||||
@@ -1630,10 +1712,34 @@ fn open_parked_path(cx: &mut App, client_ws: WorkspaceId) {
|
||||
return;
|
||||
};
|
||||
let _ = handle.update(cx, move |_, window, cx| {
|
||||
app.update(cx, |app, cx| app.new_tab_at(path, window, cx));
|
||||
app.update(cx, |app, cx| {
|
||||
for open in parked {
|
||||
match open {
|
||||
ParkedOpen::Folder(cwd) => app.new_tab_at(cwd, window, cx),
|
||||
ParkedOpen::Command { cwd, command } => {
|
||||
app.new_tab_running(cwd, command, window, cx)
|
||||
}
|
||||
ParkedOpen::Ssh(ssh) => app.quick_connect(ssh, window, cx),
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// A tab parked until its window's layout lands.
|
||||
enum ParkedOpen {
|
||||
/// What a launch and an Explorer double-click ask for.
|
||||
Folder(std::path::PathBuf),
|
||||
/// A shell in `cwd` with `command` typed into it, from a script or an
|
||||
/// `x-man-page:` link Finder handed over.
|
||||
Command {
|
||||
cwd: std::path::PathBuf,
|
||||
command: String,
|
||||
},
|
||||
/// An `ssh://` link.
|
||||
Ssh(tty7_core::core::ssh_profile::QuickConnect),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Adopt {
|
||||
IfEmpty,
|
||||
@@ -2930,32 +3036,59 @@ mod tests {
|
||||
);
|
||||
crate::ui::windows::WindowRegistry::init(cx);
|
||||
|
||||
hydrate_window_then_open(cx, ws, path.clone());
|
||||
assert_eq!(
|
||||
let parked = |cx: &mut gpui::App| -> Vec<std::path::PathBuf> {
|
||||
cx.default_global::<TreeSync>().windows[&ws]
|
||||
.then_open
|
||||
.as_ref(),
|
||||
Some(&path),
|
||||
.iter()
|
||||
.map(|open| match open {
|
||||
ParkedOpen::Folder(cwd) => cwd.clone(),
|
||||
ParkedOpen::Command { cwd, .. } => cwd.clone(),
|
||||
ParkedOpen::Ssh(_) => std::path::PathBuf::from("<ssh>"),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
hydrate_window_then_open(cx, ws, path.clone());
|
||||
assert_eq!(
|
||||
parked(cx),
|
||||
vec![path.clone()],
|
||||
"the request must be parked, not opened over a layout still in flight"
|
||||
);
|
||||
|
||||
hydrate_with(cx, ws, Adopt::IfEmpty, Vec::new());
|
||||
assert_eq!(
|
||||
cx.default_global::<TreeSync>().windows[&ws]
|
||||
.then_open
|
||||
.as_ref(),
|
||||
Some(&path),
|
||||
parked(cx),
|
||||
vec![path.clone()],
|
||||
"a retry must still owe the folder"
|
||||
);
|
||||
|
||||
// With no window to put it in there is nothing to open, and the
|
||||
// request must not survive to surface in some unrelated window.
|
||||
// The command for the folder this window was opened for lands in
|
||||
// that parked tab rather than adding a second one beside it.
|
||||
assert!(park_command_while_pulling(cx, ws, &path, "./build.sh"));
|
||||
assert_eq!(parked(cx), vec![path.clone()]);
|
||||
assert!(matches!(
|
||||
&cx.default_global::<TreeSync>().windows[&ws].then_open[0],
|
||||
ParkedOpen::Command { command, .. } if command == "./build.sh"
|
||||
));
|
||||
|
||||
// A LaunchServices open for somewhere else queues behind it rather
|
||||
// than replacing it.
|
||||
let script = std::path::PathBuf::from("/tmp/from-finder");
|
||||
assert!(park_command_while_pulling(cx, ws, &script, "./deploy.sh"));
|
||||
assert_eq!(parked(cx), vec![path.clone(), script]);
|
||||
|
||||
// With no window to put them in there is nothing to open, and the
|
||||
// requests must not survive to surface in some unrelated window.
|
||||
open_parked_path(cx, ws);
|
||||
assert!(
|
||||
cx.default_global::<TreeSync>().windows[&ws]
|
||||
.then_open
|
||||
.is_none()
|
||||
);
|
||||
assert!(parked(cx).is_empty());
|
||||
|
||||
// A window whose layout has settled is opened into directly.
|
||||
cx.default_global::<TreeSync>()
|
||||
.windows
|
||||
.get_mut(&ws)
|
||||
.unwrap()
|
||||
.sync = SyncPhase::Primed(WsMirror::default());
|
||||
assert!(!park_command_while_pulling(cx, ws, &path, "./build.sh"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -306,6 +306,115 @@ pub fn open_from_cli(cx: &mut App, path: Option<std::path::PathBuf>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs a local command in a new tab of the most recently active local
|
||||
/// workspace, restoring or creating one if every open window is remote or
|
||||
/// absent. Takes the same route as [`open_from_cli`], for the same reason: a
|
||||
/// window whose layout is still being pulled has to be opened into by the
|
||||
/// pull, not underneath it.
|
||||
pub fn run_local_command(cx: &mut App, cwd: std::path::PathBuf, command: String) {
|
||||
let Some(workspace) = WindowRegistry::most_recent_local(cx) else {
|
||||
open_missing_cli_window_with(cx, Some(cwd), |cx, restore, cwd| {
|
||||
let Some(cwd) = cwd else { return };
|
||||
open_at(cx, restore, Some(cwd.clone()));
|
||||
let Some(workspace) = WindowRegistry::most_recent_local(cx) else {
|
||||
return;
|
||||
};
|
||||
// A window that is pulling its layout parked the folder itself, in
|
||||
// `for_workspace_at`; the command has to travel with it. One that
|
||||
// is not opened its first terminal in `cwd` already, so the command
|
||||
// goes straight there rather than into a second tab.
|
||||
if crate::ui::tree_sync::park_command_while_pulling(cx, workspace, &cwd, &command) {
|
||||
activate(cx, workspace);
|
||||
} else {
|
||||
run_command_in_active_terminal(cx, workspace, command);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
if crate::ui::tree_sync::park_command_while_pulling(cx, workspace, &cwd, &command) {
|
||||
activate(cx, workspace);
|
||||
return;
|
||||
}
|
||||
run_local_command_in(cx, workspace, cwd, command);
|
||||
}
|
||||
|
||||
fn activate(cx: &mut App, workspace: WorkspaceId) {
|
||||
let Some(handle) = WindowRegistry::window_for(cx, workspace) else {
|
||||
return;
|
||||
};
|
||||
cx.activate(true);
|
||||
let _ = handle.update(cx, |_, window, _| window.activate_window());
|
||||
}
|
||||
|
||||
fn run_command_in_active_terminal(cx: &mut App, workspace: WorkspaceId, command: String) {
|
||||
let Some(handle) = WindowRegistry::window_for(cx, workspace) else {
|
||||
return;
|
||||
};
|
||||
let Some(app) = WindowRegistry::app_for(cx, workspace).and_then(|app| app.upgrade()) else {
|
||||
return;
|
||||
};
|
||||
cx.activate(true);
|
||||
let _ = handle.update(cx, move |_, window, cx| {
|
||||
app.update(cx, |app, cx| {
|
||||
app.run_in_active_terminal(&command, window, cx)
|
||||
});
|
||||
window.activate_window();
|
||||
});
|
||||
}
|
||||
|
||||
fn run_local_command_in(
|
||||
cx: &mut App,
|
||||
workspace: WorkspaceId,
|
||||
cwd: std::path::PathBuf,
|
||||
command: String,
|
||||
) {
|
||||
let Some(handle) = WindowRegistry::window_for(cx, workspace) else {
|
||||
return;
|
||||
};
|
||||
let Some(app) = WindowRegistry::app_for(cx, workspace).and_then(|app| app.upgrade()) else {
|
||||
return;
|
||||
};
|
||||
cx.activate(true);
|
||||
let _ = handle.update(cx, move |_, window, cx| {
|
||||
app.update(cx, |app, cx| app.new_tab_running(cwd, command, window, cx));
|
||||
window.activate_window();
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens an `ssh://` link in the most recently active window, restoring or
|
||||
/// creating one if none is up. Goes through the same restore as every other
|
||||
/// windowless entry point, so a tray-resident tty7 comes back to the workspace
|
||||
/// it retired with instead of claiming a fresh one.
|
||||
pub fn quick_connect_from_url(cx: &mut App, ssh: tty7_core::core::ssh_profile::QuickConnect) {
|
||||
// An SSH link opens a tab, which any window can hold.
|
||||
let workspace = WindowRegistry::most_recent_local(cx)
|
||||
.or_else(|| WindowRegistry::most_recent(cx))
|
||||
.or_else(|| {
|
||||
open_missing_cli_window_with(cx, None, open_at);
|
||||
WindowRegistry::most_recent(cx)
|
||||
});
|
||||
let Some(workspace) = workspace else {
|
||||
return;
|
||||
};
|
||||
// A window still pulling its layout would take this tab as the whole
|
||||
// workspace and push it back over what it is about to restore.
|
||||
if crate::ui::tree_sync::park_ssh_while_pulling(cx, workspace, &ssh) {
|
||||
activate(cx, workspace);
|
||||
return;
|
||||
}
|
||||
let Some(handle) = WindowRegistry::window_for(cx, workspace) else {
|
||||
return;
|
||||
};
|
||||
let Some(app) = WindowRegistry::app_for(cx, workspace).and_then(|app| app.upgrade()) else {
|
||||
return;
|
||||
};
|
||||
cx.activate(true);
|
||||
let _ = handle.update(cx, move |_, window, cx| {
|
||||
app.update(cx, |app, cx| app.quick_connect(ssh, window, cx));
|
||||
window.activate_window();
|
||||
});
|
||||
}
|
||||
|
||||
/// What a launch reopens: the workspace, and how many other open windows the
|
||||
/// restore left detached. Their panes are still running — the count exists so
|
||||
/// the launch can say so instead of letting them be forgotten (#597).
|
||||
|
||||
Reference in New Issue
Block a user