feat: add default applications page

This commit is contained in:
Michael Aaron Murphy 2024-11-22 00:42:00 +01:00
parent 4e310024b7
commit 508b05135a
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
24 changed files with 682 additions and 205 deletions

View file

@ -42,6 +42,7 @@ itertools = "0.13.0"
itoa = "1.0.11"
libcosmic.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
notify = "6.1.1"
once_cell = "1.19.0"
regex = "1.10.6"
@ -67,6 +68,7 @@ zbus = { version = "4.4.0", features = ["tokio"], optional = true }
ustr = "1.0.0"
fontdb = "0.16.2"
fixed_decimal = "0.5.6"
mime = "0.3.17"
[dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
@ -96,6 +98,7 @@ linux = [
"page-about",
"page-bluetooth",
"page-date",
"page-default-apps",
"page-input",
"page-networking",
"page-power",
@ -111,6 +114,7 @@ linux = [
page-about = ["dep:cosmic-settings-system", "dep:hostname1-zbus", "dep:zbus"]
page-bluetooth = ["dep:bluez-zbus", "dep:zbus"]
page-date = ["dep:timedate-zbus", "dep:zbus"]
page-default-apps = ["dep:mime-apps"]
page-input = [
"dep:cosmic-comp-config",
"dep:cosmic-settings-config",

View file

@ -51,7 +51,6 @@ use desktop::{
use event::wayland;
use page::Entity;
use std::collections::BTreeSet;
use std::time::Duration;
use std::{borrow::Cow, str::FromStr};
#[allow(clippy::struct_excessive_bools)]
@ -82,6 +81,8 @@ impl SettingsApp {
PageCommands::Bluetooth => self.pages.page_id::<bluetooth::Page>(),
#[cfg(feature = "page-date")]
PageCommands::DateTime => self.pages.page_id::<time::date::Page>(),
#[cfg(feature = "page-default-apps")]
PageCommands::DefaultApps => self.pages.page_id::<system::default_apps::Page>(),
PageCommands::Desktop => self.pages.page_id::<desktop::Page>(),
PageCommands::Displays => self.pages.page_id::<display::Page>(),
#[cfg(feature = "wayland")]
@ -212,10 +213,7 @@ impl cosmic::Application for SettingsApp {
}
.unwrap_or(desktop_id);
(
app,
cosmic::command::message(Message::DelayedInit(active_id)),
)
(app, cosmic::task::message(Message::DelayedInit(active_id)))
}
fn nav_model(&self) -> Option<&nav_bar::Model> {
@ -389,6 +387,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-default-apps")]
crate::pages::Message::DefaultApps(message) => {
if let Some(page) = self.pages.page_mut::<system::default_apps::Page>() {
return page.update(message).map(Into::into);
}
}
crate::pages::Message::Desktop(message) => {
page::update!(self.pages, message, desktop::Page);
}
@ -718,7 +723,7 @@ impl cosmic::Application for SettingsApp {
// It is necessary to delay init to allow time for the page sender to be initialized
Message::DelayedInit(active_id) => {
if self.page_sender.is_none() {
return cosmic::command::message(Message::DelayedInit(active_id));
return cosmic::task::message(Message::DelayedInit(active_id));
}
return self.activate_page(active_id);
@ -832,7 +837,7 @@ impl SettingsApp {
Task::batch(vec![
leave_task,
page_task,
cosmic::command::future(async { Message::SetWindowTitle }),
cosmic::task::future(async { Message::SetWindowTitle }),
])
}
@ -1005,7 +1010,7 @@ impl SettingsApp {
if tasks.is_empty() {
Task::none()
} else {
cosmic::command::batch(tasks)
cosmic::task::batch(tasks)
.map(Message::PageMessage)
.map(Into::into)
}

View file

@ -49,6 +49,9 @@ pub enum PageCommands {
/// Date & Time settings page
#[cfg(feature = "page-date")]
DateTime,
/// Default application associations
#[cfg(feature = "page-default-apps")]
DefaultApps,
/// Desktop settings page
Desktop,
/// Displays settings page

View file

@ -78,7 +78,7 @@ impl page::Page<crate::pages::Message> for Page {
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
// TODO start stream for new device
cosmic::command::future(async move {
cosmic::task::future(async move {
match zbus::Connection::system().await {
Ok(connection) => Message::DBusConnect(connection, sender),
Err(why) => Message::DBusError(why.to_string()),
@ -261,7 +261,7 @@ impl Page {
Active::Disabling
};
self.update_status();
return cosmic::command::future(change_adapter_status(
return cosmic::task::future(change_adapter_status(
connection.clone(),
path,
active,
@ -276,7 +276,7 @@ impl Page {
} else {
Active::Disabling
};
cosmic::command::future(change_adapter_status(
cosmic::task::future(change_adapter_status(
connection.clone(),
path.clone(),
active,
@ -284,7 +284,7 @@ impl Page {
})
.collect();
self.update_status();
return cosmic::command::batch(tasks);
return cosmic::task::batch(tasks);
}
tracing::warn!("No DBus connection ready");
}
@ -306,7 +306,7 @@ impl Page {
));
}
return cosmic::command::future(async move {
return cosmic::task::future(async move {
let result: zbus::Result<HashMap<OwnedObjectPath, Adapter>> = async {
futures::future::join_all(
bluez_zbus::get_adapters(&connection)
@ -342,7 +342,7 @@ impl Page {
self.update_status();
if self.selected_adapter.is_none() && self.adapters.len() == 1 {
return cosmic::command::message(Message::SelectAdapter(
return cosmic::task::message(Message::SelectAdapter(
self.adapters.keys().next().cloned(),
));
}
@ -365,7 +365,7 @@ impl Page {
tracing::debug!("Adapter {} added", adapter.address);
self.adapters.insert(path.clone(), adapter);
if self.selected_adapter.is_none() {
return cosmic::command::message(Message::SelectAdapter(Some(path)));
return cosmic::task::message(Message::SelectAdapter(Some(path)));
}
}
Message::UpdatedAdapter(path, update) => {
@ -381,7 +381,7 @@ impl Page {
&& existing.scanning == Active::Disabled =>
{
existing.scanning = Active::Enabling;
return cosmic::command::future(start_discovery(connection, path));
return cosmic::task::future(start_discovery(connection, path));
}
_ => {}
}
@ -412,19 +412,20 @@ impl Page {
if let Some(connection) = self.connection.as_ref() {
let connection = connection.clone();
if let Some((path, adapter)) = self.get_selected_adapter_mut() {
let mut fut: Vec<Task<Message>> = vec![cosmic::command::future(
get_devices(connection.clone(), path.clone()),
)];
let mut fut: Vec<Task<Message>> = vec![cosmic::task::future(get_devices(
connection.clone(),
path.clone(),
))];
if adapter.enabled == Active::Enabled
&& adapter.scanning == Active::Disabled
{
fut.push(cosmic::command::future(start_discovery(
fut.push(cosmic::task::future(start_discovery(
connection,
path.clone(),
)));
}
return cosmic::command::batch(fut);
return cosmic::task::batch(fut);
}
} else {
tracing::warn!("No DBus connection ready");
@ -440,7 +441,7 @@ impl Page {
let connection = connection.clone();
if let Some(device) = self.devices.get_mut(&path) {
device.enabled = Active::Disabling;
return cosmic::command::future(forget_device(connection, path.clone()));
return cosmic::task::future(forget_device(connection, path.clone()));
}
} else {
tracing::warn!("No DBus connection ready");
@ -458,7 +459,7 @@ impl Page {
return cosmic::Task::none();
}
device.enabled = Active::Enabling;
return cosmic::command::future(connect_device(connection, path));
return cosmic::task::future(connect_device(connection, path));
}
} else {
tracing::warn!("No DBus connection ready");
@ -474,7 +475,7 @@ impl Page {
return cosmic::Task::none();
}
device.enabled = Active::Disabling;
return cosmic::command::future(disconnect_device(connection, path));
return cosmic::task::future(disconnect_device(connection, path));
}
} else {
tracing::warn!("No DBus connection ready");

View file

@ -22,7 +22,7 @@ use cosmic::iced_widget::scrollable::{Direction, Scrollbar};
use cosmic::widget::icon::{from_name, icon};
use cosmic::widget::{
button, color_picker::ColorPickerUpdate, container, flex_row, horizontal_space, radio, row,
scrollable, settings, spin_button, text, ColorPickerModel,
scrollable, settings, text, ColorPickerModel,
};
use cosmic::{widget, Apply, Element, Task};
#[cfg(feature = "wayland")]
@ -387,7 +387,7 @@ impl Page {
)
),
// Icon theme previews
cosmic::widget::column::with_children(vec![
widget::column::with_children(vec![
text::heading(&*ICON_THEME).into(),
flex_row(
self.icon_themes
@ -424,7 +424,7 @@ impl Page {
self.context_view = Some(ContextView::MonospaceFont);
self.font_search.clear();
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("monospace-font").into(),
));
@ -434,7 +434,7 @@ impl Page {
self.context_view = Some(ContextView::SystemFont);
self.font_search.clear();
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("interface-font").into(),
));
@ -781,7 +781,7 @@ impl Page {
}
Message::Left => {
tasks.push(cosmic::command::message(app::Message::SetTheme(
tasks.push(cosmic::task::message(app::Message::SetTheme(
cosmic::theme::system_preference(),
)));
}
@ -865,7 +865,7 @@ impl Page {
#[cfg(feature = "ashpd")]
Message::StartImport => {
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
let res = SelectedFiles::open_file()
.modal(true)
.filter(FileFilter::glob(FileFilter::new("ron"), "*.ron"))
@ -888,7 +888,7 @@ impl Page {
let is_dark = self.theme_mode.is_dark;
let name = format!("{}.ron", if is_dark { fl!("dark") } else { fl!("light") });
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
let res = SelectedFiles::save_file()
.modal(true)
.current_name(Some(name.as_str()))
@ -918,7 +918,7 @@ impl Page {
return Task::none();
};
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
let res = tokio::fs::read_to_string(path).await;
if let Some(b) = res.ok().and_then(|s| ron::de::from_str(&s).ok()) {
Message::ImportSuccess(Box::new(b))
@ -944,7 +944,7 @@ impl Page {
let theme_builder = self.theme_builder.clone();
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
let Ok(builder) =
ron::ser::to_string_pretty(&theme_builder, PrettyConfig::default())
else {
@ -1062,7 +1062,7 @@ impl Page {
Message::IconsAndToolkit => {
self.context_view = Some(ContextView::IconsAndToolkit);
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
"".into(),
));
@ -1080,7 +1080,7 @@ impl Page {
let is_dark = self.theme_mode.is_dark;
let current_theme = self.theme.clone();
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
let config = if is_dark {
Theme::dark_config()
} else {
@ -1180,7 +1180,7 @@ impl Page {
let task = match message {
ColorPickerUpdate::AppliedColor | ColorPickerUpdate::Reset => {
needs_update = true;
cosmic::command::message(crate::app::Message::CloseContextDrawer)
cosmic::task::message(crate::app::Message::CloseContextDrawer)
}
ColorPickerUpdate::ActionFinished => {
@ -1189,12 +1189,12 @@ impl Page {
}
ColorPickerUpdate::Cancel => {
cosmic::command::message(crate::app::Message::CloseContextDrawer)
cosmic::task::message(crate::app::Message::CloseContextDrawer)
}
ColorPickerUpdate::ToggleColorPicker => {
self.context_view = Some(context_view);
cosmic::command::message(crate::app::Message::OpenContextDrawer(
cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
context_title,
))
@ -1436,11 +1436,11 @@ impl page::Page<crate::pages::Message> for Page {
&mut self,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Task<crate::pages::Message> {
let (task, handle) = cosmic::command::batch(vec![
let (task, handle) = cosmic::task::batch(vec![
// Load icon themes
cosmic::command::future(icon_themes::fetch()).map(crate::pages::Message::Appearance),
cosmic::task::future(icon_themes::fetch()).map(crate::pages::Message::Appearance),
// Load font families
cosmic::command::future(async move {
cosmic::task::future(async move {
let (mono, interface) = font_config::load_font_families();
Message::FontConfig(font_config::Message::LoadedFonts(mono, interface))
})
@ -1457,7 +1457,7 @@ impl page::Page<crate::pages::Message> for Page {
handle.abort();
}
cosmic::command::message(crate::pages::Message::Appearance(Message::Left))
cosmic::task::message(crate::pages::Message::Appearance(Message::Left))
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
@ -1971,7 +1971,7 @@ pub fn window_management() -> Section<crate::pages::Message> {
settings::section()
.title(&section.title)
.add(settings::item::builder(&descriptions[active_hint]).control(
cosmic::widget::spin_button(
widget::spin_button(
page.theme_builder.active_hint.to_string(),
page.theme_builder.active_hint,
1,
@ -1980,16 +1980,16 @@ pub fn window_management() -> Section<crate::pages::Message> {
Message::WindowHintSize,
),
))
.add(settings::item::builder(&descriptions[gaps]).control(
cosmic::widget::spin_button(
.add(
settings::item::builder(&descriptions[gaps]).control(widget::spin_button(
page.theme_builder.gaps.1.to_string(),
page.theme_builder.gaps.1,
1,
page.theme.active_hint,
500,
Message::GapSize,
),
))
)),
)
.apply(Element::from)
.map(crate::pages::Message::Appearance)
})

View file

@ -442,7 +442,7 @@ impl Page {
}
Message::AddAppletDrawer => {
self.context = Some(ContextDrawer::AddApplet);
return cosmic::command::message(app::Message::OpenContextDrawer(
return cosmic::task::message(app::Message::OpenContextDrawer(
self.entity,
Cow::Owned(fl!("add-applet")),
));

View file

@ -577,7 +577,7 @@ impl Page {
Category::Wallpapers => {
if self.config.current_folder.is_some() {
let _ = self.config.set_current_folder(None);
task = cosmic::command::future(async move {
task = cosmic::task::future(async move {
let folder = change_folder(Config::default_folder().to_owned(), true).await;
Message::ChangeFolder(folder)
});
@ -597,7 +597,7 @@ impl Page {
tracing::error!(?path, ?why, "failed to set current folder");
}
task = cosmic::command::future(async move {
task = cosmic::task::future(async move {
Message::ChangeFolder(change_folder(path, false).await)
});
}
@ -605,7 +605,7 @@ impl Page {
Category::AddFolder => {
#[cfg(feature = "xdg-portal")]
return cosmic::command::future(async {
return cosmic::task::future(async {
let dialog_result = file_chooser::open::Dialog::new()
.title(fl!("wallpaper", "folder-dialog"))
.accept_label(fl!("dialog-add"))
@ -751,7 +751,7 @@ impl Page {
Message::ColorAddContext => {
self.context_view = Some(ContextView::AddColor);
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("color-picker").into(),
));
@ -972,20 +972,20 @@ impl Page {
}
// Load preview images concurrently for each custom image stored in the on-disk config.
return cosmic::command::batch(
return cosmic::task::batch(
self.config
.custom_images()
.iter()
.cloned()
.map(|path| {
cosmic::command::future(async move {
cosmic::task::future(async move {
let result = wallpaper::load_image_with_thumbnail(path);
Message::ImageAdd(result.map(Arc::new))
})
})
// Cache wallpaper preview early to prevent blank previews on reload
.chain(std::iter::once(cosmic::command::message::<
.chain(std::iter::once(cosmic::task::message::<
_,
crate::app::Message,
>(

View file

@ -277,7 +277,7 @@ impl page::Page<crate::pages::Message> for Page {
}));
}
cosmic::command::future(on_enter())
cosmic::task::future(on_enter())
}
fn on_leave(&mut self) -> Task<crate::pages::Message> {
@ -293,7 +293,7 @@ impl page::Page<crate::pages::Message> for Page {
&mut self,
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Task<crate::pages::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
let mut randr = List::default();
let test_mode = randr.modes.insert(cosmic_randr_shell::Mode {
@ -380,7 +380,7 @@ impl Page {
tracing::error!(?why, "cosmic-randr error");
} else {
// Reload display info
return cosmic::command::future(async move {
return cosmic::task::future(async move {
crate::Message::PageMessage(on_enter().await)
});
}
@ -406,11 +406,11 @@ impl Page {
Message::DialogCountdown => {
if self.dialog_countdown == 0 {
if self.dialog.is_some() {
return cosmic::command::message(app::Message::from(Message::DialogCancel));
return cosmic::task::message(app::Message::from(Message::DialogCancel));
}
} else {
self.dialog_countdown -= 1;
return cosmic::command::future(async move {
return cosmic::task::future(async move {
tokio::time::sleep(time::Duration::from_secs(1)).await;
Message::DialogCountdown
});
@ -449,7 +449,7 @@ impl Page {
//
// Message::NightLightContext => {
// self.context = Some(ContextDrawer::NightLight);
// return cosmic::command::message(app::Message::OpenContextDrawer(
// return cosmic::task::message(app::Message::OpenContextDrawer(
// text::NIGHT_LIGHT.clone().into(),
// ));
// }
@ -473,7 +473,7 @@ impl Page {
Message::Position(display, x, y) => return self.set_position(display, x, y),
Message::Refresh => {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
crate::Message::PageMessage(on_enter().await)
});
}
@ -583,7 +583,7 @@ impl Page {
}
self.dialog = Some(revert_request);
self.dialog_countdown = 10;
cosmic::command::future(async {
cosmic::task::future(async {
tokio::time::sleep(time::Duration::from_secs(1)).await;
app::Message::from(Message::DialogCountdown)
})
@ -872,7 +872,7 @@ impl Page {
// Removes the dialog if no change is being made
if Some(request) == self.dialog {
tasks.push(cosmic::command::message(app::Message::from(
tasks.push(cosmic::task::message(app::Message::from(
Message::DialogComplete,
)));
}
@ -970,7 +970,7 @@ impl Page {
}
}
tasks.push(cosmic::command::future(async move {
tasks.push(cosmic::task::future(async move {
tracing::debug!(?task, "executing");
app::Message::from(Message::RandrResult(Arc::new(task.status().await)))
}));

View file

@ -460,7 +460,7 @@ impl Page {
Message::ShowInputSourcesContext => {
self.context = Some(Context::ShowInputSourcesContext);
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("keyboard-sources", "add").into(),
));
@ -472,7 +472,7 @@ impl Page {
Message::OpenSpecialCharacterContext(key) => {
self.context = Some(Context::SpecialCharacter(key));
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
key.title().into(),
));

View file

@ -393,7 +393,7 @@ impl Model {
self.shortcut_context = Some(id);
self.replace_dialog = None;
let mut tasks = vec![cosmic::command::message(
let mut tasks = vec![cosmic::task::message(
crate::app::Message::OpenContextDrawer(self.entity, description.into()),
)];

View file

@ -212,7 +212,7 @@ impl Page {
Message::ShortcutContext => {
self.add_shortcut.enable();
return Task::batch(vec![
cosmic::command::message(crate::app::Message::OpenContextDrawer(
cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("custom-shortcuts", "context").into(),
)),

View file

@ -221,28 +221,28 @@ impl Page {
match message {
Message::Category(category) => match category {
Category::Custom => {
cosmic::command::message(crate::app::Message::Page(self.sub_pages.custom))
cosmic::task::message(crate::app::Message::Page(self.sub_pages.custom))
}
Category::ManageWindow => cosmic::command::message(crate::app::Message::Page(
self.sub_pages.manage_window,
)),
Category::ManageWindow => {
cosmic::task::message(crate::app::Message::Page(self.sub_pages.manage_window))
}
Category::MoveWindow => {
cosmic::command::message(crate::app::Message::Page(self.sub_pages.move_window))
cosmic::task::message(crate::app::Message::Page(self.sub_pages.move_window))
}
Category::Nav => {
cosmic::command::message(crate::app::Message::Page(self.sub_pages.nav))
cosmic::task::message(crate::app::Message::Page(self.sub_pages.nav))
}
Category::System => {
cosmic::command::message(crate::app::Message::Page(self.sub_pages.system))
cosmic::task::message(crate::app::Message::Page(self.sub_pages.system))
}
Category::WindowTiling => cosmic::command::message(crate::app::Message::Page(
self.sub_pages.window_tiling,
)),
Category::WindowTiling => {
cosmic::task::message(crate::app::Message::Page(self.sub_pages.window_tiling))
}
},
Message::Search(input) => {

View file

@ -29,6 +29,8 @@ pub enum Message {
CustomShortcuts(input::keyboard::shortcuts::custom::Message),
#[cfg(feature = "page-date")]
DateAndTime(time::date::Message),
#[cfg(feature = "page-default-apps")]
DefaultApps(system::default_apps::Message),
Desktop(desktop::Message),
DesktopWallpaper(desktop::wallpaper::Message),
#[cfg(feature = "page-workspaces")]

View file

@ -289,10 +289,10 @@ impl Page {
Message::OpenPage { page, device } => {
let mut tasks = Vec::<Task<crate::app::Message>>::new();
tasks.push(cosmic::command::message(crate::app::Message::Page(page)));
tasks.push(cosmic::task::message(crate::app::Message::Page(page)));
if let Some(device) = device {
tasks.push(cosmic::command::message(crate::app::Message::PageMessage(
tasks.push(cosmic::task::message(crate::app::Message::PageMessage(
match device {
DeviceVariant::WiFi(device) => {
crate::pages::Message::WiFi(wifi::Message::SelectDevice(device))

View file

@ -339,7 +339,7 @@ impl page::Page<crate::pages::Message> for Page {
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
@ -455,7 +455,7 @@ impl Page {
Message::WireGuardConfig => {
if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
let new_path = path.replace(&filename, &device);
_ = std::fs::rename(&path, &new_path);
match super::nm_add_vpn_file("wireguard", new_path).await {
@ -474,7 +474,7 @@ impl Page {
ConnectionSettings::Vpn(ref settings) => settings,
ConnectionSettings::Wireguard { id } => {
let connection_name = id.clone();
return cosmic::command::future(async move {
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::Error(
ErrorKind::Connect,
@ -501,7 +501,7 @@ impl Page {
_ => {
let connection_name = settings.id.clone();
return cosmic::command::future(async move {
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::Error(
ErrorKind::Connect,
@ -546,7 +546,7 @@ impl Page {
Message::Settings(uuid) => {
self.close_popup_and_apply_updates();
return cosmic::command::future(async move {
return cosmic::task::future(async move {
super::nm_edit_connection(uuid.as_ref())
.then(|res| async move {
match res {
@ -639,7 +639,7 @@ impl Page {
username: String,
password: SecureString,
) -> Task<Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
if let Err(why) = nmcli::set_username(&connection_name, &username).await {
return Message::Error(ErrorKind::WithPassword("username"), why.to_string());
}
@ -866,7 +866,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
Err(why) => Message::Error(ErrorKind::UpdatingState, why.to_string()),
@ -875,7 +875,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard);
@ -938,7 +938,7 @@ fn add_network() -> Task<crate::app::Message> {
}
}
})
.apply(cosmic::command::future)
.apply(cosmic::task::future)
}
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
@ -1040,7 +1040,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
Ok::<_, zbus::Error>(settings)
};
cosmic::command::future(async move {
cosmic::task::future(async move {
settings.await.map_or_else(
|why| Message::Error(ErrorKind::ConnectionSettings, why.to_string()),
Message::KnownConnections,

View file

@ -776,7 +776,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
Ok::<_, zbus::Error>(settings)
};
cosmic::command::future(async move {
cosmic::task::future(async move {
settings
.await
.context("failed to get connection settings")
@ -789,7 +789,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
}
pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
Err(why) => Message::Error(why.to_string()),
@ -798,7 +798,7 @@ pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
}
pub fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);
match network_manager::devices::list(&conn, filter).await {

View file

@ -161,7 +161,7 @@ impl page::Page<crate::pages::Message> for Page {
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
@ -265,7 +265,7 @@ impl Page {
Message::NetworkManager(_event) => (),
Message::AddNetwork => {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
_ = super::nm_add_wired().await;
// TODO: Update when iced is rebased to use then method.
Message::Refresh
@ -332,7 +332,7 @@ impl Page {
Message::Settings(uuid) => {
self.close_popup_and_apply_updates();
return cosmic::command::future(async move {
return cosmic::task::future(async move {
_ = super::nm_edit_connection(uuid.as_ref()).await;
// TODO: Update when iced is rebased to use then method.
Message::Refresh
@ -618,7 +618,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
Err(why) => Message::Error(why.to_string()),
@ -627,7 +627,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
cosmic::command::future(async move {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);

View file

@ -308,7 +308,7 @@ impl Page {
let mut command = None;
if let Some(&node_id) = self.source_ids.get(self.active_source.unwrap_or(0)) {
command = Some(cosmic::command::future(async move {
command = Some(cosmic::task::future(async move {
tokio::time::sleep(Duration::from_millis(64)).await;
crate::pages::Message::Sound(Message::SourceVolumeApply(node_id))
}));
@ -338,7 +338,7 @@ impl Page {
let mut command = None;
if let Some(&node_id) = self.sink_ids.get(self.active_sink.unwrap_or(0)) {
command = Some(cosmic::command::future(async move {
command = Some(cosmic::task::future(async move {
tokio::time::sleep(Duration::from_millis(64)).await;
crate::pages::Message::Sound(Message::SinkVolumeApply(node_id))
}));
@ -542,7 +542,7 @@ impl Page {
.insert(device_id.clone(), Some(profile.clone()));
self.changing_sink_profile = true;
return cosmic::command::future(async move {
return cosmic::task::future(async move {
pactl_set_card_profile(name, profile).await;
Message::SinkProfileSelect(device_id)
})
@ -574,7 +574,7 @@ impl Page {
.insert(device_id.clone(), Some(profile.clone()));
self.changing_source_profile = true;
return cosmic::command::future(async move {
return cosmic::task::future(async move {
pactl_set_card_profile(name, profile).await;
Message::SourceProfileSelect(device_id)
})

View file

@ -0,0 +1,414 @@
// Copyright 2024 System76 <info@system76.com>
// Copyright 2024 bbb651 <bar.ye651@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{BTreeMap, BTreeSet},
path::{Path, PathBuf},
sync::Arc,
};
use cosmic::{
widget::{self, dropdown, icon, settings},
Apply, Element, Task,
};
use cosmic_settings_page::{self as page, section, Section};
use mime_apps::App;
use slotmap::SlotMap;
use tokio::sync::mpsc;
const DROPDOWN_WEB_BROWSER: usize = 0;
const DROPDOWN_FILE_MANAGER: usize = 1;
const DROPDOWN_MAIL: usize = 2;
const DROPDOWN_MUSIC: usize = 3;
const DROPDOWN_VIDEO: usize = 4;
const DROPDOWN_PHOTO: usize = 5;
const DROPDOWN_CALENDAR: usize = 6;
// const DROPDOWN_TERMINAL: usize = 7;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum Category {
Audio,
Calendar,
FileManager,
Image,
Mail,
Mime(&'static str),
// Terminal,
Video,
WebBrowser,
}
#[derive(Clone, Debug)]
pub enum Message {
SetDefault(Category, usize),
Update(CachedMimeApps),
}
impl From<Message> for crate::app::Message {
fn from(message: Message) -> Self {
crate::pages::Message::DefaultApps(message).into()
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::DefaultApps(message)
}
}
#[derive(Clone, Debug)]
pub struct CachedMimeApps {
pub list: mime_apps::List,
pub local_list: mime_apps::List,
pub apps: Vec<AppMeta>,
pub known_mimes: BTreeSet<mime::Mime>,
pub config_path: Box<Path>,
}
#[derive(Clone, Debug)]
pub struct AppMeta {
selected: Option<usize>,
app_ids: Vec<String>,
apps: Vec<String>,
icons: Vec<icon::Handle>,
}
#[derive(Clone, Debug, Default)]
pub struct Page {
on_enter_handle: Option<cosmic::iced::task::Handle>,
mime_apps: Option<CachedMimeApps>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<cosmic_settings_page::Content> {
Some(vec![sections.insert(apps())])
}
fn info(&self) -> page::Info {
page::Info::new("default-apps", "application-default-symbolic")
.title(fl!("default-apps"))
.description(fl!("default-apps", "desc"))
}
fn on_enter(
&mut self,
_sender: mpsc::Sender<crate::pages::Message>,
) -> Task<crate::pages::Message> {
let (task, on_enter_handle) = Task::future(async move {
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());
let mut local_list = mime_apps::List::default();
if let Some(path) = mime_apps::local_list_path() {
if let Ok(buffer) = std::fs::read_to_string(&path) {
local_list.load_from(&buffer);
}
}
let assocs = mime_apps::associations::by_app();
let apps = vec![
load_defaults(&assocs, "x-scheme-handler/http").await,
load_defaults(&assocs, "inode/directory").await,
load_defaults(&assocs, "x-scheme-handler/mailto").await,
load_defaults(&assocs, "audio/mp3").await,
load_defaults(&assocs, "video/mp4").await,
load_defaults(&assocs, "image/png").await,
load_defaults(&assocs, "text/calendar").await,
AppMeta {
selected: None,
app_ids: Vec::new(),
apps: Vec::new(),
icons: Vec::new(),
},
];
Message::Update(CachedMimeApps {
apps,
list,
local_list,
known_mimes: mime_apps::mime_info::mime_types(),
config_path: dirs::config_dir()
.expect("config dir not found")
.join("mimeapps.list")
.into(),
})
.into()
})
.abortable();
self.on_enter_handle = Some(on_enter_handle);
task
}
fn on_leave(&mut self) -> Task<crate::pages::Message> {
if let Some(handle) = self.on_enter_handle.take() {
handle.abort();
}
self.mime_apps = None;
Task::none()
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Task<crate::Message> {
match message {
Message::SetDefault(category, id) => {
let Some(mime_apps) = self.mime_apps.as_mut() else {
return Task::none();
};
let mime_types: Vec<&str>;
let (category_id, mime_types): (usize, &[&str]) = match category {
Category::Audio => (DROPDOWN_MUSIC, {
mime_types = mime_apps
.known_mimes
.iter()
.map(|m| m.essence_str())
.filter(|m| m.starts_with("audio"))
.chain(
[
"application/ogg",
"application/x-cue",
"application/x-ogg",
"x-content/audio-cdda",
]
.into_iter(),
)
.collect();
&mime_types
}),
Category::Calendar => (DROPDOWN_CALENDAR, &["text/calendar"]),
Category::FileManager => (DROPDOWN_FILE_MANAGER, &["inode/directory"]),
Category::Image => (DROPDOWN_PHOTO, {
mime_types = mime_apps
.known_mimes
.iter()
.map(|m| m.essence_str())
.filter(|m| m.starts_with("image"))
.collect();
&mime_types
}),
Category::Mail => (DROPDOWN_MAIL, &["x-scheme-handler/mailto"]),
// Category::Terminal => (DROPDOWN_TERMINAL, &[]),
Category::Video => (DROPDOWN_VIDEO, {
mime_types = mime_apps
.known_mimes
.iter()
.map(|m| m.essence_str())
.filter(|m| m.starts_with("video"))
.collect();
&mime_types
}),
Category::WebBrowser => (
DROPDOWN_WEB_BROWSER,
&[
"text/html",
"application/xhtml+xml",
"x-scheme-handler/chrome",
"x-scheme-handler/http",
"x-scheme-handler/https",
],
),
Category::Mime(_mime_type) => return Task::none(),
};
let meta = &mut mime_apps.apps[category_id];
if meta.selected != Some(id) {
meta.selected = Some(id);
let appid = &meta.app_ids[id];
for mime in mime_types {
if let Ok(mime) = mime.parse() {
mime_apps
.local_list
.set_default_app(mime, [appid, ".desktop"].concat());
};
}
let mut buffer = mime_apps.local_list.to_string();
buffer.push('\n');
_ = std::fs::write(&mime_apps.config_path, buffer);
_ = std::process::Command::new("update-desktop-database").status();
}
}
Message::Update(mime_apps) => {
self.mime_apps = Some(mime_apps);
}
}
Task::none()
}
}
fn apps() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |_binder, page, section| {
let Some(mime_apps) = page.mime_apps.as_ref() else {
return widget::row().into();
};
settings::section()
.title(&section.title)
.add({
let meta = &mime_apps.apps[DROPDOWN_WEB_BROWSER];
settings::flex_item(
fl!("default-apps", "web-browser"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::WebBrowser, id)
})
.icons(&meta.icons),
)
.min_item_width(300.0)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_FILE_MANAGER];
settings::flex_item(
fl!("default-apps", "file-manager"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::FileManager, id)
})
.icons(&meta.icons),
)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_MAIL];
settings::flex_item(
fl!("default-apps", "mail-client"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::Mail, id)
})
.icons(&meta.icons),
)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_MUSIC];
settings::flex_item(
fl!("default-apps", "music"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::Audio, id)
})
.icons(&meta.icons),
)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_VIDEO];
settings::flex_item(
fl!("default-apps", "video"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::Video, id)
})
.icons(&meta.icons),
)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_PHOTO];
settings::flex_item(
fl!("default-apps", "photos"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::Image, id)
})
.icons(&meta.icons),
)
})
.add({
let meta = &mime_apps.apps[DROPDOWN_CALENDAR];
settings::flex_item(
fl!("default-apps", "calendar"),
dropdown(&meta.apps, meta.selected, |id| {
Message::SetDefault(Category::Calendar, id)
})
.icons(&meta.icons),
)
})
// TODO: Decide on a mechanism for getting and setting the default terminal.
// .add({
// let meta = &mime_apps.apps[DROPDOWN_TERMINAL];
// settings::flex_item(
// fl!("default-apps", "terminal"),
// dropdown(&meta.apps, meta.selected, |id| {
// Message::SetDefault(Category::Terminal, id)
// })
// .icons(&meta.icons),
// )
// })
.apply(Element::from)
.map(crate::pages::Message::DefaultApps)
})
}
async fn load_defaults(assocs: &BTreeMap<Arc<str>, Arc<App>>, for_mime: &str) -> AppMeta {
let Ok(mime) = for_mime.parse() else {
return AppMeta {
selected: None,
app_ids: Vec::new(),
apps: Vec::new(),
icons: Vec::new(),
};
};
let current_app_entry = xdg_mime_query_default(for_mime).await;
let current_appid = current_app_entry
.as_ref()
.and_then(|entry| entry.strip_suffix(".desktop"));
let current_app = current_appid.and_then(|appid| assocs.get(appid));
let mut unsorted = mime_apps::apps_for_mime(&mime, assocs).collect::<Vec<_>>();
unsorted.sort_unstable_by_key(|(_, app)| &app.name);
let mut selected = None;
let mut app_ids = Vec::new();
let mut apps = Vec::new();
let mut icons = Vec::new();
for (id, (appid, app)) in unsorted.iter().enumerate() {
if let Some(current_app) = current_app {
if app.name.as_ref() == current_app.name.as_ref() {
selected = Some(id);
}
}
app_ids.push(appid.as_ref().into());
apps.push(app.name.as_ref().into());
icons.push(if app.icon.starts_with('/') {
icon::from_path(PathBuf::from(app.icon.as_ref()))
} else {
icon::from_name(app.icon.as_ref()).size(20).handle()
});
}
AppMeta {
selected,
app_ids,
apps,
icons,
}
}
async fn xdg_mime_query_default(mime_type: &str) -> Option<String> {
let output = tokio::process::Command::new("xdg-mime")
.args(&["query", "default", mime_type])
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|string| string.trim().to_owned())
}

View file

@ -3,6 +3,8 @@
#[cfg(feature = "page-about")]
pub mod about;
#[cfg(feature = "page-default-apps")]
pub mod default_apps;
pub mod firmware;
pub mod users;
@ -33,6 +35,10 @@ impl page::AutoBind<crate::pages::Message> for Page {
page = page.sub_page::<about::Page>();
}
page = page.sub_page::<firmware::Page>();
#[cfg(feature = "page-default-apps")]
{
page = page.sub_page::<default_apps::Page>();
}
page
}
}

View file

@ -183,7 +183,7 @@ impl Page {
Message::TimezoneContext => {
self.timezone_search.clear();
self.timezone_context = true;
return cosmic::command::message(crate::app::Message::OpenContextDrawer(
return cosmic::task::message(crate::app::Message::OpenContextDrawer(
self.entity,
fl!("time-zone").into(),
));
@ -262,14 +262,14 @@ impl Page {
Message::Error(why) => {
tracing::error!(why, "failed to set timezone");
self.timezone_context = false;
return cosmic::command::message(crate::Message::CloseContextDrawer);
return cosmic::task::message(crate::Message::CloseContextDrawer);
}
Message::UpdateTime => {
self.set_ntp(true);
self.update_local_time();
self.timezone_context = false;
return cosmic::command::message(crate::Message::CloseContextDrawer);
return cosmic::task::message(crate::Message::CloseContextDrawer);
}
Message::Refresh(info) => {

View file

@ -139,7 +139,7 @@ impl page::Page<crate::pages::Message> for Page {
&mut self,
_sender: mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
cosmic::command::future(async move { Message::Refresh(Arc::new(page_reload().await)) })
cosmic::task::future(async move { Message::Refresh(Arc::new(page_reload().await)) })
}
fn context_drawer(&self) -> Option<Element<'_, crate::pages::Message>> {
@ -175,7 +175,7 @@ impl Page {
let lang = language.lang_code.clone();
let region = region.lang_code.clone();
commands.push(cosmic::command::future(async move {
commands.push(cosmic::task::future(async move {
_ = set_locale(lang, region).await;
Message::Refresh(Arc::new(page_reload().await))
}));
@ -206,7 +206,7 @@ impl Page {
}
Message::InstallAdditionalLanguages => {
return cosmic::command::future(async move {
return cosmic::task::future(async move {
_ = tokio::process::Command::new("gnome-language-selector")
.status()
.await;