feat: refactor the settings page architecture

This commit is contained in:
Michael Aaron Murphy 2023-04-25 00:30:50 +02:00
parent efdd934e62
commit c015ad9948
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
55 changed files with 2212 additions and 1635 deletions

895
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,45 +1,9 @@
[package]
name = "cosmic-settings"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
rust-version = "1.65.0"
[workspace]
members = ["app", "page", "pages/*"]
default-members = ["app"]
[dependencies]
apply = "0.3.0"
async-channel = "1.8.0"
bytecheck = "0.6.9"
color-eyre = "0.6.2"
derive_setters = "0.1.5"
dirs = "4.0.0"
generator = "0.7.2"
i18n-embed-fl = "0.6.5"
once_cell = "1.17.0"
regex = "1.7.1"
rkyv = { version = "0.7.39", features = ["validation"]}
rust-embed = "6.4.2"
slotmap = "1.0.6"
tokio = "1.25.0"
[dependencies.cosmic-settings-system]
path = "pages/system"
[dependencies.i18n-embed]
version = "0.13.8"
features = ["fluent-system", "desktop-requester"]
[dependencies.libcosmic]
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
rev = "843919e44f0a00c33c29358359be5b4bfa41ab00"
rev = "8232e1d249a467673dbc5b0aa2f2e1665fb18dde"
default-features = false
features = ["debug", "winit", "dyrend", "tokio"]
[profile.dev]
opt-level = "s"
incremental = true
[profile.release]
opt-level = "s"
overflow-checks = true
lto = "thin"
incremental = false

View file

