Add current page download pipeline

This commit is contained in:
2026-05-09 12:54:24 -04:00
parent 1faa7423c1
commit 189b9b8b89
17 changed files with 842 additions and 13 deletions
+2
View File
@@ -22,6 +22,8 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
ureq.workspace = true
url.workspace = true
[build-dependencies]
toml.workspace = true
+4 -1
View File
@@ -26,6 +26,7 @@ actions!(
ely_app,
[
CloseCurrentTab,
DownloadCurrentPage,
FocusAddressBar,
FocusCommandMode,
OpenDownloads,
@@ -81,6 +82,9 @@ fn main() {
MenuItem::action("Command Mode", FocusCommandMode),
MenuItem::action("Toggle Sidebar", ToggleSidebar),
MenuItem::separator(),
MenuItem::action("Download Current Page", DownloadCurrentPage),
MenuItem::action("Open Downloads", OpenDownloads),
MenuItem::separator(),
MenuItem::action("Zoom In", ZoomIn),
MenuItem::action("Zoom Out", ZoomOut),
MenuItem::action("Reset Zoom", ResetZoom),
@@ -92,7 +96,6 @@ fn main() {
MenuItem::action("Next Space", SelectNextSpace),
MenuItem::action("Previous Space", SelectPreviousSpace),
MenuItem::separator(),
MenuItem::action("Open Downloads", OpenDownloads),
MenuItem::action("Open History", OpenHistory),
MenuItem::action("Open Task Manager", OpenTaskManager),
MenuItem::action("Open Settings", OpenSettings),
@@ -0,0 +1,370 @@
use std::{
fs::{self, File, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
time::Duration,
};
use ely_domain::UrlText;
use thiserror::Error;
use url::Url;
const DOWNLOAD_BUFFER_BYTES: usize = 64 * 1024;
const DOWNLOAD_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
const DOWNLOAD_READ_TIMEOUT: Duration = Duration::from_secs(120);
const USER_AGENT: &str = concat!("ELY Browser/", env!("CARGO_PKG_VERSION"));
#[derive(Clone, Debug)]
pub struct HttpDownloadPlan {
url: UrlText,
target_path: PathBuf,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpDownloadResult {
target_path: PathBuf,
received_bytes: u64,
total_bytes: Option<u64>,
}
#[derive(Debug, Error)]
pub enum HttpDownloadError {
#[error("download URL is invalid: {url}")]
InvalidUrl { url: String },
#[error("unsupported download URL scheme: {scheme}")]
UnsupportedScheme { scheme: String },
#[error("download target has no parent directory: {path}")]
MissingTargetDirectory { path: PathBuf },
#[error("failed to create download directory: {path}: {source}")]
DirectoryUnavailable {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to create download file: {path}: {source}")]
CreateFile {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("download returned HTTP {status} for {url}")]
HttpStatus { url: String, status: u16 },
#[error("download request failed for {url}: {source}")]
Request {
url: String,
#[source]
source: Box<ureq::Error>,
},
#[error("failed to read download stream for {url}: {source}")]
Read {
url: String,
#[source]
source: io::Error,
},
#[error("failed to write download file: {path}: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to finalize download file: {from} -> {to}: {source}")]
FinalizeFile {
from: PathBuf,
to: PathBuf,
#[source]
source: io::Error,
},
#[error("download ended at {received_bytes} bytes, expected {total_bytes} bytes for {url}")]
ContentLengthMismatch { url: String, received_bytes: u64, total_bytes: u64 },
}
impl HttpDownloadPlan {
#[must_use]
pub fn new(url: UrlText, target_path: PathBuf) -> Self {
Self { url, target_path }
}
#[must_use]
pub fn target_path(&self) -> &Path {
&self.target_path
}
}
impl HttpDownloadResult {
#[must_use]
pub fn received_bytes(&self) -> u64 {
self.received_bytes
}
}
pub fn download_to_file(plan: HttpDownloadPlan) -> Result<HttpDownloadResult, HttpDownloadError> {
validate_download_url(&plan.url)?;
prepare_target_directory(plan.target_path())?;
let response = call_download_url(&plan.url)?;
let total_bytes = content_length(&response);
let mut reader = response.into_reader();
let (temporary_path, mut file) = create_temporary_file(plan.target_path())?;
let stream_result = stream_response_body(&plan.url, &temporary_path, &mut reader, &mut file);
let received_bytes = match stream_result {
Ok(received_bytes) => received_bytes,
Err(error) => {
_ = fs::remove_file(&temporary_path);
return Err(error);
}
};
if let Err(error) = validate_content_length(plan.url.as_str(), received_bytes, total_bytes) {
_ = fs::remove_file(&temporary_path);
return Err(error);
}
fs::rename(&temporary_path, plan.target_path()).map_err(|source| {
HttpDownloadError::FinalizeFile {
from: temporary_path,
to: plan.target_path.clone(),
source,
}
})?;
Ok(HttpDownloadResult { target_path: plan.target_path, received_bytes, total_bytes })
}
fn stream_response_body(
url: &UrlText,
target_path: &Path,
reader: &mut dyn Read,
file: &mut File,
) -> Result<u64, HttpDownloadError> {
let mut buffer = [0; DOWNLOAD_BUFFER_BYTES];
let mut received_bytes = 0;
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(|source| HttpDownloadError::Read { url: url.as_str().to_string(), source })?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read]).map_err(|source| HttpDownloadError::Write {
path: target_path.to_path_buf(),
source,
})?;
received_bytes += bytes_read as u64;
}
file.flush()
.map_err(|source| HttpDownloadError::Write { path: target_path.to_path_buf(), source })?;
Ok(received_bytes)
}
fn call_download_url(url: &UrlText) -> Result<ureq::Response, HttpDownloadError> {
let agent = ureq::AgentBuilder::new()
.timeout_connect(DOWNLOAD_CONNECT_TIMEOUT)
.timeout_read(DOWNLOAD_READ_TIMEOUT)
.build();
match agent
.get(url.as_str())
.set("User-Agent", USER_AGENT)
.set("Accept-Encoding", "identity")
.call()
{
Ok(response) => Ok(response),
Err(ureq::Error::Status(status, _)) => {
Err(HttpDownloadError::HttpStatus { url: url.as_str().to_string(), status })
}
Err(source) => Err(HttpDownloadError::Request {
url: url.as_str().to_string(),
source: Box::new(source),
}),
}
}
fn validate_download_url(url: &UrlText) -> Result<(), HttpDownloadError> {
let parsed = Url::parse(url.as_str())
.map_err(|_| HttpDownloadError::InvalidUrl { url: url.as_str().to_string() })?;
match parsed.scheme() {
"http" | "https" => Ok(()),
scheme => Err(HttpDownloadError::UnsupportedScheme { scheme: scheme.to_string() }),
}
}
fn prepare_target_directory(target_path: &Path) -> Result<(), HttpDownloadError> {
let Some(directory) = target_path.parent() else {
return Err(HttpDownloadError::MissingTargetDirectory { path: target_path.to_path_buf() });
};
fs::create_dir_all(directory).map_err(|source| HttpDownloadError::DirectoryUnavailable {
path: directory.to_path_buf(),
source,
})
}
fn create_temporary_file(target_path: &Path) -> Result<(PathBuf, File), HttpDownloadError> {
let file_name =
target_path.file_name().map(|value| value.to_string_lossy()).unwrap_or_default();
let prefix = if file_name.is_empty() { "download".into() } else { file_name };
for attempt in 0..1_000 {
let temporary_name = if attempt == 0 {
format!(".{prefix}.elydownload")
} else {
format!(".{prefix}.elydownload.{attempt}")
};
let temporary_path = target_path.with_file_name(temporary_name);
match OpenOptions::new().create_new(true).write(true).open(&temporary_path) {
Ok(file) => return Ok((temporary_path, file)),
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
Err(source) => {
return Err(HttpDownloadError::CreateFile { path: temporary_path, source });
}
}
}
Err(HttpDownloadError::CreateFile {
path: target_path.with_file_name(format!(".{prefix}.elydownload")),
source: io::Error::new(io::ErrorKind::AlreadyExists, "temporary download path exhausted"),
})
}
fn content_length(response: &ureq::Response) -> Option<u64> {
response.header("Content-Length").and_then(|value| value.parse().ok())
}
fn validate_content_length(
url: &str,
received_bytes: u64,
total_bytes: Option<u64>,
) -> Result<(), HttpDownloadError> {
if let Some(total_bytes) = total_bytes
&& received_bytes != total_bytes
{
return Err(HttpDownloadError::ContentLengthMismatch {
url: url.to_string(),
received_bytes,
total_bytes,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{
error::Error,
fs,
io::{Read, Write},
net::TcpListener,
path::PathBuf,
thread::{self, JoinHandle},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use ely_domain::UrlText;
use super::{HttpDownloadError, HttpDownloadPlan, download_to_file};
type TestServerHandle = JoinHandle<Result<(), std::io::Error>>;
type SpawnedHttpServer = (String, TestServerHandle);
#[test]
fn downloads_local_http_response_to_file() -> Result<(), Box<dyn Error>> {
let body = b"ELY download body".to_vec();
let (url, server) = spawn_http_server(body.clone(), 200)?;
let target_path = temp_file_path("body")?;
let result =
download_to_file(HttpDownloadPlan::new(UrlText::parse(url)?, target_path.clone()))?;
assert_eq!(result.target_path, target_path);
assert_eq!(result.received_bytes(), body.len() as u64);
assert_eq!(result.total_bytes, Some(body.len() as u64));
assert_eq!(fs::read(&target_path)?, body);
fs::remove_file(target_path)?;
join_server(server)?;
Ok(())
}
#[test]
fn rejects_internal_scheme_before_writing_file() -> Result<(), Box<dyn Error>> {
let target_path = temp_file_path("internal")?;
let error = match download_to_file(HttpDownloadPlan::new(
UrlText::parse("ely://downloads")?,
target_path.clone(),
)) {
Ok(_) => return Err("internal scheme download succeeded".into()),
Err(error) => error,
};
assert!(
matches!(error, HttpDownloadError::UnsupportedScheme { scheme } if scheme == "ely")
);
assert!(!target_path.exists());
Ok(())
}
#[test]
fn reports_http_status_errors() -> Result<(), Box<dyn Error>> {
let (url, server) = spawn_http_server(Vec::new(), 404)?;
let target_path = temp_file_path("status")?;
let error = match download_to_file(HttpDownloadPlan::new(
UrlText::parse(url.clone())?,
target_path.clone(),
)) {
Ok(_) => return Err("HTTP error download succeeded".into()),
Err(error) => error,
};
assert!(matches!(error, HttpDownloadError::HttpStatus { status: 404, .. }));
assert!(!target_path.exists());
join_server(server)?;
Ok(())
}
fn spawn_http_server(body: Vec<u8>, status: u16) -> Result<SpawnedHttpServer, Box<dyn Error>> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
let handle = thread::spawn(move || -> Result<(), std::io::Error> {
let (mut stream, _) = listener.accept()?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut request_buffer = [0; 1024];
_ = stream.read(&mut request_buffer)?;
let status_text = if status == 200 { "OK" } else { "Not Found" };
let response = format!(
"HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
stream.write_all(response.as_bytes())?;
stream.write_all(&body)?;
stream.flush()
});
Ok((format!("http://{address}/download.bin"), handle))
}
fn join_server(handle: TestServerHandle) -> Result<(), Box<dyn Error>> {
match handle.join() {
Ok(result) => result.map_err(Into::into),
Err(_) => Err("HTTP server thread panicked".into()),
}
}
fn temp_file_path(name: &str) -> Result<PathBuf, Box<dyn Error>> {
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
Ok(std::env::temp_dir().join(format!("ely-http-download-{name}-{nanos}.bin")))
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod download_checksums;
pub mod download_files;
pub mod http_downloads;
pub mod plugin_package_store;
pub mod plugin_packages;
pub mod plugin_signatures;
+24 -2
View File
@@ -42,6 +42,9 @@ impl ElyShell {
if install_plugin_from_file_command(command) {
self.choose_plugin_package(window, cx);
}
if download_current_page_command(command) {
self.download_active_tab(window, cx);
}
match space_file_command(command) {
Some(SpaceFileCommand::ExportActiveSpace) => self.export_active_space(window, cx),
@@ -83,6 +86,18 @@ fn install_plugin_from_file_command(command: &str) -> bool {
)
}
fn download_current_page_command(command: &str) -> bool {
matches!(
command.trim().to_ascii_lowercase().as_str(),
"download-current-page"
| "download current page"
| "save-page"
| "save page"
| "save-current-page"
| "save current page"
)
}
fn space_file_command(command: &str) -> Option<SpaceFileCommand> {
match command.trim().to_ascii_lowercase().as_str() {
"export-space" | "export space" | "export-active-space" | "export active space" => {
@@ -134,8 +149,8 @@ fn local_data_file_command(command: &str) -> Option<LocalDataFileCommand> {
mod tests {
use super::{
BookmarkFileCommand, LocalDataFileCommand, ShortcutFileCommand, SpaceFileCommand,
bookmark_file_command, install_plugin_from_file_command, local_data_file_command,
shortcut_file_command, space_file_command,
bookmark_file_command, download_current_page_command, install_plugin_from_file_command,
local_data_file_command, shortcut_file_command, space_file_command,
};
#[test]
@@ -151,6 +166,13 @@ mod tests {
assert!(!install_plugin_from_file_command("open plugins"));
}
#[test]
fn download_current_page_command_matches_save_aliases() {
assert!(download_current_page_command("download-current-page"));
assert!(download_current_page_command("save current page"));
assert!(!download_current_page_command("open downloads"));
}
#[test]
fn space_file_command_matches_export_and_import_aliases() {
assert_eq!(space_file_command("export-space"), Some(SpaceFileCommand::ExportActiveSpace));
@@ -0,0 +1,156 @@
use std::path::{Path, PathBuf};
use directories::UserDirs;
use ely_browser_core::BrowserCore;
use ely_domain::{DownloadDestination, UrlText};
use url::Url;
pub(super) enum ActiveTabDownloadTarget {
Prompt { url: UrlText, directory: PathBuf, file_name: String },
Ready { url: UrlText, target_path: PathBuf },
}
pub(super) fn active_tab_download_target(
core: &BrowserCore,
) -> Result<ActiveTabDownloadTarget, String> {
let tab = core.active_tab().map_err(|error| error.to_string())?;
let parsed = Url::parse(tab.url().as_str()).map_err(|error| error.to_string())?;
match parsed.scheme() {
"http" | "https" => {}
_ => return Err("Only HTTP and HTTPS pages can be downloaded.".to_string()),
}
let snapshot = core.snapshot().map_err(|error| error.to_string())?;
let file_name = suggested_download_file_name(tab.url(), tab.title());
match snapshot.active_download_policy.destination() {
DownloadDestination::AskEveryTime => Ok(ActiveTabDownloadTarget::Prompt {
url: tab.url().clone(),
directory: default_download_directory()?,
file_name,
}),
DownloadDestination::FixedDirectory(directory) => Ok(ActiveTabDownloadTarget::Ready {
url: tab.url().clone(),
target_path: unique_download_target_path(directory, &file_name),
}),
}
}
fn default_download_directory() -> Result<PathBuf, String> {
UserDirs::new()
.and_then(|dirs| dirs.download_dir().map(Path::to_path_buf))
.ok_or_else(|| "Downloads directory is unavailable.".to_string())
}
fn suggested_download_file_name(url: &UrlText, title: &str) -> String {
let parsed = Url::parse(url.as_str()).ok();
let url_file_name = parsed
.as_ref()
.filter(|url| !url.path().ends_with('/'))
.and_then(|url| url.path_segments())
.and_then(|mut segments| segments.rfind(|segment| !segment.is_empty()))
.map(str::to_string);
let raw_file_name = url_file_name
.or_else(|| page_title_file_name(title))
.or_else(|| {
parsed.as_ref().and_then(|url| url.host_str().map(|host| format!("{host}.html")))
})
.unwrap_or_else(|| "download.html".to_string());
sanitize_download_file_name(&raw_file_name)
}
fn page_title_file_name(title: &str) -> Option<String> {
let title = title.trim();
(!title.is_empty()).then(|| format!("{title}.html"))
}
fn sanitize_download_file_name(file_name: &str) -> String {
let cleaned = file_name
.trim()
.chars()
.filter_map(|ch| match ch {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => Some('-'),
ch if ch.is_control() => None,
ch => Some(ch),
})
.take(160)
.collect::<String>();
let cleaned = cleaned.trim_matches(|ch| ch == '.' || ch == ' ').to_string();
match cleaned.as_str() {
"" | "." | ".." => "download.html".to_string(),
_ => cleaned,
}
}
fn unique_download_target_path(directory: &Path, file_name: &str) -> PathBuf {
let candidate = directory.join(file_name);
if !candidate.exists() {
return candidate;
}
let (stem, extension) = split_file_name(file_name);
for suffix in 1..1_000 {
let file_name = match extension {
Some(extension) => format!("{stem} ({suffix}).{extension}"),
None => format!("{stem} ({suffix})"),
};
let candidate = directory.join(file_name);
if !candidate.exists() {
return candidate;
}
}
directory.join(format!("{stem} (1000)"))
}
fn split_file_name(file_name: &str) -> (&str, Option<&str>) {
match file_name.rsplit_once('.') {
Some((stem, extension)) if !stem.is_empty() && !extension.is_empty() => {
(stem, Some(extension))
}
_ => (file_name, None),
}
}
#[cfg(test)]
mod tests {
use std::{error::Error, fs};
use ely_domain::UrlText;
use super::{suggested_download_file_name, unique_download_target_path};
#[test]
fn suggested_download_file_name_prefers_url_leaf() -> Result<(), Box<dyn Error>> {
let url = UrlText::parse("https://example.com/files/report.pdf?token=1")?;
assert_eq!(suggested_download_file_name(&url, "Example"), "report.pdf");
Ok(())
}
#[test]
fn suggested_download_file_name_uses_title_for_directory_urls() -> Result<(), Box<dyn Error>> {
let url = UrlText::parse("https://example.com/articles/")?;
assert_eq!(suggested_download_file_name(&url, "A/B: Research"), "A-B- Research.html");
Ok(())
}
#[test]
fn unique_download_target_path_preserves_existing_files() -> Result<(), Box<dyn Error>> {
let directory = std::env::temp_dir().join("ely-unique-download-test");
fs::create_dir_all(&directory)?;
let existing = directory.join("report.pdf");
fs::write(&existing, [])?;
assert_eq!(
unique_download_target_path(&directory, "report.pdf"),
directory.join("report (1).pdf")
);
fs::remove_file(existing)?;
fs::remove_dir(directory)?;
Ok(())
}
}
+136 -4
View File
@@ -1,11 +1,18 @@
use ely_domain::DownloadId;
use gpui::Context;
use std::path::PathBuf;
use ely_domain::{DownloadId, UrlText};
use gpui::{Context, Window};
use crate::services::{
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
download_checksums::DownloadChecksumCalculator,
download_files::DownloadFileAction,
http_downloads::{HttpDownloadError, HttpDownloadPlan, HttpDownloadResult, download_to_file},
};
use super::{ElyShell, ShellState};
use super::{
ElyShell, ShellState,
download_targets::{ActiveTabDownloadTarget, active_tab_download_target},
};
#[derive(Clone, Debug)]
pub(super) struct PendingDownloadFileAction {
@@ -29,6 +36,57 @@ impl PendingDownloadFileAction {
}
impl ElyShell {
pub(super) fn download_active_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.download_action_error = None;
let target = match &self.state {
ShellState::Ready(core) => active_tab_download_target(core),
ShellState::StartupError(message) => Err(message.clone()),
};
let target = match target {
Ok(target) => target,
Err(message) => {
self.set_download_action_error(message, cx);
return;
}
};
match target {
ActiveTabDownloadTarget::Ready { url, target_path } => {
self.start_download_to_path(url, target_path, window, cx);
}
ActiveTabDownloadTarget::Prompt { url, directory, file_name } => {
let prompt = cx.prompt_for_new_path(&directory, Some(&file_name));
cx.spawn_in(window, async move |shell, window| {
let selected_path = match prompt.await {
Ok(Ok(path)) => path,
Ok(Err(error)) => {
_ = shell.update_in(window, |shell, _, cx| {
shell.set_download_action_error(error.to_string(), cx);
});
return;
}
Err(error) => {
_ = shell.update_in(window, |shell, _, cx| {
shell.set_download_action_error(error.to_string(), cx);
});
return;
}
};
let Some(path) = selected_path else {
return;
};
_ = shell.update_in(window, |shell, window, cx| {
shell.start_download_to_path(url, path, window, cx);
});
})
.detach();
}
}
}
pub(super) fn pause_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.pause_download(download_id).is_ok()
@@ -103,6 +161,80 @@ impl ElyShell {
cx.notify();
}
fn start_download_to_path(
&mut self,
source_url: UrlText,
target_path: PathBuf,
window: &mut Window,
cx: &mut Context<Self>,
) {
let download_id = match &mut self.state {
ShellState::Ready(core) => match core.record_download_started_at_path(
source_url.clone(),
target_path.clone(),
None,
) {
Ok(download_id) => Ok(download_id),
Err(error) => Err(error.to_string()),
},
ShellState::StartupError(message) => Err(message.clone()),
};
let download_id = match download_id {
Ok(download_id) => download_id,
Err(message) => {
self.set_download_action_error(message, cx);
return;
}
};
self.open_downloads(window, cx);
cx.notify();
let plan = HttpDownloadPlan::new(source_url, target_path);
cx.spawn_in(window, async move |shell, window| {
let result =
window.background_executor().spawn(async move { download_to_file(plan) }).await;
_ = shell.update_in(window, |shell, _, cx| {
shell.handle_http_download_result(download_id, result, cx);
});
})
.detach();
}
fn handle_http_download_result(
&mut self,
download_id: DownloadId,
result: Result<HttpDownloadResult, HttpDownloadError>,
cx: &mut Context<Self>,
) {
let update_result = match (&mut self.state, result) {
(ShellState::Ready(core), Ok(result)) => core
.complete_download(&download_id, result.received_bytes())
.map(|()| None)
.map_err(|error| error.to_string()),
(ShellState::Ready(core), Err(error)) => {
let message = error.to_string();
match core.fail_download(&download_id) {
Ok(()) => Ok(Some(message)),
Err(state_error) => Err(format!("{message}; {state_error}")),
}
}
(ShellState::StartupError(message), _) => Err(message.clone()),
};
match update_result {
Ok(message) => self.download_action_error = message,
Err(message) => self.download_action_error = Some(message),
}
cx.notify();
}
fn set_download_action_error(&mut self, message: String, cx: &mut Context<Self>) {
self.download_action_error = Some(message);
cx.notify();
}
pub(super) fn open_download_file(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
self.run_download_file_action(download_id, DownloadFileAction::Open, cx);
}
+13 -3
View File
@@ -2,6 +2,7 @@ mod archive_labels;
mod bookmark_files;
mod bookmarks;
mod command_actions;
mod download_targets;
mod downloads;
mod focus;
mod history;
@@ -44,9 +45,9 @@ use plugins::{PendingPluginInstall, PendingPluginUninstall};
use web_surface::WebSurfaceStore;
use crate::{
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
CloseCurrentTab, DownloadCurrentPage, FocusAddressBar, FocusCommandMode, OpenDownloads,
OpenHistory, OpenNewTab, OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab,
SelectNextTab, SelectPreviousTab, ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
};
enum ShellState {
@@ -348,6 +349,15 @@ impl ElyShell {
self.open_downloads(window, cx);
}
fn on_download_current_page(
&mut self,
_: &DownloadCurrentPage,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.download_active_tab(window, cx);
}
fn on_open_history(&mut self, _: &OpenHistory, window: &mut Window, cx: &mut Context<Self>) {
self.open_history(window, cx);
}
+1
View File
@@ -46,6 +46,7 @@ impl ElyShell {
.on_action(cx.listener(Self::on_close_current_tab))
.on_action(cx.listener(Self::on_focus_address_bar))
.on_action(cx.listener(Self::on_focus_command_mode))
.on_action(cx.listener(Self::on_download_current_page))
.on_action(cx.listener(Self::on_open_downloads))
.on_action(cx.listener(Self::on_open_history))
.on_action(cx.listener(Self::on_open_new_tab))