Add task manager internal page

This commit is contained in:
2026-05-08 01:03:59 -04:00
parent b8d2b291b2
commit a6077fa00a
8 changed files with 332 additions and 3 deletions
+4
View File
@@ -18,6 +18,7 @@ actions!(
OpenHistory, OpenHistory,
OpenNewTab, OpenNewTab,
OpenSettings, OpenSettings,
OpenTaskManager,
Quit, Quit,
RestoreClosedTab, RestoreClosedTab,
SelectNextTab, SelectNextTab,
@@ -38,6 +39,8 @@ fn main() {
KeyBinding::new("ctrl-shift-j", OpenDownloads, None), KeyBinding::new("ctrl-shift-j", OpenDownloads, None),
KeyBinding::new("cmd-y", OpenHistory, None), KeyBinding::new("cmd-y", OpenHistory, None),
KeyBinding::new("ctrl-h", OpenHistory, None), KeyBinding::new("ctrl-h", OpenHistory, None),
KeyBinding::new("cmd-escape", OpenTaskManager, None),
KeyBinding::new("shift-escape", OpenTaskManager, None),
KeyBinding::new("cmd-,", OpenSettings, None), KeyBinding::new("cmd-,", OpenSettings, None),
KeyBinding::new("ctrl-,", OpenSettings, None), KeyBinding::new("ctrl-,", OpenSettings, None),
KeyBinding::new("cmd-l", FocusAddressBar, None), KeyBinding::new("cmd-l", FocusAddressBar, None),
@@ -78,6 +81,7 @@ fn main() {
MenuItem::separator(), MenuItem::separator(),
MenuItem::action("Open Downloads", OpenDownloads), MenuItem::action("Open Downloads", OpenDownloads),
MenuItem::action("Open History", OpenHistory), MenuItem::action("Open History", OpenHistory),
MenuItem::action("Open Task Manager", OpenTaskManager),
MenuItem::action("Open Settings", OpenSettings), MenuItem::action("Open Settings", OpenSettings),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action("Toggle Pin", TogglePinnedTab), MenuItem::action("Toggle Pin", TogglePinnedTab),
@@ -7,6 +7,7 @@ mod plugins;
mod profiles; mod profiles;
mod reading_list; mod reading_list;
mod sync; mod sync;
mod task_manager;
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing}; use ely_design_system::{colors, spacing};
@@ -32,6 +33,7 @@ impl ElyShell {
"ely://downloads" => self.render_downloads_page(snapshot, cx), "ely://downloads" => self.render_downloads_page(snapshot, cx),
"ely://history" => self.render_history_page(snapshot, cx), "ely://history" => self.render_history_page(snapshot, cx),
"ely://archive" => self.render_archive_page(snapshot, cx), "ely://archive" => self.render_archive_page(snapshot, cx),
"ely://task-manager" => self.render_task_manager_page(snapshot),
"ely://about" => self.render_about_page(snapshot), "ely://about" => self.render_about_page(snapshot),
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx), "ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx), "ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
@@ -0,0 +1,285 @@
use ely_browser_core::{BrowserSnapshot, InstalledPlugin};
use ely_design_system::colors;
use ely_domain::{BrowserTab, DownloadEntry, DownloadState, TabId, TabState};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px, rgb,
};
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
use super::{
ElyShell,
download_labels::{download_size_label, download_state_label},
render_canvas_surface,
};
impl ElyShell {
pub(super) fn render_task_manager_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement {
render_canvas_surface(
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_5()
.child(render_task_manager_header(snapshot))
.child(render_task_summary(snapshot))
.child(render_task_table(snapshot)),
)
}
}
fn render_task_manager_header(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.flex()
.items_end()
.justify_between()
.gap_4()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Task Manager"))
.child(div().text_sm().text_color(rgb(colors::MUTED)).child(format!(
"{} / {}",
snapshot.active_profile_name, snapshot.active_space_name
))),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(IconName::LayoutDashboard)
.child(format!("{} local tasks", task_count(snapshot))),
)
.into_any_element()
}
fn render_task_summary(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.border_t_1()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.py_3()
.flex()
.items_center()
.justify_between()
.gap_4()
.children([
task_metric("Tabs", snapshot.tabs.len()),
task_metric("Downloads", snapshot.download_entries.len()),
task_metric("Plugins", snapshot.installed_plugins.len()),
task_metric("Spaces", snapshot.spaces.len()),
])
.into_any_element()
}
fn task_metric(label: &'static str, value: usize) -> AnyElement {
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(label))
.child(
div().text_sm().font_semibold().text_color(rgb(colors::INK)).child(value.to_string()),
)
.into_any_element()
}
fn render_task_table(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.flex_1()
.min_h_0()
.flex()
.flex_col()
.overflow_y_scrollbar()
.child(render_section_header("Tabs"))
.children(
snapshot
.tabs
.iter()
.enumerate()
.map(|(index, tab)| render_tab_task_row(index, tab, &snapshot.active_tab_id)),
)
.when(!snapshot.download_entries.is_empty(), |this| {
this.child(render_section_header("Downloads")).children(
snapshot
.download_entries
.iter()
.rev()
.enumerate()
.map(|(index, entry)| render_download_task_row(index, entry)),
)
})
.when(!snapshot.installed_plugins.is_empty(), |this| {
this.child(render_section_header("Plugins")).children(
snapshot
.installed_plugins
.iter()
.enumerate()
.map(|(index, plugin)| render_plugin_task_row(index, plugin)),
)
})
.into_any_element()
}
fn render_section_header(label: &'static str) -> AnyElement {
div()
.pt_4()
.pb_2()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(label)
.into_any_element()
}
fn render_tab_task_row(index: usize, tab: &BrowserTab, active_tab_id: &TabId) -> AnyElement {
let status = tab_state_label(tab.state());
let status_color = tab_state_color(tab.state());
let scope = if tab.id() == active_tab_id { "Active tab" } else { "Tab" };
let icon = if tab.id() == active_tab_id { IconName::CircleCheck } else { IconName::Globe };
render_task_row(
format!("tab-task-{index}"),
icon,
tab.title().to_string(),
tab.display_url(),
scope,
status,
status_color,
)
}
fn render_download_task_row(index: usize, entry: &DownloadEntry) -> AnyElement {
render_task_row(
format!("download-task-{index}"),
IconName::File,
entry.file_name().to_string(),
download_size_label(entry),
"Download",
download_state_label(entry.state()),
download_state_color(entry.state()),
)
}
fn render_plugin_task_row(index: usize, plugin: &InstalledPlugin) -> AnyElement {
let status = if plugin.enabled() { "Enabled" } else { "Disabled" };
let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED };
let detail = format!(
"{} permissions - {} contributions",
plugin.manifest().permissions().len(),
plugin.manifest().contributes().len()
);
render_task_row(
format!("plugin-task-{index}"),
IconName::Asterisk,
plugin.manifest().name().to_string(),
detail,
"Plugin",
status,
status_color,
)
}
fn render_task_row(
id: String,
icon: IconName,
title: String,
detail: String,
scope: &'static str,
status: &'static str,
status_color: u32,
) -> AnyElement {
div()
.id(SharedString::from(id))
.py_3()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.flex()
.items_center()
.justify_between()
.gap_4()
.child(
div()
.min_w_0()
.flex()
.items_center()
.gap_3()
.child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon))
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_semibold()
.truncate()
.text_color(rgb(colors::INK))
.child(title),
)
.child(
div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(detail),
),
),
)
.child(
div()
.flex()
.items_center()
.justify_end()
.gap_3()
.text_xs()
.child(div().min_w(px(86.0)).text_color(rgb(colors::MUTED)).child(scope))
.child(
div()
.min_w(px(76.0))
.font_semibold()
.text_color(rgb(status_color))
.child(status),
),
)
.into_any_element()
}
fn task_count(snapshot: &BrowserSnapshot) -> usize {
snapshot.tabs.len() + snapshot.download_entries.len() + snapshot.installed_plugins.len()
}
fn tab_state_label(state: &TabState) -> &'static str {
match state {
TabState::Loading => "Loading",
TabState::Ready => "Ready",
TabState::Crashed => "Crashed",
TabState::Discarded => "Discarded",
TabState::Archived => "Archived",
}
}
fn tab_state_color(state: &TabState) -> u32 {
match state {
TabState::Loading => colors::PRIMARY,
TabState::Ready => colors::SUCCESS,
TabState::Crashed => colors::ERROR,
TabState::Discarded | TabState::Archived => colors::MUTED,
}
}
fn download_state_color(state: &DownloadState) -> u32 {
match state {
DownloadState::InProgress => colors::PRIMARY,
DownloadState::Completed => colors::SUCCESS,
DownloadState::Paused => colors::MUTED,
DownloadState::Cancelled | DownloadState::Failed => colors::ERROR,
}
}
+15 -2
View File
@@ -13,8 +13,8 @@ use plugins::{PendingPluginInstall, PendingPluginUninstall};
use crate::{ use crate::{
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab, CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab, OpenSettings, OpenTaskManager, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
TogglePinnedTab, ToggleFavoriteTab, TogglePinnedTab,
}; };
enum ShellState { enum ShellState {
@@ -114,6 +114,10 @@ impl ElyShell {
self.open_internal_tab("ely://settings", window, cx); self.open_internal_tab("ely://settings", window, cx);
} }
fn open_task_manager(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.open_internal_tab("ely://task-manager", window, cx);
}
fn open_internal_tab(&mut self, url_text: &str, window: &mut Window, cx: &mut Context<Self>) { fn open_internal_tab(&mut self, url_text: &str, window: &mut Window, cx: &mut Context<Self>) {
if let Ok(url) = UrlText::parse(url_text) { if let Ok(url) = UrlText::parse(url_text) {
self.open_url(url, window, cx); self.open_url(url, window, cx);
@@ -294,6 +298,15 @@ impl ElyShell {
self.open_settings(window, cx); self.open_settings(window, cx);
} }
fn on_open_task_manager(
&mut self,
_: &OpenTaskManager,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_task_manager(window, cx);
}
fn on_restore_closed_tab( fn on_restore_closed_tab(
&mut self, &mut self,
_: &RestoreClosedTab, _: &RestoreClosedTab,
+1
View File
@@ -42,6 +42,7 @@ impl ElyShell {
.on_action(cx.listener(Self::on_open_history)) .on_action(cx.listener(Self::on_open_history))
.on_action(cx.listener(Self::on_open_new_tab)) .on_action(cx.listener(Self::on_open_new_tab))
.on_action(cx.listener(Self::on_open_settings)) .on_action(cx.listener(Self::on_open_settings))
.on_action(cx.listener(Self::on_open_task_manager))
.on_action(cx.listener(Self::on_restore_closed_tab)) .on_action(cx.listener(Self::on_restore_closed_tab))
.on_action(cx.listener(Self::on_select_next_tab)) .on_action(cx.listener(Self::on_select_next_tab))
.on_action(cx.listener(Self::on_select_previous_tab)) .on_action(cx.listener(Self::on_select_previous_tab))
@@ -21,6 +21,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
"ely://downloads" => Some("Downloads"), "ely://downloads" => Some("Downloads"),
"ely://history" => Some("History"), "ely://history" => Some("History"),
"ely://archive" => Some("Archived Tabs"), "ely://archive" => Some("Archived Tabs"),
"ely://task-manager" => Some("Task Manager"),
"ely://about" => Some("About ELY Browser"), "ely://about" => Some("About ELY Browser"),
"ely://settings" => Some("Settings"), "ely://settings" => Some("Settings"),
"ely://settings/plugins" => Some("Plugin Settings"), "ely://settings/plugins" => Some("Plugin Settings"),
@@ -98,6 +99,10 @@ pub(crate) fn history_url() -> Result<UrlText, CoreError> {
internal_page_url("ely://history") internal_page_url("ely://history")
} }
pub(crate) fn task_manager_url() -> Result<UrlText, CoreError> {
internal_page_url("ely://task-manager")
}
pub(crate) fn about_url() -> Result<UrlText, CoreError> { pub(crate) fn about_url() -> Result<UrlText, CoreError> {
internal_page_url("ely://about") internal_page_url("ely://about")
} }
@@ -5,7 +5,7 @@ use crate::{
navigation::{ navigation::{
about_url, bookmarks_url, downloads_url, history_url, move_tab_space_name, about_url, bookmarks_url, downloads_url, history_url, move_tab_space_name,
new_profile_name, new_space_name, reading_list_url, search_url, settings_page_url, new_profile_name, new_space_name, reading_list_url, search_url, settings_page_url,
settings_url, space_icon, switch_profile_name, sync_status_url, settings_url, space_icon, switch_profile_name, sync_status_url, task_manager_url,
}, },
}; };
@@ -126,6 +126,10 @@ impl BrowserCore {
self.open_tab(history_url()?); self.open_tab(history_url()?);
Ok(true) Ok(true)
} }
"task-manager" | "tasks" | "open-task-manager" | "open task manager" => {
self.open_tab(task_manager_url()?);
Ok(true)
}
"about" | "open-about" | "open about" => { "about" | "open-about" | "open about" => {
self.open_tab(about_url()?); self.open_tab(about_url()?);
Ok(true) Ok(true)
+15
View File
@@ -81,6 +81,21 @@ fn open_history_command_opens_history_page() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
#[test]
fn open_task_manager_command_opens_task_manager_page() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.set_command_query(">open-task-manager");
let intent = core.submit_command()?;
let active_tab = core.active_tab()?;
assert_eq!(intent, Some(CommandIntent::Command("open-task-manager".to_string())));
assert_eq!(active_tab.title(), "Task Manager");
assert_eq!(active_tab.url().as_str(), "ely://task-manager");
assert_eq!(core.snapshot()?.command_query, "");
Ok(())
}
#[test] #[test]
fn open_about_command_opens_about_page() -> Result<(), Box<dyn Error>> { fn open_about_command_opens_about_page() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;