@ -41,13 +41,10 @@ Translation files may be found in the [i18n directory](./i18n). New translations
Licensed under the [GNU Public License 3.0](https://choosealicense.com/licenses/gpl-3.0).
### Contribution
Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the GNU Public License 3.0 (GPL-3.0). Each source file should have a SPDX copyright notice at the top of the file:
```
// Copyright {year-created} System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
```

37
app/Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "cosmic-settings"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
rust-version = "1.65.0"
[dependencies]
apply = "0.3.0"
async-channel = "1.8.0"
bytecheck = "0.6.10"
color-eyre = "0.6.2"
cosmic-settings-page = { path = "../page" }
cosmic-settings-system = { path = "../pages/system" }
cosmic-settings-time = { path = "../pages/time" }
derive_setters = "0.1.5"
dirs = "4.0.0"
generator = "0.7.4"
i18n-embed-fl = "0.6.6"
libcosmic = {workspace = true}
once_cell = "1.17.1"
regex = "1.8.1"
rkyv = { version = "0.7.41", features = ["validation"]}
rust-embed = "6.6.1"
slotmap = "1.0.6"
tokio = "1.27.0"
downcast-rs = "1.2.0"
[dependencies.i18n-embed]
version = "0.13.8"
features = ["fluent-system", "desktop-requester"]
[profile.release]
opt-level = "s"
overflow-checks = true
lto = "thin"
incremental = false

0
app/README.md Normal file
View file

View file

@ -1,4 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"
assets_dir = "../i18n"

View file

@ -3,6 +3,8 @@
use apply::Apply;
use cosmic_settings_page::{self as page, section};
use cosmic::{
iced::widget::{self, column, container, horizontal_space, row},
iced::{self, Application, Command, Length, Subscription},
@ -19,7 +21,7 @@ use cosmic::{
use crate::{
config::{self, Config},
page::{self, desktop, section, sound, system, time, Page},
pages::{desktop, sound, system, time},
widget::{page_title, parent_page_button, search_header, sub_page_button},
};
@ -34,7 +36,7 @@ pub struct SettingsApp {
pub nav_bar_toggled_condensed: bool,
pub nav_bar_toggled: bool,
pub nav_bar: segmented_button::SingleSelectModel,
pub pages: page::Model,
pub pages: page::Binder<crate::pages::Message>,
pub scaling_factor: f32,
pub search: search::Model,
pub search_selections: Vec<(page::Entity, section::Entity)>,
@ -48,10 +50,7 @@ pub struct SettingsApp {
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub enum Message {
About(system::about::Message),
Close,
DateAndTime(time::date::Message),
Desktop(desktop::Message),
Drag,
KeyboardNav(keyboard_nav::Message),
Maximize,
@ -59,6 +58,7 @@ pub enum Message {
NavBar(segmented_button::Entity),
None,
Page(page::Entity),
PageMessage(crate::pages::Message),
Search(search::Message),
ToggleNavBar,
ToggleNavBarCondensed,
@ -71,7 +71,7 @@ impl Application for SettingsApp {
type Message = Message;
type Theme = Theme;
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
fn new(_: Self::Flags) -> (Self, Command<Self::Message>) {
let mut config_path = config::PathManager::new();
let mut app = SettingsApp {
@ -83,7 +83,7 @@ impl Application for SettingsApp {
nav_bar: segmented_button::Model::default(),
nav_bar_toggled: true,
nav_bar_toggled_condensed: false,
pages: page::Model::default(),
pages: page::Binder::default(),
title: crate::fl!("app"),
scaling_factor: std::env::var("COSMIC_SCALE")
.ok()
@ -101,7 +101,7 @@ impl Application for SettingsApp {
// app.insert_page::<networking::Page>();
// app.insert_page::<bluetooth::Page>();
app.insert_page::<desktop::Page>();
let desktop_id = app.insert_page::<desktop::Page>().id();
// app.insert_page::<input::Page>();
@ -119,14 +119,14 @@ impl Application for SettingsApp {
// app.insert_page::<accessibility::Page>();
// app.insert_page::<applications::Page>();
let mut command = Command::none();
let active_id = app
.pages
.info
.iter()
.find(|(_id, info)| info.id == *app.config.active_page)
.map_or(desktop_id, |(id, _info)| id);
for (id, info) in app.pages.pages.iter() {
if info.id == &*app.config.active_page {
command = app.activate_page(id);
break;
}
}
let command = app.activate_page(active_id);
(app, command)
}
@ -195,21 +195,26 @@ impl Application for SettingsApp {
self.search_clear();
}
Message::None | Message::Search(_) => {}
Message::About(message) => {
if let Some(model) = self.pages.resource_mut::<system::about::Model>() {
model.update(message);
Message::PageMessage(message) => match message {
crate::pages::Message::About(message) => {
if let Some(page) = self.pages.page_mut::<system::about::Page>() {
page.update(message);
}
}
}
Message::Desktop(message) => {
if let Some(model) = self.pages.resource_mut::<desktop::Model>() {
model.update(message);
crate::pages::Message::DateAndTime(message) => {
if let Some(page) = self.pages.page_mut::<time::date::Page>() {
page.update(message);
}
}
}
Message::DateAndTime(message) => {
if let Some(model) = self.pages.resource_mut::<time::date::Model>() {
model.update(message);
crate::pages::Message::Desktop(message) => {
if let Some(page) = self.pages.page_mut::<desktop::Page>() {
page.update(message);
}
}
}
crate::pages::Message::External { .. } => {
todo!("external plugins not supported yet");
}
},
}
ret
}
@ -309,7 +314,7 @@ impl SettingsApp {
self.active_page = page;
if current_page != page {
self.config.active_page = Box::from(self.pages.pages[page].id);
self.config.active_page = Box::from(&*self.pages.info[page].id);
self.config_path
.config("main", |path| self.config.serialize(path));
}
@ -318,12 +323,15 @@ impl SettingsApp {
self.search.state = search::State::Inactive;
self.activate_navbar(page);
self.pages.init_page(page).unwrap_or(Command::none())
self.pages
.page_reload(page)
.unwrap_or(Command::none())
.map(Message::PageMessage)
}
/// Activates the navbar item associated with a page.
fn activate_navbar(&mut self, mut page: page::Entity) {
if let Some(parent) = self.pages.pages[page].parent {
if let Some(parent) = self.pages.info[page].parent {
page = parent;
}
@ -333,7 +341,9 @@ impl SettingsApp {
}
/// Adds a main page to the settings application.
fn insert_page<P: Page>(&mut self) -> page::Insert {
fn insert_page<P: page::AutoBind<crate::pages::Message>>(
&mut self,
) -> page::Insert<crate::pages::Message> {
let id = self.pages.register::<P>().id();
self.navbar_insert(id);
@ -344,25 +354,24 @@ impl SettingsApp {
}
fn navbar_insert(&mut self, id: page::Entity) -> segmented_button::SingleSelectEntityMut {
let page = &self.pages.pages[id];
let page = &self.pages.info[id];
self.nav_bar
.insert()
.text(page.title.clone())
.icon(IconSource::from(page.icon_name))
.icon(IconSource::from(page.icon_name.clone()))
.data(id)
.with_id(|nav_id| self.pages.data_set(id, nav_id))
}
/// Displays the view of a page.
fn page_view(&self, content: &[section::Entity]) -> cosmic::Element<Message> {
let page = &self.pages.pages[self.active_page];
let page = &self.pages.info[self.active_page];
let mut column_widgets = Vec::with_capacity(1);
if let Some(parent) = page.parent {
column_widgets.push(parent_page_button(
&self.pages.pages[parent],
&self.pages.info[parent],
page,
Message::Page(parent),
));
@ -371,7 +380,11 @@ impl SettingsApp {
column_widgets.reserve_exact(1 + content.len());
for id in content.iter().copied() {
let section = &self.pages.sections[id];
column_widgets.push((section.view_fn)(self, section));
let model = &self.pages.page[self.active_page];
column_widgets.push(
(section.view_fn)(&self.pages, model.as_ref(), section).map(Message::PageMessage),
);
}
settings::view_column(column_widgets).into()
@ -418,13 +431,15 @@ impl SettingsApp {
let mut current_page = page::Entity::default();
for (page, section) in self.search_selections.iter().copied() {
let section = &self.pages.sections[section];
let model = &self.pages.page[page];
if page != current_page {
current_page = page;
sections.push(search_header(&self.pages, page));
}
let section = (section.view_fn)(self, section)
let section = (section.view_fn)(&self.pages, model.as_ref(), section)
.map(Message::PageMessage)
.apply(iced::widget::container)
.padding([0, 0, 0, 48]);
@ -436,13 +451,13 @@ impl SettingsApp {
/// Displays the sub-pages view of a page.
fn sub_page_view(&self, sub_pages: &[page::Entity]) -> cosmic::Element<Message> {
let page = &self.pages.pages[self.active_page];
let page = &self.pages.info[self.active_page];
let mut column_widgets = Vec::with_capacity(sub_pages.len());
column_widgets.push(page_title(page));
for entity in sub_pages.iter().copied() {
let sub_page = &self.pages.pages[entity];
let sub_page = &self.pages.info[entity];
column_widgets.push(sub_page_button(entity, sub_page));
}

View file

@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "i18n/"]
#[folder = "../i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {

View file

@ -16,9 +16,9 @@ pub mod localize;
pub mod widget;
pub mod page;
pub mod pages;
use cosmic::{iced::Application, settings};
use cosmic::iced::Application;
use i18n_embed::DesktopLanguageRequester;
/// # Errors
@ -35,11 +35,11 @@ pub fn main() -> color_eyre::Result<()> {
let requested_languages = DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("error while loading fluent localizations: {}", error);
eprintln!("error while loading fluent localizations: {error}");
}
settings::set_default_icon_theme("Pop");
let mut settings = settings();
cosmic::settings::set_default_icon_theme("Pop");
let mut settings = cosmic::settings();
settings.window.min_size = Some((600, 300));
SettingsApp::run(settings)?;

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("appearance", "preferences-pop-desktop-appearance-symbolic")
.title(fl!("appearance"))
.description(fl!("appearance", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("dock", "preferences-pop-desktop-dock-symbolic")
.title(fl!("dock"))
.description(fl!("dock", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -8,18 +8,28 @@ pub mod options;
pub mod wallpaper;
pub mod workspaces;
use crate::page;
use cosmic_settings_page as page;
pub struct Page;
#[derive(Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct Page {
pub top_left_hot_corner: bool,
pub show_workspaces_button: bool,
pub show_applications_button: bool,
pub show_minimize_button: bool,
pub show_maximize_button: bool,
pub slideshow: bool,
pub same_background: bool,
}
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("desktop", "video-display-symbolic").title(fl!("desktop"))
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("desktop", "video-display-symbolic").title(fl!("desktop"))
}
}
fn sub_pages(page: page::Insert) -> page::Insert {
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<options::Page>()
.sub_page::<wallpaper::Page>()
.sub_page::<appearance::Page>()
@ -40,19 +50,7 @@ pub enum Message {
TopLeftHotCorner(bool),
}
#[derive(Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct Model {
pub top_left_hot_corner: bool,
pub show_workspaces_button: bool,
pub show_applications_button: bool,
pub show_minimize_button: bool,
pub show_maximize_button: bool,
pub slideshow: bool,
pub same_background: bool,
}
impl Model {
impl Page {
pub fn update(&mut self, message: Message) {
match message {
Message::SameBackground(value) => self.same_background = value,
@ -66,20 +64,20 @@ impl Model {
}
}
// impl From<Page> for Message {
// fn from(page: Page) -> Message {
// Message::Page(page)
// impl From<page::Info> for Message {
// fn from(page: page::Info) -> Message {
// Message::page::Info(page)
// }
// }
// pub enum Output {
// Page(Page),
// page::Info(page::Info),
// }
// impl SubPage for DesktopPage {
// impl Subpage::Info for Desktoppage::Info {
// //TODO: translate
// fn title(&self) -> &'static str {
// use DesktopPage::*;
// use Desktoppage::Info::*;
// match self {
// Workspaces => "Workspaces",
// Notifications => "Notifications",
@ -88,7 +86,7 @@ impl Model {
// //TODO: translate
// fn description(&self) -> &'static str {
// use DesktopPage::*;
// use Desktoppage::Info::*;
// match self {
// Workspaces => "Set workspace number, behavior, and placement.",
// Notifications => {
@ -98,18 +96,18 @@ impl Model {
// }
// fn icon_name(&self) -> &'static str {
// use DesktopPage::*;
// use Desktoppage::Info::*;
// match self {
// Workspaces => "preferences-pop-desktop-workspaces-symbolic",
// Notifications => "preferences-system-notifications-symbolic",
// }
// }
// fn parent_page(&self) -> Page {
// Page::Desktop(None)
// fn parent_page(&self) -> page::Info {
// page::Info::Desktop(None)
// }
// fn into_page(self) -> Page {
// Page::Desktop(Some(self))
// fn into_page(self) -> page::Info {
// page::Info::Desktop(Some(self))
// }
// }

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("notifications", "preferences-system-notifications-symbolic")
.title(fl!("notifications"))
.description(fl!("notifications", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -10,23 +10,19 @@ use cosmic::{
Element,
};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
#[derive(Default)]
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("desktop-options", "video-display-symbolic")
.title(fl!("desktop-options"))
.description(fl!("desktop-options", "desc"))
}
impl page::Page<crate::pages::Message> for Page {
#[allow(clippy::too_many_lines)]
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(super_key_action()),
sections.insert(hot_corner()),
@ -34,17 +30,25 @@ impl page::Page for Page {
sections.insert(window_controls()),
])
}
fn info(&self) -> page::Info {
page::Info::new("desktop-options", "video-display-symbolic")
.title(fl!("desktop-options"))
.description(fl!("desktop-options", "desc"))
}
}
pub fn hot_corner() -> Section {
Section::new()
impl page::AutoBind<crate::pages::Message> for Page {}
pub fn hot_corner() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("hot-corner"))
.descriptions(vec![fl!("hot-corner", "top-left-corner")])
.view_fn(|app, section| {
let desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|binder, _page, section| {
let desktop = binder
.page::<super::Page>()
.expect("desktop page not found");
let descriptions = &section.descriptions;
settings::view_section(&section.title)
.add(settings::item(
@ -54,24 +58,19 @@ pub fn hot_corner() -> Section {
}),
))
.apply(Element::from)
.map(crate::Message::Desktop)
.map(crate::pages::Message::Desktop)
})
}
pub fn super_key_action() -> Section {
Section::new()
pub fn super_key_action() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("super-key-action"))
.descriptions(vec![
fl!("super-key-action", "launcher"),
fl!("super-key-action", "workspaces"),
fl!("super-key-action", "applications"),
])
.view_fn(|app, section| {
let _desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|_binder, _page, section| {
let descriptions = &section.descriptions;
settings::view_section(&section.title)
@ -91,18 +90,17 @@ pub fn super_key_action() -> Section {
})
}
pub fn top_panel() -> Section {
Section::new()
pub fn top_panel() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("top-panel"))
.descriptions(vec![
fl!("top-panel", "workspaces"),
fl!("top-panel", "applications"),
])
.view_fn(|app, section| {
let desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|binder, _page, section| {
let desktop = binder
.page::<super::Page>()
.expect("desktop page not found");
let descriptions = &section.descriptions;
settings::view_section(&section.title)
@ -123,22 +121,21 @@ pub fn top_panel() -> Section {
),
))
.apply(Element::from)
.map(crate::Message::Desktop)
.map(crate::pages::Message::Desktop)
})
}
pub fn window_controls() -> Section {
Section::new()
pub fn window_controls() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("window-controls"))
.descriptions(vec![
fl!("window-controls", "minimize"),
fl!("window-controls", "maximize"),
])
.view_fn(|app, section| {
let desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|binder, _page, section| {
let desktop = binder
.page::<super::Page>()
.expect("desktop page not found");
let descriptions = &section.descriptions;
settings::view_section(&section.title)
@ -159,6 +156,6 @@ pub fn window_controls() -> Section {
),
))
.apply(Element::from)
.map(crate::Message::Desktop)
.map(crate::pages::Message::Desktop)
})
}

View file

@ -10,40 +10,43 @@ use cosmic::{
widget::{list_column, settings, toggler},
Element,
};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
#[derive(Default)]
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(settings())])
}
fn page() -> page::Meta {
page::Meta::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
fn info(&self) -> page::Info {
page::Info::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
.title(fl!("wallpaper"))
.description(fl!("wallpaper", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(settings())])
}
}
pub fn settings() -> Section {
Section::new()
impl page::AutoBind<crate::pages::Message> for Page {}
pub fn settings() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![
fl!("wallpaper", "same"),
fl!("wallpaper", "fit"),
fl!("wallpaper", "slide"),
fl!("wallpaper", "change"),
])
.view_fn(|app, section| {
.view::<Page>(|binder, _page, section| {
let desktop = binder
.page::<super::Page>()
.expect("desktop page not found");
let descriptions = &section.descriptions;
let desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
let image_paths: Vec<std::path::PathBuf> = Vec::new();
let mut image_column = Vec::with_capacity(image_paths.len() / 4);
@ -90,6 +93,6 @@ pub fn settings() -> Section {
settings::view_column(children)
.padding(0)
.apply(Element::from)
.map(crate::Message::Desktop)
.map(crate::pages::Message::Desktop)
})
}

View file

@ -3,42 +3,41 @@
use cosmic::iced::{widget::horizontal_space, Length};
use cosmic::widget::settings;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
#[derive(Default)]
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("workspaces", "preferences-pop-desktop-workspaces-symbolic")
.title(fl!("workspaces"))
.description(fl!("workspaces", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(behavior()),
sections.insert(multi_behavior()),
])
}
fn info(&self) -> page::Info {
page::Info::new("workspaces", "preferences-pop-desktop-workspaces-symbolic")
.title(fl!("workspaces"))
.description(fl!("workspaces", "desc"))
}
}
fn behavior() -> Section {
Section::new()
impl page::AutoBind<crate::pages::Message> for Page {}
fn behavior() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("workspaces-behavior"))
.descriptions(vec![
fl!("workspaces-behavior", "dynamic"),
fl!("workspaces-behavior", "fixed"),
])
.view_fn(|app, section| {
let _desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|_binder, _page, section| {
let descriptions = &section.descriptions;
settings::view_section(&section.title)
@ -54,18 +53,14 @@ fn behavior() -> Section {
})
}
fn multi_behavior() -> Section {
Section::new()
fn multi_behavior() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("workspaces-multi-behavior"))
.descriptions(vec![
fl!("workspaces-multi-behavior", "span"),
fl!("workspaces-multi-behavior", "separate"),
])
.view_fn(|app, section| {
let _desktop = app
.pages
.resource::<super::Model>()
.expect("desktop model is missing");
.view::<Page>(|_binder, _page, section| {
let descriptions = &section.descriptions;
settings::view_section(&section.title)
.add(settings::item(

16
app/src/pages/mod.rs Normal file
View file

@ -0,0 +1,16 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod desktop;
pub mod networking;
pub mod sound;
pub mod system;
pub mod time;
#[derive(Clone, Debug)]
pub enum Message {
About(system::about::Message),
DateAndTime(time::date::Message),
Desktop(desktop::Message),
External { id: String, message: Vec<u8> },
}

View file

@ -1,10 +1,10 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page;
use cosmic_settings_page as page;
pub fn page() -> page::Meta {
page::Meta::new("online-accounts", "goa-panel-symbolic")
pub fn info() -> page::Info {
page::Info::new("online-accounts", "goa-panel-symbolic")
.title(fl!("online-accounts"))
.description(fl!("online-accounts", "desc"))
}

View file

@ -1,10 +1,10 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page;
use cosmic_settings_page as page;
pub fn page() -> page::Meta {
page::Meta::new("wired", "network-workgroup-symbolic")
pub fn info() -> page::Info {
page::Info::new("wired", "network-workgroup-symbolic")
.title(fl!("wired"))
.description(fl!("wired", "desc"))
}

View file

@ -1,27 +1,18 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page::{self, Content, Section};
use cosmic::{iced, widget::settings};
use cosmic_settings_page::{self as page, section, Section};
use slotmap::SlotMap;
use super::section;
#[derive(Default)]
pub struct Sound {}
pub struct Page;
impl page::Page for Page {
type Model = Sound;
fn page() -> page::Meta {
page::Meta::new("sound", "multimedia-volume-control-symbolic")
.title(fl!("sound"))
.description(fl!("sound", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(output()),
sections.insert(input()),
@ -29,21 +20,24 @@ impl page::Page for Page {
sections.insert(applications()),
])
}
fn info(&self) -> page::Info {
page::Info::new("sound", "multimedia-volume-control-symbolic")
.title(fl!("sound"))
.description(fl!("sound", "desc"))
}
}
fn alerts() -> Section {
Section::new()
impl page::AutoBind<crate::pages::Message> for Page {}
fn alerts() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("sound-alerts"))
.descriptions(vec![
fl!("sound-alerts", "volume"),
fl!("sound-alerts", "sound"),
])
.view_fn(|app, section| {
let _sound = app
.pages
.resource::<Sound>()
.expect("sound model is missing");
.view::<Page>(|_binder, _page, section| {
settings::view_section(&section.title)
.add(settings::item(
&section.descriptions[0],
@ -57,16 +51,11 @@ fn alerts() -> Section {
})
}
fn applications() -> Section {
Section::new()
fn applications() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("sound-applications"))
.descriptions(vec![fl!("sound-applications", "desc")])
.view_fn(|app, section| {
let _sound = app
.pages
.resource::<Sound>()
.expect("sound model is missing");
.view::<Page>(|_binder, _page, section| {
settings::view_section(&section.title)
.add(settings::item(
&section.descriptions[0],
@ -76,20 +65,15 @@ fn applications() -> Section {
})
}
fn input() -> Section {
Section::new()
fn input() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("sound-input"))
.descriptions(vec![
fl!("sound-input", "volume"),
fl!("sound-input", "device"),
fl!("sound-input", "level"),
])
.view_fn(|app, section| {
let _sound = app
.pages
.resource::<Sound>()
.expect("sound model is missing");
.view::<Page>(|_binder, _page, section| {
settings::view_section(&section.title)
.add(settings::item(
&section.descriptions[0],
@ -107,8 +91,8 @@ fn input() -> Section {
})
}
fn output() -> Section {
Section::new()
fn output() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("sound-output"))
.descriptions(vec![
fl!("sound-output", "volume"),
@ -117,12 +101,7 @@ fn output() -> Section {
fl!("sound-output", "config"),
fl!("sound-output", "balance"),
])
.view_fn(|app, section| {
let _sound = app
.pages
.resource::<Sound>()
.expect("sound model is missing");
.view::<Page>(|_binder, _page, section| {
settings::view_section(&section.title)
.add(settings::item(
&section.descriptions[0],

View file

@ -0,0 +1,158 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::{self as page, section, Section};
use cosmic::{
iced::{
widget::{horizontal_space, row},
Length,
},
widget::{icon, list_column, settings, text},
};
use cosmic_settings_system::about::Info;
use slotmap::SlotMap;
#[derive(Clone, Debug)]
pub enum Message {
Info(Box<Info>),
}
#[derive(Clone, Debug, Default)]
pub struct Page {
info: Info,
// support_page: page::Entity,
}
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<page::Content> {
Some(vec![
sections.insert(distributor_logo()),
sections.insert(device()),
sections.insert(hardware()),
sections.insert(os()),
sections.insert(related()),
])
}
fn info(&self) -> page::Info {
page::Info::new("about", "help-about-symbolic")
.title(fl!("about"))
.description(fl!("about", "desc"))
}
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
Some(Box::pin(async move {
crate::pages::Message::About(Message::Info(Box::new(Info::load())))
}))
}
}
impl Page {
pub fn update(&mut self, message: Message) {
match message {
Message::Info(info) => self.info = *info,
}
}
}
fn device() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![fl!("about-device"), fl!("about-device", "desc")])
.view::<Page>(|_binder, page, section| {
let desc = &section.descriptions;
let device_name = settings::item::builder(&desc[0])
.description(&desc[1])
.control(text(&page.info.device_name));
list_column().add(device_name).into()
})
}
fn distributor_logo() -> Section<crate::pages::Message> {
Section::default()
.search_ignore()
.view::<Page>(|_binder, _page, _section| {
row!(
horizontal_space(Length::Fill),
icon("distributor-logo", 78),
horizontal_space(Length::Fill),
)
// Add extra padding to reach 40px from the first section.
.padding([0, 16, 0, 16])
.into()
})
}
fn hardware() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("about-hardware"))
.descriptions(vec![
fl!("about-hardware", "model"),
fl!("about-hardware", "memory"),
fl!("about-hardware", "processor"),
fl!("about-hardware", "graphics"),
fl!("about-hardware", "disk-capacity"),
])
.view::<Page>(|_binder, page, section| {
let desc = &section.descriptions;
let mut sections = settings::view_section(&section.title)
.add(settings::item(&desc[0], text(&page.info.hardware_model)))
.add(settings::item(&desc[1], text(&page.info.memory)))
.add(settings::item(&desc[2], text(&page.info.processor)));
for card in &page.info.graphics {
sections = sections.add(settings::item(&desc[3], text(card.as_str())));
}
sections
.add(settings::item(&desc[4], text(&page.info.disk_capacity)))
.into()
})
}
fn os() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("about-os"))
.descriptions(vec![
fl!("about-os", "os"),
fl!("about-os", "os-architecture"),
fl!("about-os", "desktop-environment"),
fl!("about-os", "windowing-system"),
])
.view::<Page>(|_binder, page, section| {
let desc = &section.descriptions;
settings::view_section(&section.title)
.add(settings::item(&desc[0], text(&page.info.operating_system)))
.add(settings::item(&desc[1], text(&page.info.os_architecture)))
.add(settings::item(
&desc[2],
text(&page.info.desktop_environment),
))
.add(settings::item(&desc[3], text(&page.info.windowing_system)))
.into()
})
}
fn related() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("about-related"))
.descriptions(vec![fl!("about-related", "support")])
.view::<Page>(|_binder, _page, section| {
settings::view_section(&section.title)
.add(settings::item(&section.descriptions[0], text("TODO")))
.into()
})
}
// fn page(app: &crate::SettingsApp) -> &Page {
// app.pages
// .resource::<Page>()
// .expect("missing system->about page")
// }

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("firmware", "firmware-manager-symbolic")
.title(fl!("firmware"))
.description(fl!("firmware", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -0,0 +1,25 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod about;
pub mod firmware;
pub mod users;
use cosmic_settings_page as page;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("system", "system-users-symbolic").title(fl!("system"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<users::Page>()
.sub_page::<about::Page>()
.sub_page::<firmware::Page>()
}
}

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("users", "system-users-symbolic")
.title(fl!("users"))
.description(fl!("users", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -1,22 +1,49 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page::{self, section, Content, Section};
use apply::Apply;
use cosmic::{
iced::{widget::horizontal_space, Length},
widget::settings,
};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
// use icu::calendar::{DateTime, Gregorian};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Model {
pub struct Page {
auto: bool,
auto_timezone: bool,
military_time: bool,
// info: Option<cosmic_settings_time::Info>,
}
impl Model {
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![
sections.insert(date()),
sections.insert(timezone()),
sections.insert(format()),
])
}
fn info(&self) -> page::Info {
page::Info::new("time-date", "preferences-system-time-symbolic")
.title(fl!("time-date"))
.description(fl!("time-date", "desc"))
}
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
None
}
}
impl Page {
pub fn update(&mut self, message: Message) {
match message {
Message::Automatic(enable) => self.auto = enable,
@ -33,68 +60,40 @@ pub enum Message {
MilitaryTime(bool),
}
pub struct Page;
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::Page for Page {
type Model = Model;
fn page() -> page::Meta {
page::Meta::new("time-date", "preferences-system-time-symbolic")
.title(fl!("time-date"))
.description(fl!("time-date", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![
sections.insert(date()),
sections.insert(timezone()),
sections.insert(format()),
])
}
}
fn date() -> Section {
Section::new()
fn date() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("time-date"))
.descriptions(vec![fl!("time-date", "auto"), fl!("time-date")])
.view_fn(|app, section| {
let model = app
.pages
.resource::<Model>()
.expect("time & date model not found");
.view::<Page>(|_binder, page, section| {
settings::view_section(&section.title)
.add(
settings::item::builder(&section.descriptions[0])
.toggler(model.auto, Message::Automatic),
.toggler(page.auto, Message::Automatic),
)
.add(settings::item(
&section.descriptions[1],
horizontal_space(Length::Fill),
))
.apply(cosmic::Element::from)
.map(crate::Message::DateAndTime)
.map(crate::pages::Message::DateAndTime)
})
}
fn format() -> Section {
Section::new()
fn format() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("time-format"))
.descriptions(vec![
fl!("time-format", "twenty-four"),
fl!("time-format", "first"),
])
.view_fn(|app, section| {
let model = app
.pages
.resource::<Model>()
.expect("time & date model not found");
.view::<Page>(|_binder, page, section| {
settings::view_section(&section.title)
// 24-hour toggle
.add(
settings::item::builder(&section.descriptions[0])
.toggler(model.military_time, Message::MilitaryTime),
.toggler(page.military_time, Message::MilitaryTime),
)
// First day of week
.add(settings::item(
@ -102,30 +101,25 @@ fn format() -> Section {
horizontal_space(Length::Fill),
))
.apply(cosmic::Element::from)
.map(crate::Message::DateAndTime)
.map(crate::pages::Message::DateAndTime)
})
}
fn timezone() -> Section {
Section::new()
fn timezone() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("time-zone"))
.descriptions(vec![
fl!("time-zone", "auto"),
fl!("time-zone", "auto-info"),
fl!("time-zone"),
])
.view_fn(|app, section| {
let model = app
.pages
.resource::<Model>()
.expect("time & date model not found");
.view::<Page>(|_binder, page, section| {
settings::view_section(&section.title)
// Automatic timezone toggle
.add(
settings::item::builder(&section.descriptions[0])
.description(&section.descriptions[1])
.toggler(model.auto_timezone, Message::AutomaticTimezone),
.toggler(page.auto_timezone, Message::AutomaticTimezone),
)
// Time zone select
.add(
@ -133,6 +127,6 @@ fn timezone() -> Section {
.control(horizontal_space(Length::Fill)),
)
.apply(cosmic::Element::from)
.map(crate::Message::DateAndTime)
.map(crate::pages::Message::DateAndTime)
})
}

24
app/src/pages/time/mod.rs Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page as page;
pub mod date;
pub mod region;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> page::Info {
page::Info::new("time", "preferences-system-time-symbolic")
.title(fl!("time"))
.description(fl!("time", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<date::Page>().sub_page::<region::Page>()
}
}

View file

@ -0,0 +1,26 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page;
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("time-region", "preferences-desktop-locale-symbolic")
.title(fl!("time-region"))
.description(fl!("time-region", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -9,17 +9,19 @@ use cosmic::iced::{
};
use cosmic::widget::{divider, icon, list, settings, text};
use cosmic::{theme, Element};
use crate::page::{self, Meta};
use cosmic_settings_page as page;
#[must_use]
pub fn search_header(pages: &page::Model, page: page::Entity) -> cosmic::Element<crate::Message> {
let page_meta = &pages.pages[page];
pub fn search_header<Message>(
pages: &page::Binder<Message>,
page: page::Entity,
) -> cosmic::Element<crate::Message> {
let page_meta = &pages.info[page];
let mut column_children = Vec::with_capacity(4);
if let Some(parent) = page_meta.parent {
let parent_meta = &pages.pages[parent];
let parent_meta = &pages.info[parent];
column_children.push(
text(parent_meta.title.as_str())
@ -52,7 +54,7 @@ pub fn search_page_link<Message: 'static>(title: &str) -> Button<Message, cosmic
}
#[must_use]
pub fn page_title<Message: 'static>(page: &Meta) -> Element<Message> {
pub fn page_title<Message: 'static>(page: &page::Info) -> Element<Message> {
row!(
text(page.title.as_str()).size(32),
horizontal_space(Length::Fill)
@ -62,8 +64,8 @@ pub fn page_title<Message: 'static>(page: &Meta) -> Element<Message> {
#[must_use]
pub fn parent_page_button<'a, Message: Clone + 'static>(
parent: &'a Meta,
sub_page: &'a Meta,
parent: &'a page::Info,
sub_page: &'a page::Info,
on_press: Message,
) -> Element<'a, Message> {
column!(
@ -85,10 +87,10 @@ pub fn parent_page_button<'a, Message: Clone + 'static>(
}
#[must_use]
pub fn sub_page_button(entity: page::Entity, page: &Meta) -> Element<page::Entity> {
pub fn sub_page_button(entity: page::Entity, page: &page::Info) -> Element<page::Entity> {
settings::item::builder(page.title.as_str())
.description(page.description.as_str())
.icon(icon(page.icon_name, 20).style(theme::Svg::Symbolic))
.icon(icon(&*page.icon_name, 20).style(theme::Svg::Symbolic))
.control(row!(
horizontal_space(Length::Fill),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)

View file

@ -15,12 +15,12 @@ rootdir := ''
prefix := '/usr'
# File paths
bin-src := 'target/release/' + name
bin-dest := rootdir + prefix + '/bin/' + name
bin-src := 'target' / 'release' / name
bin-dest := clean(rootdir / prefix) / 'bin' / name
desktop := appid + '.desktop'
desktop-src := 'resources/' + desktop
desktop-dest := rootdir + prefix + '/share/applications/' + desktop
desktop-src := 'resources' / desktop
desktop-dest := clean(rootdir / prefix) / 'share' / 'applications' / desktop
[private]
help:
@ -34,21 +34,21 @@ clean:
clean-dist: clean
rm -rf .cargo vendor vendor.tar target
# Compile debug build of cosmic-settings
# Compile with debug profile
build-debug *args:
cargo build {{args}}
# Compile release build of cosmic-settings
# Compile with release profile
build-release *args: (build-debug '--release' args)
# Vendored release build of cosmic-settings
# Compile with a vendored tarball
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Run `cargo clippy`
# Check for errors and linter warnings
check *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Run `cargo clippy` with json message format
# Runs a check with JSON message format for IDE integration
check-json: (check '--message-format=json')
# Installation command
@ -67,11 +67,11 @@ install: (install-bin bin-src bin-dest) (install-file desktop-src desktop-dest)
# Run the application for testing purposes
run *args:
cargo run {{args}}
env RUST_BACKTRACE=full cargo run --release {{args}}
# Run `cargo test`
test:
cargo test --all-features
cargo test
# Uninstalls everything (requires same arguments as given to install)
uninstall:
@ -86,10 +86,9 @@ vendor:
tar pcf vendor.tar vendor
rm -rf vendor
# Extracts vendored dependencies if vendor=1
# Extracts vendored dependencies
[private]
vendor-extract:
#!/usr/bin/env sh
rm -rf vendor
tar pxf vendor.tar

13
page/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "cosmic-settings-page"
version = "0.1.0"
edition = "2021"
[dependencies]
derive_setters = "0.1.5"
regex = "1.8.1"
slotmap = "1.0.6"
libcosmic = { workspace = true }
generator = "0.7.4"
downcast-rs = "1.2.0"
once_cell = "1.17.1"

7
page/README.md Normal file
View file

@ -0,0 +1,7 @@
# cosmic-settings-page
This module contains the APIs for creating and managing settings pages.
- A [Page](./src/lib.rs) implements the `Page` and `AutoBind` traits.
- A [Section](./src/section.rs) is a subset of a page, with a view function to generate the UI.
- The [Binder](./src/binder.rs) holds all of the pages, their sections, and additional metadata associated with them

199
page/src/binder.rs Normal file
View file

@ -0,0 +1,199 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::section::{self, Section};
use crate::{Content, Info, Page};
use cosmic::iced_native::command::{Action, Command};
use regex::Regex;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
collections::HashMap,
};
/// All settings pages are registered and managed by the [`Binder`].
pub struct Binder<Message> {
pub info: SlotMap<crate::Entity, Info>,
pub page: SecondaryMap<crate::Entity, Box<dyn Page<Message>>>,
pub typed_page_ids: HashMap<TypeId, crate::Entity>,
pub resource: HashMap<TypeId, Box<dyn Any>>,
pub storage: HashMap<TypeId, SecondaryMap<crate::Entity, Box<dyn Any>>>,
pub sub_pages: SparseSecondaryMap<crate::Entity, Vec<crate::Entity>>,
pub sections: SlotMap<section::Entity, Section<Message>>,
pub content: SparseSecondaryMap<crate::Entity, Content>,
}
impl<Message> Default for Binder<Message> {
fn default() -> Self {
Self {
content: SparseSecondaryMap::new(),
info: SlotMap::with_key(),
page: SecondaryMap::new(),
typed_page_ids: HashMap::new(),
resource: HashMap::new(),
sections: SlotMap::with_key(),
storage: HashMap::new(),
sub_pages: SparseSecondaryMap::new(),
}
}
}
impl<Message: 'static> Binder<Message> {
/// Check if a page exists in the model.
#[must_use]
pub fn contains_item(&self, id: crate::Entity) -> bool {
self.info.contains_key(id)
}
/// Returns the content of a page, if it has any.
#[must_use]
pub fn content(&self, page: crate::Entity) -> Option<&[section::Entity]> {
self.content.get(page).map(Vec::as_slice)
}
/// Get an immutable reference to data associated with a page.
#[must_use]
pub fn data<Data: 'static>(&self, id: crate::Entity) -> Option<&Data> {
self.storage
.get(&TypeId::of::<Data>())
.and_then(|storage| storage.get(id))
.and_then(|data| data.downcast_ref())
}
/// Get a mutable reference to data associated with a page.
pub fn data_mut<Data: 'static>(&mut self, id: crate::Entity) -> Option<&mut Data> {
self.storage
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.get_mut(id))
.and_then(|data| data.downcast_mut())
}
/// Associates data with the item.
pub fn data_set<Data: 'static>(&mut self, id: crate::Entity, data: Data) {
if self.contains_item(id) {
self.storage
.entry(TypeId::of::<Data>())
.or_insert_with(SecondaryMap::new)
.insert(id, Box::new(data));
}
}
/// Removes a specific data type from the item.
pub fn data_remove<Data: 'static>(&mut self, id: crate::Entity) {
self.storage
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.remove(id));
}
/// Registers a new page in the settings panel.
pub fn register<P: AutoBind<Message>>(&mut self) -> crate::Insert<Message> {
let page = P::default();
let id = self.register_page(page);
self.typed_page_ids.insert(TypeId::of::<P>(), id);
P::sub_pages(crate::Insert { id, model: self })
}
pub fn register_page<P: Page<Message>>(&mut self, page: P) -> crate::Entity {
let id = self.info.insert(page.info());
if let Some(content) = page.content(&mut self.sections) {
self.content.insert(id, content);
}
self.page.insert(id, Box::new(page));
id
}
#[must_use]
pub fn model(&self, id: crate::Entity) -> Option<&dyn Page<Message>> {
self.page.get(id).map(AsRef::as_ref)
}
#[must_use]
pub fn model_mut(&mut self, id: crate::Entity) -> Option<&mut dyn Page<Message>> {
self.page.get_mut(id).map(AsMut::as_mut)
}
/// Obtain a reference to a page by its type ID.
#[must_use]
pub fn page<P: Page<Message>>(&self) -> Option<&P> {
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
let page = self.page.get(*id)?;
page.downcast_ref::<P>()
}
/// Obtain a reference to a page by its type ID.
#[must_use]
pub fn page_mut<P: Page<Message>>(&mut self) -> Option<&mut P> {
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
let page = self.page.get_mut(*id)?;
page.downcast_mut::<P>()
}
/// Calls a page's load function to refresh its data.
pub fn page_reload(&mut self, id: crate::Entity) -> Option<Command<Message>> {
if let Some(page) = self.page.get(id) {
if let Some(future) = page.load(id) {
return Some(Command::single(Action::Future(future)));
}
}
None
}
#[must_use]
pub fn resource<Resource: 'static>(&self) -> Option<&Resource> {
self.resource
.get(&TypeId::of::<Resource>())
.and_then(|resource| resource.downcast_ref())
}
#[must_use]
pub fn resource_mut<Resource: 'static>(&mut self) -> Option<&mut Resource> {
self.resource
.get_mut(&TypeId::of::<Resource>())
.and_then(|resource| resource.downcast_mut())
}
#[allow(unused_must_use)]
pub fn resource_register<Resource: Default + 'static>(&mut self) {
self.resource
.entry(TypeId::of::<Resource>())
.or_insert_with(|| Box::<Resource>::default());
}
/// Finds content of panels that match the search.
pub fn search<'a>(
&'a self,
rule: &'a Regex,
) -> impl Iterator<Item = (crate::Entity, section::Entity)> + 'a {
generator::Gn::new_scoped_local(|mut s| {
for (page, sections) in self.content.iter() {
for id in sections {
if self.sections[*id].search_matches(rule) {
s.yield_((page, *id));
}
}
}
generator::done!();
})
}
/// Returns the sub-pages of a page, if it has any.
pub fn sub_pages(&self, page: crate::Entity) -> Option<&[crate::Entity]> {
self.sub_pages.get(page).map(AsRef::as_ref)
}
}
pub trait AutoBind<Message: 'static>: Page<Message> + Default + 'static {
/// Attaches sub-pages to the page.
#[allow(clippy::must_use_candidate)]
fn sub_pages(page: crate::Insert<Message>) -> crate::Insert<Message> {
page
}
}

50
page/src/insert.rs Normal file
View file

@ -0,0 +1,50 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::{AutoBind, Binder, Content, Entity, Info};
/// An inserted page which may have additional properties assigned to it.
pub struct Insert<'a, Message> {
pub model: &'a mut Binder<Message>,
pub id: Entity,
}
impl<'a, Message: 'static> Insert<'a, Message> {
#[must_use]
pub fn id(self) -> Entity {
self.id
}
#[must_use]
pub fn content(self, content: Content) -> Self {
self.model.content.insert(self.id, content);
self
}
/// Adds a page and associates it with its parent page.
#[allow(clippy::return_self_not_must_use)]
#[allow(clippy::must_use_candidate)]
pub fn sub_page<P: AutoBind<Message>>(self) -> Self {
let sub_page = P::default();
let page = self.model.info.insert(Info {
parent: Some(self.id),
..sub_page.info()
});
if let Some(content) = sub_page.content(&mut self.model.sections) {
self.model.content.insert(page, content);
}
self.model.page.insert(page, Box::new(sub_page));
self.model
.sub_pages
.entry(self.id)
.expect("parent page missing")
.and_modify(|v| v.push(page))
.or_insert_with(|| vec![page]);
self
}
}

85
page/src/lib.rs Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
mod binder;
pub use binder::{AutoBind, Binder};
mod insert;
use downcast_rs::{impl_downcast, Downcast};
pub use insert::Insert;
pub mod section;
pub use section::Section;
use derive_setters::Setters;
use slotmap::SlotMap;
use std::{borrow::Cow, future::Future, pin::Pin};
slotmap::new_key_type! {
/// The unique ID of a page.
pub struct Entity;
}
/// A collection of sections which a page may be comprised of.
pub type Content = Vec<section::Entity>;
/// A request by a page to run a command in the background.
pub type Task<Message> = Pin<Box<dyn Future<Output = Message> + Send>>;
pub trait Page<Message: 'static>: Downcast {
/// Information about the page
fn info(&self) -> Info;
#[must_use]
fn content(
&self,
_sections: &mut SlotMap<section::Entity, Section<Message>>,
) -> Option<Content> {
None
}
#[must_use]
#[allow(unused)]
fn load(&self, page: crate::Entity) -> Option<crate::Task<Message>> {
None
}
}
impl_downcast!(Page<Message>);
/// Information about a page; including its title, icon, and description.
#[derive(Setters)]
#[must_use]
pub struct Info {
/// An identifier that is the same between application runs.
#[setters(skip)]
pub id: Cow<'static, str>,
/// The icon associated with the page.
#[setters(skip)]
pub icon_name: Cow<'static, str>,
/// The title of the page.
#[setters(into)]
pub title: String,
/// A description of the page.
#[setters(into)]
pub description: String,
/// The parent of the page.
#[setters(strip_option)]
pub parent: Option<Entity>,
}
impl Info {
pub fn new(id: impl Into<Cow<'static, str>>, icon_name: impl Into<Cow<'static, str>>) -> Self {
Self {
title: String::new(),
icon_name: icon_name.into(),
id: id.into(),
description: String::new(),
parent: None,
}
}
}

102
page/src/section.rs Normal file
View file

@ -0,0 +1,102 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use derive_setters::Setters;
use regex::Regex;
use crate::{Binder, Page};
slotmap::new_key_type! {
/// The unique ID of a section of page.
pub struct Entity;
}
pub type ViewFn<Message> = Box<
dyn for<'a> Fn(
&'a Binder<Message>,
&'a dyn Page<Message>,
&'a Section<Message>,
) -> cosmic::Element<'a, Message>,
>;
/// A searchable sub-component of a page.
///
/// Searches can group multiple sections together.
#[derive(Setters)]
#[must_use]
pub struct Section<Message> {
#[setters(into)]
pub title: String,
pub descriptions: Vec<String>,
#[setters(skip)]
pub view_fn: ViewFn<Message>,
#[setters(bool)]
pub search_ignore: bool,
}
impl<Message: 'static> Default for Section<Message> {
fn default() -> Self {
Self {
title: String::new(),
descriptions: Vec::new(),
view_fn: Box::new(unimplemented),
search_ignore: false,
}
}
}
impl<Message: 'static> Section<Message> {
#[must_use]
pub fn search_matches(&self, rule: &Regex) -> bool {
if self.search_ignore {
return false;
}
if rule.is_match(self.title.as_str()) {
return true;
}
for description in &self.descriptions {
if rule.is_match(description.as_str()) {
return true;
}
}
false
}
/// # Panics
///
/// Will panic if the `Model` type does not match the page type.
pub fn view<Model: Page<Message>>(
mut self,
func: impl for<'a> Fn(
&'a Binder<Message>,
&'a Model,
&'a Section<Message>,
) -> cosmic::Element<'a, Message>
+ 'static,
) -> Self {
self.view_fn = Box::new(move |binder, model: &dyn Page<Message>, section| {
let model = model.downcast_ref::<Model>().unwrap_or_else(|| {
panic!(
"page model type mismatch: expected {}",
std::any::type_name::<Model>()
)
});
func(binder, model, section)
});
self
}
}
#[must_use]
pub fn unimplemented<'a, Message: 'static>(
_binder: &'a Binder<Message>,
_page: &'a dyn Page<Message>,
_section: &'a Section<Message>,
) -> cosmic::Element<'a, Message> {
cosmic::widget::settings::view_column(vec![cosmic::widget::settings::view_section("").into()])
.into()
}

279
pages/system/Cargo.lock generated
View file

@ -1,279 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "byte-unit"
version = "4.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3348673602e04848647fffaa8e9a861e7b5d5cae6570727b41bde0f722514484"
dependencies = [
"utf8-width",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "concat-in-place"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b80dba65d26e0c4b692ad0312b837f1177e8175031af57fd1de4f3bc36b430"
[[package]]
name = "const_format"
version = "0.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cosmic-settings-system"
version = "0.1.0"
dependencies = [
"bumpalo",
"byte-unit",
"concat-in-place",
"const_format",
"memchr",
"sysinfo",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [
"cfg-if",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "ntapi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [
"winapi",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "once_cell"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "proc-macro2"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sysinfo"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975fe381e0ecba475d4acff52466906d95b153a40324956552e027b2a9eaa89e"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"ntapi",
"once_cell",
"rayon",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -2,7 +2,7 @@
name = "cosmic-settings-system"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
license = "GPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,9 +10,9 @@ license = "MPL-2.0"
byte-unit = { version = "4.0.18", default-features = false }
const_format = "0.2.30"
concat-in-place = "1.1.0"
sysinfo = "0.27.7"
sysinfo = "0.28.4"
memchr = "2.5.0"
[dependencies.bumpalo]
version = "3.12.0"
version = "3.12.1"
features = ["collections"]

210
pages/system/src/about.rs Normal file
View file

@ -0,0 +1,210 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use bumpalo::Bump;
use std::{ffi::OsStr, io::Read};
use sysinfo::{DiskExt, SystemExt};
use concat_in_place::strcat;
use const_format::concatcp;
const DMI_DIR: &str = "/sys/devices/virtual/dmi/id/";
const BOARD_NAME: &str = concatcp!(DMI_DIR, "board_name");
const BOARD_VERSION: &str = concatcp!(DMI_DIR, "board_version");
const SYS_VENDOR: &str = concatcp!(DMI_DIR, "sys_vendor");
const VERSION_IGNORING_PRODUCTS: &[&str] = &["Dev One"];
#[must_use]
#[derive(Clone, Debug, Default)]
pub struct Info {
pub desktop_environment: String,
pub device_name: String,
pub disk_capacity: String,
pub graphics: Vec<String>,
pub hardware_model: String,
pub memory: String,
pub operating_system: String,
pub os_architecture: String,
pub processor: String,
pub windowing_system: String,
}
impl Info {
pub fn load() -> Info {
let mut info = Info::default();
let mut bump = Bump::with_capacity(8 * 1024);
architecture(&bump, &mut info.os_architecture);
bump.reset();
hardware_model(&bump, &mut info.hardware_model);
bump.reset();
operating_system(&bump, &mut info.operating_system);
bump.reset();
processor_name(&bump, &mut info.processor);
bump.reset();
let mut sys = sysinfo::System::new();
sys.refresh_disks_list();
sys.refresh_disks();
sys.refresh_memory();
let mut total_capacity = 0;
for disk in sys.disks() {
total_capacity += disk.total_space();
}
info.disk_capacity = format_size(total_capacity);
if let Some(name) = sys.host_name() {
info.device_name = name;
}
info.memory = format_size(sys.total_memory());
if let Ok(mut session) = std::env::var("XDG_SESSION_TYPE") {
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.windowing_system = session;
}
if let Ok(mut session) = std::env::var("DESKTOP_SESSION") {
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.desktop_environment = session;
}
if let Ok(output) = std::process::Command::new("lspci").output() {
if let Ok(stdout) = std::str::from_utf8(&output.stdout) {
for line in stdout.lines() {
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b"VGA") {
let line = &line[pos + 3..];
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b": ") {
info.graphics.push(String::from(&line[pos + 2..]));
}
}
}
}
}
info
}
}
pub fn architecture(bump: &Bump, arch: &mut String) {
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(value) = read_to_string("/proc/sys/kernel/arch", buffer) {
arch.push_str(value.trim());
}
}
pub fn hardware_model(bump: &Bump, hardware_model: &mut String) {
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut sys_vendor) = read_to_string(SYS_VENDOR, buffer) {
sys_vendor = sys_vendor.trim();
hardware_model.push_str(sys_vendor);
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut name) = read_to_string(BOARD_NAME, buffer) {
name = name.trim();
if !name.is_empty() && name != sys_vendor {
// Ensure that the name does not contain the vendor.
name = match name.strip_prefix(sys_vendor) {
Some(stripped) => stripped.trim(),
None => name,
};
strcat!(&mut *hardware_model, " " name);
}
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut version) = read_to_string(BOARD_VERSION, buffer) {
version = version.trim();
if !version.is_empty() && !VERSION_IGNORING_PRODUCTS.contains(&name) {
strcat!(hardware_model, " (" version.trim() ")");
}
}
}
}
}
pub fn operating_system(bump: &Bump, operating_system: &mut String) {
let mut buffer = bumpalo::collections::Vec::new_in(bump);
let Some(os_release) = read_to_string("/etc/os-release", &mut buffer) else {
return;
};
for line in os_release.lines() {
if let Some(mut value) = line.strip_prefix("PRETTY_NAME=") {
if let Some(v) = value.strip_prefix('"') {
value = v;
}
if let Some(v) = value.strip_suffix('"') {
value = v;
}
operating_system.push_str(value.trim());
break;
}
}
}
pub fn processor_name(bump: &Bump, name: &mut String) {
if let Some(cpuinfo) = read_to_string(
"/proc/cpuinfo",
&mut bumpalo::collections::Vec::new_in(bump),
) {
for line in cpuinfo.lines() {
if let Some(info) = line.strip_prefix("model name") {
if let Some(info) = info.trim_start().strip_prefix(':') {
name.push_str(info.trim());
}
break;
}
}
}
}
pub fn read_to_string<'a, P: AsRef<OsStr>>(
path: P,
buffer: &'a mut bumpalo::collections::Vec<u8>,
) -> Option<&'a str> {
let mut file = std::fs::File::open(path.as_ref()).ok()?;
if let Ok(metadata) = file.metadata() {
if let Ok(len) = usize::try_from(metadata.len()) {
buffer.reserve_exact(len);
}
}
let mut buf = [0; 16 * 1024];
loop {
match file.read(&mut buf) {
Ok(0) => break,
Ok(read) => buffer.extend_from_slice(&buf[..read]),
Err(_) => return None,
}
}
std::str::from_utf8(buffer.as_slice()).ok()
}
fn format_size(size: u64) -> String {
byte_unit::Byte::from_bytes(size)
.get_appropriate_unit(true)
.to_string()
}

View file

@ -1,208 +1,4 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
// SPDX-License-Identifier: GPL-3.0-only
use bumpalo::Bump;
use std::{ffi::OsStr, io::Read};
use sysinfo::{DiskExt, SystemExt};
use concat_in_place::strcat;
use const_format::concatcp;
const DMI_DIR: &str = "/sys/devices/virtual/dmi/id/";
const BOARD_NAME: &str = concatcp!(DMI_DIR, "board_name");
const BOARD_VERSION: &str = concatcp!(DMI_DIR, "board_version");
const SYS_VENDOR: &str = concatcp!(DMI_DIR, "sys_vendor");
#[derive(Clone, Debug, Default)]
pub struct Info {
pub desktop_environment: String,
pub device_name: String,
pub disk_capacity: String,
pub graphics: Vec<String>,
pub hardware_model: String,
pub memory: String,
pub operating_system: String,
pub os_architecture: String,
pub processor: String,
pub windowing_system: String,
}
impl Info {
pub fn load() -> Info {
let mut info = Info::default();
let mut bump = Bump::with_capacity(8 * 1024);
architecture(&bump, &mut info.os_architecture);
bump.reset();
hardware_model(&bump, &mut info.hardware_model);
bump.reset();
operating_system(&bump, &mut info.operating_system);
bump.reset();
processor_name(&bump, &mut info.processor);
bump.reset();
let mut sys = sysinfo::System::new();
sys.refresh_disks_list();
sys.refresh_disks();
sys.refresh_memory();
let mut total_capacity = 0;
for disk in sys.disks() {
total_capacity += disk.total_space();
}
info.disk_capacity = format_size(total_capacity);
if let Some(name) = sys.host_name() {
info.device_name = name;
}
info.memory = format_size(sys.total_memory());
if let Ok(mut session) = std::env::var("XDG_SESSION_TYPE") {
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.windowing_system = session;
}
if let Ok(mut session) = std::env::var("DESKTOP_SESSION") {
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.desktop_environment = session;
}
if let Ok(output) = std::process::Command::new("lspci").output() {
if let Ok(stdout) = std::str::from_utf8(&output.stdout) {
for line in stdout.lines() {
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b"VGA") {
let line = &line[pos + 3..];
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b": ") {
info.graphics.push(String::from(&line[pos + 2..]));
}
}
}
}
}
info
}
}
pub fn architecture(bump: &Bump, arch: &mut String) {
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(value) = read_to_string("/proc/sys/kernel/arch", buffer) {
arch.push_str(value.trim());
}
}
pub fn hardware_model(bump: &Bump, hardware_model: &mut String) {
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut sys_vendor) = read_to_string(SYS_VENDOR, buffer) {
sys_vendor = sys_vendor.trim();
hardware_model.push_str(sys_vendor);
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut name) = read_to_string(BOARD_NAME, buffer) {
name = name.trim();
if !name.is_empty() && name != sys_vendor {
// Ensure that the name does not contain the vendor.
name = match name.strip_prefix(sys_vendor) {
Some(stripped) => stripped.trim(),
None => name,
};
strcat!(&mut *hardware_model, " " name);
}
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
if let Some(mut version) = read_to_string(BOARD_VERSION, buffer) {
version = version.trim();
// These have bogus values for their version.
const IGNORE_PRODUCTS: &[&str] = &["Dev One"];
if !version.is_empty() && !IGNORE_PRODUCTS.contains(&name) {
strcat!(hardware_model, " (" version.trim() ")");
}
}
}
}
}
pub fn operating_system(bump: &Bump, operating_system: &mut String) {
let mut buffer = bumpalo::collections::Vec::new_in(bump);
let Some(os_release) = read_to_string("/etc/os-release", &mut buffer) else {
return;
};
for line in os_release.lines() {
if let Some(mut value) = line.strip_prefix("PRETTY_NAME=") {
if let Some(v) = value.strip_prefix('"') {
value = v;
}
if let Some(v) = value.strip_suffix('"') {
value = v;
}
operating_system.push_str(value.trim());
break;
}
}
}
pub fn processor_name(bump: &Bump, name: &mut String) {
if let Some(cpuinfo) = read_to_string(
"/proc/cpuinfo",
&mut bumpalo::collections::Vec::new_in(bump),
) {
for line in cpuinfo.lines() {
if let Some(info) = line.strip_prefix("model name") {
if let Some(info) = info.trim_start().strip_prefix(":") {
name.push_str(info.trim());
}
break;
}
}
}
}
pub fn read_to_string<'a, P: AsRef<OsStr>>(
path: P,
buffer: &'a mut bumpalo::collections::Vec<u8>,
) -> Option<&'a str> {
let mut file = std::fs::File::open(path.as_ref()).ok()?;
if let Ok(metadata) = file.metadata() {
buffer.reserve_exact(metadata.len() as usize);
}
let mut buf = [0; 16 * 1024];
loop {
match file.read(&mut buf) {
Ok(0) => break,
Ok(read) => buffer.extend_from_slice(&buf[..read]),
Err(_) => return None,
}
}
std::str::from_utf8(buffer.as_slice()).ok()
}
fn format_size(size: u64) -> String {
byte_unit::Byte::from_bytes(size)
.get_appropriate_unit(true)
.to_string()
}
pub mod about;

14
pages/time/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "cosmic-settings-time"
version = "0.1.0"
edition = "2021"
[dependencies]
icu_calendar = "1.2.0"
icu_timezone = "1.2.0"
timedate-zbus = "0.1.0"
[dependencies.zbus]
version = "3.12.0"
default-features = false
features = ["tokio"]

48
pages/time/src/lib.rs Normal file
View file

@ -0,0 +1,48 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::time::SystemTime;
use icu_calendar::{types::IsoSecond, DateTime, Iso};
use icu_timezone::CustomTimeZone;
use timedate_zbus::TimeDateProxy;
pub struct Info {
pub can_ntp: bool,
pub timezone: CustomTimeZone,
pub local_time: DateTime<Iso>,
}
impl Info {
pub async fn load(proxy: &TimeDateProxy<'_>) -> Option<Info> {
let can_ntp = proxy.can_ntp().await.unwrap_or_default();
let Ok(timezone) = proxy.timezone()
.await
.unwrap_or_default()
.parse::<CustomTimeZone>() else {
return None;
};
let Ok(duration) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) else {
return None;
};
let seconds = duration.as_secs();
let Ok(iso_seconds) = IsoSecond::try_from((seconds % 60) as u8) else {
return None;
};
#[allow(clippy::cast_possible_truncation)]
let mut local_time = DateTime::from_minutes_since_local_unix_epoch((seconds / 60) as i32);
local_time.time.second = iso_seconds;
Some(Info {
can_ntp,
timezone,
local_time,
})
}
}

View file

@ -1,21 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page::{self, section, Content, Section};
use slotmap::SlotMap;
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("appearance", "preferences-pop-desktop-appearance-symbolic")
.title(fl!("appearance"))
.description(fl!("appearance", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}

View file

@ -1,22 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("dock", "preferences-pop-desktop-dock-symbolic")
.title(fl!("dock"))
.description(fl!("dock", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}

View file

@ -1,22 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("notifications", "preferences-system-notifications-symbolic")
.title(fl!("notifications"))
.description(fl!("notifications", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}

View file

@ -1,84 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod desktop;
pub mod networking;
pub mod section;
pub mod time;
pub use section::Section;
pub mod sound;
pub mod system;
mod model;
pub use model::{Insert, Model, PageTask};
use derive_setters::Setters;
use slotmap::SlotMap;
slotmap::new_key_type! {
/// ID of a page
pub struct Entity;
}
pub trait Page {
type Model: Default + 'static;
fn page() -> Meta;
#[must_use]
fn content(_sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
None
}
/// Attaches sub-pages to the page.
#[allow(clippy::must_use_candidate)]
fn sub_pages(page: Insert) -> Insert {
page
}
#[must_use]
#[allow(unused)]
fn load(page: Entity) -> PageTask {
Box::pin(async move { crate::Message::None })
}
}
#[derive(Setters)]
#[must_use]
pub struct Meta {
/// A unique identity that is the same between application runs.
#[setters(skip)]
pub id: &'static str,
/// The icon associated with the page.
#[setters(skip)]
pub icon_name: &'static str,
/// The title of the page.
#[setters(into)]
pub title: String,
/// A description of the page.
#[setters(into)]
pub description: String,
/// The parent of the page.
#[setters(strip_option)]
pub parent: Option<Entity>,
}
impl Meta {
pub const fn new(id: &'static str, icon_name: &'static str) -> Self {
Self {
title: String::new(),
icon_name,
id,
description: String::new(),
parent: None,
}
}
}
pub type Content = Vec<section::Entity>;

View file

@ -1,200 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
any::{Any, TypeId},
collections::HashMap,
future::Future,
pin::Pin,
};
use crate::page::{self, section, Content, Meta, Page, Section};
use cosmic::iced_native::command::{Action, Command};
use regex::Regex;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
pub type PageTask = Pin<Box<dyn Future<Output = crate::Message> + Send>>;
pub struct Model {
pub pages: SlotMap<page::Entity, Meta>,
pub page_load: SecondaryMap<page::Entity, fn(page::Entity) -> PageTask>,
pub resource: HashMap<TypeId, Box<dyn Any>>,
pub storage: HashMap<TypeId, SecondaryMap<page::Entity, Box<dyn Any>>>,
pub sub_pages: SparseSecondaryMap<page::Entity, Vec<page::Entity>>,
pub sections: SlotMap<section::Entity, Section>,
pub content: SparseSecondaryMap<page::Entity, Content>,
}
impl Default for Model {
fn default() -> Self {
Self {
content: SparseSecondaryMap::new(),
pages: SlotMap::with_key(),
page_load: SecondaryMap::new(),
resource: HashMap::new(),
sections: SlotMap::with_key(),
storage: HashMap::new(),
sub_pages: SparseSecondaryMap::new(),
}
}
}
impl Model {
/// Check if a page exists in the model.
#[must_use]
pub fn contains_item(&self, id: page::Entity) -> bool {
self.pages.contains_key(id)
}
/// Returns the content of a page, if it has any.
#[must_use]
pub fn content(&self, page: page::Entity) -> Option<&[section::Entity]> {
self.content.get(page).map(Vec::as_slice)
}
/// Get an immutable reference to data associated with a page.
#[must_use]
pub fn data<Data: 'static>(&self, id: page::Entity) -> Option<&Data> {
self.storage
.get(&TypeId::of::<Data>())
.and_then(|storage| storage.get(id))
.and_then(|data| data.downcast_ref())
}
/// Get a mutable reference to data associated with a page.
pub fn data_mut<Data: 'static>(&mut self, id: page::Entity) -> Option<&mut Data> {
self.storage
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.get_mut(id))
.and_then(|data| data.downcast_mut())
}
/// Associates data with the item.
pub fn data_set<Data: 'static>(&mut self, id: page::Entity, data: Data) {
if self.contains_item(id) {
self.storage
.entry(TypeId::of::<Data>())
.or_insert_with(SecondaryMap::new)
.insert(id, Box::new(data));
}
}
/// Removes a specific data type from the item.
pub fn data_remove<Data: 'static>(&mut self, id: page::Entity) {
self.storage
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.remove(id));
}
pub fn init_page(&mut self, id: page::Entity) -> Option<Command<crate::Message>> {
if let Some(func) = self.page_load.get(id).copied() {
return Some(Command::single(Action::Future(func(id))));
}
None
}
// Registers a new page in the settings panel.
pub fn register<P: Page>(&mut self) -> Insert {
let id = self.pages.insert(P::page());
self.page_load.insert(id, P::load);
if let Some(content) = P::content(&mut self.sections) {
self.content.insert(id, content);
}
self.resource_register::<P::Model>();
P::sub_pages(Insert { id, model: self })
}
#[must_use]
pub fn resource<Resource: 'static>(&self) -> Option<&Resource> {
self.resource
.get(&TypeId::of::<Resource>())
.and_then(|resource| resource.downcast_ref())
}
#[must_use]
pub fn resource_mut<Resource: 'static>(&mut self) -> Option<&mut Resource> {
self.resource
.get_mut(&TypeId::of::<Resource>())
.and_then(|resource| resource.downcast_mut())
}
#[allow(unused_must_use)]
pub fn resource_register<Resource: Default + 'static>(&mut self) {
self.resource
.entry(TypeId::of::<Resource>())
.or_insert_with(|| Box::new(Resource::default()));
}
/// Finds content of panels that match the search.
pub fn search<'a>(
&'a self,
rule: &'a Regex,
) -> impl Iterator<Item = (page::Entity, section::Entity)> + 'a {
generator::Gn::new_scoped_local(|mut s| {
for (page, sections) in self.content.iter() {
for id in sections {
if self.sections[*id].matches_search(rule) {
s.yield_((page, *id));
}
}
}
generator::done!();
})
}
/// Returns the sub-pages of a page, if it has any.
pub fn sub_pages(&self, page: page::Entity) -> Option<&[page::Entity]> {
self.sub_pages.get(page).map(AsRef::as_ref)
}
}
pub struct Insert<'a> {
pub model: &'a mut Model,
pub id: page::Entity,
}
impl<'a> Insert<'a> {
#[must_use]
pub fn id(self) -> page::Entity {
self.id
}
#[must_use]
pub fn content(self, content: Content) -> Self {
self.model.content.insert(self.id, content);
self
}
/// Adds a page and associates it with its parent page.
#[allow(clippy::return_self_not_must_use)]
#[allow(clippy::must_use_candidate)]
pub fn sub_page<P: Page>(self) -> Self {
let page = self.model.pages.insert(Meta {
parent: Some(self.id),
..P::page()
});
self.model.page_load.insert(page, P::load);
if let Some(content) = P::content(&mut self.model.sections) {
self.model.content.insert(page, content);
}
self.model.resource_register::<P::Model>();
self.model
.sub_pages
.entry(self.id)
.expect("parent page missing")
.and_modify(|v| v.push(page))
.or_insert_with(|| vec![page]);
self
}
}

View file

@ -1,65 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use derive_setters::Setters;
use regex::Regex;
use crate::SettingsApp;
slotmap::new_key_type! {
/// ID of a page section
pub struct Entity;
}
/// A searchable sub-component of a page. Searches can group multiple sections together.
#[derive(Setters)]
#[must_use]
pub struct Section {
#[setters(into)]
pub title: String,
pub descriptions: Vec<String>,
pub view_fn: for<'a> fn(&'a SettingsApp, &'a Section) -> cosmic::Element<'a, crate::Message>,
#[setters(bool)]
pub search_ignore: bool,
}
impl Section {
pub const fn new() -> Self {
Self {
title: String::new(),
descriptions: Vec::new(),
view_fn: Self::unimplemented,
search_ignore: false,
}
}
#[must_use]
pub fn matches_search(&self, rule: &Regex) -> bool {
if self.search_ignore {
return false;
}
if rule.is_match(self.title.as_str()) {
return true;
}
for description in &self.descriptions {
if rule.is_match(description.as_str()) {
return true;
}
}
false
}
#[must_use]
pub fn unimplemented<'a>(
_app: &'a SettingsApp,
_section: &'a Section,
) -> cosmic::Element<'a, crate::Message> {
cosmic::widget::settings::view_column(vec![cosmic::widget::settings::view_section("")
.add(crate::widget::unimplemented_page())
.into()])
.into()
}
}

View file

@ -1,161 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
page::{self, desktop::wallpaper::settings, Content, Section},
SettingsApp,
};
use cosmic::{
iced::{
widget::{horizontal_space, row},
Length,
},
widget::{icon, list_column, settings, text},
};
use cosmic_settings_system::Info;
use slotmap::SlotMap;
#[derive(Clone, Debug)]
pub enum Message {
Info(Info),
}
#[derive(Clone, Debug, Default)]
pub struct Model {
info: Info,
support_page: page::Entity,
}
impl Model {
pub fn update(&mut self, message: Message) {
match message {
Message::Info(info) => self.info = info,
}
}
}
pub struct Page;
impl page::Page for Page {
type Model = Model;
fn page() -> page::Meta {
page::Meta::new("about", "help-about-symbolic")
.title(fl!("about"))
.description(fl!("about", "desc"))
}
fn content(sections: &mut SlotMap<page::section::Entity, Section>) -> Option<Content> {
Some(vec![
sections.insert(distributor_logo()),
sections.insert(device()),
sections.insert(hardware()),
sections.insert(os()),
sections.insert(related()),
])
}
fn load(_page: page::Entity) -> crate::page::PageTask {
Box::pin(async move { crate::Message::About(Message::Info(Info::load())) })
}
}
fn device() -> Section {
Section::new()
.descriptions(vec![fl!("about-device"), fl!("about-device", "desc")])
.view_fn(|app, section| {
let desc = &section.descriptions;
let model = model(app);
let device_name = settings::item::builder(&desc[0])
.description(&desc[1])
.control(text(&model.info.device_name));
list_column().add(device_name).into()
})
}
fn distributor_logo() -> Section {
Section::new().search_ignore().view_fn(|_app, _section| {
row!(
horizontal_space(Length::Fill),
icon("distributor-logo", 78),
horizontal_space(Length::Fill),
)
// Add extra padding to reach 40px from the first section.
.padding([0, 16, 0, 16])
.into()
})
}
fn hardware() -> Section {
Section::new()
.title(fl!("about-hardware"))
.descriptions(vec![
fl!("about-hardware", "model"),
fl!("about-hardware", "memory"),
fl!("about-hardware", "processor"),
fl!("about-hardware", "graphics"),
fl!("about-hardware", "disk-capacity"),
])
.view_fn(|app, section| {
let desc = &section.descriptions;
let model = model(app);
let mut sections = settings::view_section(&section.title)
.add(settings::item(&desc[0], text(&model.info.hardware_model)))
.add(settings::item(&desc[1], text(&model.info.memory)))
.add(settings::item(&desc[2], text(&model.info.processor)));
for card in &model.info.graphics {
sections = sections.add(settings::item(&desc[3], text(card.as_str())));
}
// .add(settings::item(&desc[3], text(&model.info.graphics)))
sections
.add(settings::item(&desc[4], text(&model.info.disk_capacity)))
.into()
})
}
fn os() -> Section {
Section::new()
.title(fl!("about-os"))
.descriptions(vec![
fl!("about-os", "os"),
fl!("about-os", "os-architecture"),
fl!("about-os", "desktop-environment"),
fl!("about-os", "windowing-system"),
])
.view_fn(|app, section| {
let model = model(app);
let desc = &section.descriptions;
settings::view_section(&section.title)
.add(settings::item(&desc[0], text(&model.info.operating_system)))
.add(settings::item(&desc[1], text(&model.info.os_architecture)))
.add(settings::item(
&desc[2],
text(&model.info.desktop_environment),
))
.add(settings::item(&desc[3], text(&model.info.windowing_system)))
.into()
})
}
fn related() -> Section {
Section::new()
.title(fl!("about-related"))
.descriptions(vec![fl!("about-related", "support")])
.view_fn(|_app, section| {
settings::view_section(&section.title)
.add(settings::item(&section.descriptions[0], text("TODO")))
.into()
})
}
fn model(app: &SettingsApp) -> &Model {
app.pages
.resource::<Model>()
.expect("missing system->about model")
}

View file

@ -1,22 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("firmware", "firmware-manager-symbolic")
.title(fl!("firmware"))
.description(fl!("firmware", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}

View file

@ -1,27 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod about;
pub mod firmware;
pub mod users;
use crate::page;
pub struct Page;
impl page::Page for Page {
type Model = Model;
fn page() -> page::Meta {
page::Meta::new("system", "system-users-symbolic").title(fl!("system"))
}
fn sub_pages(page: page::Insert) -> page::Insert {
page.sub_page::<users::Page>()
.sub_page::<about::Page>()
.sub_page::<firmware::Page>()
}
}
#[derive(Default)]
pub struct Model {}

View file

@ -1,22 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use slotmap::SlotMap;
use crate::page::{self, section, Content, Section};
pub struct Page;
impl page::Page for Page {
type Model = super::Model;
fn page() -> page::Meta {
page::Meta::new("users", "system-users-symbolic")
.title(fl!("users"))
.description(fl!("users", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}

View file

@ -1,23 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page;
pub mod date;
pub mod region;
pub struct Page;
impl page::Page for Page {
type Model = ();
fn page() -> page::Meta {
page::Meta::new("time", "preferences-system-time-symbolic")
.title(fl!("time"))
.description(fl!("time", "desc"))
}
fn sub_pages(page: page::Insert) -> page::Insert {
page.sub_page::<date::Page>().sub_page::<region::Page>()
}
}

View file

@ -1,21 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use crate::page::{self, section, Content, Section};
use slotmap::SlotMap;
pub struct Page;
impl page::Page for Page {
type Model = ();
fn page() -> page::Meta {
page::Meta::new("time-region", "preferences-desktop-locale-symbolic")
.title(fl!("time-region"))
.description(fl!("time-region", "desc"))
}
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
Some(vec![sections.insert(Section::new())])
}
}