feat(settings): add grouped configuration hub, languages and tray controls

This commit is contained in:
aria committed 2026-10-28 03:37:22 +09:00
1 parent 6f34fd080f
commit cc3b6f903f
90 files changed
+3625 -408

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -64,7 +64,7 @@ impl App {
)
.push(
ferese_theme::controls::switch(adapter.powered, p)
.name("Bluetooth power")
.name(ferese_config::tr!("Bluetooth power"))
.on_press_maybe(ready.then(|| {
message(Input::Run(Command::BluetoothPower(
adapter.path.clone(),
@@ -49,7 +49,7 @@ impl App {
)
.push(
ferese_theme::controls::switch(wifi.enabled, p)
.name("Wi-Fi power")
.name(ferese_config::tr!("Wi-Fi power"))
.on_press_maybe(
(ready
&& (wifi.hardware_enabled || wifi.enabled)
+8 -5
View File
@@ -36,14 +36,14 @@ impl App {
.width(Length::Fill),
)
.push(self.settings_icon_button(
"Refresh displays",
ferese_config::tr!("Refresh displays"),
"M20 7V3l-3 3a8 8 0 1 0 3 6 M20 3h-5",
Some(Message::RefreshDisplays),
)),
);
if let Some(error) = &self.display_error {
panel = panel.push(cosmic::widget::tooltip(
self.note("Live display information unavailable"),
self.note(ferese_config::tr!("Live display information unavailable")),
self.label(error.clone(), 11.),
cosmic::widget::tooltip::Position::Bottom,
));
@@ -159,13 +159,16 @@ impl App {
)));
panel = panel.push(row([]).spacing(5).align_y(Alignment::Center)
.push(cosmic::widget::tooltip(visuals::action_icon("M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18 M12 11v6 M12 7h.01", palette.muted),
self.label("Auto uses 60 Hz below 30% battery and returns to normal on AC or at 35%. Applying a display mode can briefly blank the screen.", 11.), cosmic::widget::tooltip::Position::Top))
self.label(ferese_config::tr!("Auto uses 60 Hz below 30% battery and returns to normal on AC or at 35%. Applying a display mode can briefly blank the screen."), 11.), cosmic::widget::tooltip::Position::Top))
.push(self.note(if target.active { "Changes apply live" } else { "Saved profile · applies when active" })));
} else {
panel = panel.push(self.note("Managed automatically · no saved display profile"));
panel = panel.push(self.note(ferese_config::tr!(
"Managed automatically · no saved display profile"
)));
}
} else {
panel = panel.push(self.note("No displays or saved profiles found"));
panel =
panel.push(self.note(ferese_config::tr!("No displays or saved profiles found")));
}
container(panel)
.padding([7, 10])
+4 -4
View File
@@ -123,10 +123,10 @@ impl App {
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
let mut labels = column([])
.spacing(2)
.push(self.label(field.label.clone(), 13.));
.push(self.label(ferese_config::locale::translate(&field.label), 13.));
if !field.description.is_empty() {
labels = labels.push(
self.label(field.description.clone(), 11.)
self.label(ferese_config::locale::translate(&field.description), 11.)
.class(cosmic::theme::Text::Color(palette.muted)),
);
}
@@ -169,7 +169,7 @@ impl App {
format!("{value:.2}{suffix}")
};
if margin_limit == Some(None) {
self.label("—", 12.).into()
self.label(ferese_config::tr!("—"), 12.).into()
} else if max <= min {
self.label(display, 12.).into()
} else {
@@ -195,7 +195,7 @@ impl App {
let mut options = row([]).spacing(2);
for (key, label) in choices {
options = options.push(
button::custom(self.label(*label, 12.))
button::custom(self.label(ferese_config::locale::text(label), 12.))
.padding([4, 8])
.class(self.control_button_style(palette, value == *key))
.on_press(Message::Change(set(&path, *key))),
+67 -2
View File
@@ -1,3 +1,4 @@
mod all_options;
mod connections;
mod form_controls;
mod navigation;
@@ -8,6 +9,7 @@ mod panel_edit;
mod panel_inspector;
mod panel_preview;
mod theme_controls;
mod tray_controls;
use theme_controls::gallery_id;
@@ -62,6 +64,11 @@ fn main() -> cosmic::iced::Result {
}
Some("wallpaper") => Some(InitialPage::Wallpaper),
Some("bar") => Some(InitialPage::Bar),
Some("appearance") => Some(InitialPage::Settings(Page::Appearance)),
Some("general") => Some(InitialPage::Settings(Page::General)),
Some("theme") => Some(InitialPage::Settings(Page::Theme)),
Some("tray") => Some(InitialPage::Settings(Page::Tray)),
Some("all-options") => Some(InitialPage::Settings(Page::Configuration)),
_ => {
eprintln!("--page accepts connections, wifi, bluetooth, wallpaper, or bar");
return Ok(());
@@ -79,6 +86,9 @@ fn main() -> cosmic::iced::Result {
let path = path.canonicalize().unwrap_or(path);
fonts::families();
let initial = Snapshot::read(&path);
if let Ok(snapshot) = &initial {
ferese_config::locale::set(ferese_config::locale::from_document(&snapshot.doc));
}
let theme = visuals::native_theme(initial.as_ref().ok());
cosmic::app::run::<App>(
@@ -94,12 +104,16 @@ fn main() -> cosmic::iced::Result {
#[derive(Clone, Copy)]
enum InitialPage {
Connections(connections::Tab),
Settings(Page),
Wallpaper,
Bar,
}
#[derive(Clone, Debug)]
enum Message {
Options(all_options::Input),
TrayApps(Vec<tray_controls::Application>),
RefreshTrayApps,
ReadRenderScale(cosmic::iced::window::Id),
RenderScale(f32),
Connections(connections::Input),
@@ -165,6 +179,8 @@ enum Message {
}
struct App {
tray_apps: Vec<tray_controls::Application>,
options: all_options::State,
connections: connections::State,
displays: Vec<displays::Display>,
display_selection: Option<String>,
@@ -265,7 +281,11 @@ impl cosmic::Application for App {
let font = fonts::interface_font(&resolved.presented.tokens.typography.font_family);
let native_palette = visuals::Palette::from_resolved(&resolved.presented);
let family_sections = theme_controls::FamilySections::new(&resolved.families, &current);
ferese_config::locale::set(ferese_config::locale::from_document(&current.doc));
let options = all_options::State::load(&current);
let mut app = Self {
tray_apps: Vec::new(),
options,
connections: connections::State {
tab: match initial_page {
Some(InitialPage::Connections(tab)) => tab,
@@ -291,6 +311,7 @@ impl cosmic::Application for App {
Some(InitialPage::Connections(_)) => Page::Connections,
Some(InitialPage::Wallpaper) => Page::Wallpaper,
Some(InitialPage::Bar) => Page::Bar,
Some(InitialPage::Settings(page)) => page,
None => Page::Appearance,
},
profile_pages: std::env::var_os("FERESE_PROFILE_SETTINGS").is_some(),
@@ -328,9 +349,15 @@ impl cosmic::Application for App {
let task = app
.core
.main_window_id()
.map(|id| app.set_window_title("Ferese Settings".into(), id))
.map(|id| app.set_window_title(ferese_config::tr!("Ferese Settings").into(), id))
.unwrap_or_else(Task::none);
let tray_apps = if app.page == Page::Tray {
app.load_tray_apps()
} else {
Task::none()
};
let task = Task::batch([task, tray_apps]);
let connections = if app.page == Page::Connections {
app.refresh_connections()
} else {
@@ -347,6 +374,9 @@ impl cosmic::Application for App {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::TrayApps(apps) => self.tray_apps = apps,
Message::RefreshTrayApps => return self.load_tray_apps(),
Message::Options(input) => return self.options_input(input),
Message::DragWindow => return self.core.drag(None),
Message::ReadRenderScale(id) => {
return cosmic::iced::window::scale_factor(id)
@@ -427,11 +457,16 @@ impl cosmic::Application for App {
|| !self.inputs.is_empty()
|| !self.ranges.is_empty()
|| !self.pending.is_empty()
|| self.options.dirty()
{
self.status =
"Config changed externally · Reload when your edits are finished"
.into();
} else {
ferese_config::locale::set(ferese_config::locale::from_document(
&snapshot.doc,
));
self.options.reset(&snapshot);
self.current = snapshot.clone();
self.draft = snapshot;
self.family_sections = theme_controls::FamilySections::new(
@@ -493,6 +528,12 @@ impl cosmic::Application for App {
self.page_transition = self.profile_pages.then(std::time::Instant::now);
let leaving = self.page == Page::Connections && page != Page::Connections;
if page == Page::Configuration && !self.options.dirty() {
self.options.reset(&self.current);
}
if page == Page::Configuration {
self.options.filter = self.search.clone();
}
self.page = page;
self.search.clear();
self.visible_rows = initial_visible_rows();
@@ -507,6 +548,7 @@ impl cosmic::Application for App {
let load = match page {
Page::Displays | Page::Bar => self.load_displays(),
Page::Wallpaper => self.load_thumbnail(),
Page::Tray => self.load_tray_apps(),
_ => Task::none(),
};
return Task::batch([reset, cleanup, load]);
@@ -517,6 +559,9 @@ impl cosmic::Application for App {
if matches!(page, Page::Displays | Page::Bar) {
return Task::batch([reset, self.load_displays()]);
}
if page == Page::Tray {
return Task::batch([reset, self.load_tray_apps()]);
}
if page == Page::Wallpaper {
return Task::batch([reset, self.load_thumbnail()]);
}
@@ -725,6 +770,12 @@ impl cosmic::Application for App {
Ok((snapshot, live)) => {
self.undo = Some(std::mem::take(&mut self.saving_previous));
self.current = snapshot;
ferese_config::locale::set(ferese_config::locale::from_document(
&self.current.doc,
));
if !self.options.dirty() || self.options.applying {
self.options.reset(&self.current);
}
self.font = fonts::interface_font(
&self.resolved.presented.tokens.typography.font_family,
);
@@ -755,7 +806,18 @@ impl cosmic::Application for App {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
Message::ExpireUndo(revision)
});
let title = self
.core
.main_window_id()
.map(|id| {
self.set_window_title(
ferese_config::tr!("Ferese Settings").into(),
id,
)
})
.unwrap_or_else(Task::none);
return Task::batch([
title,
self.update_theme(),
self.flush(),
self.load_thumbnail(),
@@ -766,12 +828,15 @@ impl cosmic::Application for App {
self.error = Some(error);
self.pending.clear();
self.draft = self.current.clone();
self.options.applying = false;
self.status = "Not saved".into();
}
}
}
Message::Reload if !self.saving => match Snapshot::read(&self.path) {
Ok(snapshot) => {
ferese_config::locale::set(ferese_config::locale::from_document(&snapshot.doc));
self.options.reset(&snapshot);
self.current = snapshot.clone();
self.draft = snapshot;
self.family_sections =
@@ -1754,7 +1819,7 @@ mod tests {
.expect("requested renderer");
eprintln!("renderer={}", Headless::name(&renderer));
for page in [Page::Shortcuts, Page::Appearance] {
for page in [Page::Shortcuts, Page::Theme] {
let mut app = shortcuts(500);
app.page = page;
let theme = app.native_palette.native_theme();
+57 -36
View File
@@ -26,7 +26,7 @@ impl App {
.align_y(Alignment::Center)
.push(visuals::brand_icon())
.push(
self.label("Ferese", 16.)
self.label(ferese_config::tr!("Ferese"), 16.)
.class(cosmic::theme::Text::Color(palette.text)),
)
.width(Length::Fill),
@@ -36,7 +36,7 @@ impl App {
)
.push(widget::Space::new().height(14))
.push(
text_input("Search settings", self.search.clone())
text_input(ferese_config::tr!("Search settings"), self.search.clone())
.id(widget::Id::new("settings-search"))
.on_input(Message::Search)
.font(self.font)
@@ -46,7 +46,7 @@ impl App {
.push(widget::Space::new().height(8));
if compact {
sidebar = sidebar.push(self.settings_button(
"Close navigation",
ferese_config::tr!("Close navigation"),
"M6 6l12 12 M6 18L18 6",
Some(Message::Sidebar(false)),
false,
@@ -56,7 +56,7 @@ impl App {
for (title, pages) in Page::NAVIGATION {
let mut section = column([]).spacing(3).push(
container(
self.label(title, 11.)
self.label(ferese_config::locale::text(title), 11.)
.class(cosmic::theme::Text::Color(palette.muted)),
)
.padding([0, 8]),
@@ -118,13 +118,13 @@ impl App {
)
.width(Length::Fill),
);
if self.page == Page::Appearance && self.search.is_empty() {
if self.page == Page::Theme && self.search.is_empty() {
let automatic = self.draft.string("theme.mode", "dark") == "auto";
heading = heading.push(
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Auto", 12.))
.push(self.label(ferese_config::tr!("Auto"), 12.))
.push(
ferese_theme::controls::switch(automatic, palette)
.name(if automatic {
@@ -179,11 +179,18 @@ impl App {
.into_iter()
.flat_map(|(_, pages)| pages.iter().copied())
};
if !pages().any(|p| p.matches(&self.search)) {
body = body
.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
if !pages().any(|p| {
p.matches(&self.search)
|| (p == Page::Configuration && self.options.matches(&self.search))
}) {
body = body.push(self.note(ferese_config::tr!(
"No matching settings. Try wallpaper, keyboard, or motion."
)));
}
for page in pages().filter(|p| p.matches(&self.search)) {
for page in pages().filter(|p| {
p.matches(&self.search)
|| (*p == Page::Configuration && self.options.matches(&self.search))
}) {
let destination = page.search_destination(&self.search);
body = body.push(
button::custom(
@@ -199,13 +206,24 @@ impl App {
);
}
} else {
if self.page == Page::Appearance {
body = body.push(self.settings_hub());
}
if self.page == Page::Configuration {
body = body.push(self.all_options_view());
}
if self.page == Page::Tray {
body = body
.push(self.tray_application_controls())
.push(self.tray_status_controls());
}
if self.page == Page::Windows {
body = body.push(visuals::preview_resolved(
&self.draft,
&self.resolved.presented,
));
}
if self.page == Page::Appearance {
if self.page == Page::Theme {
let split = visuals::split(&self.draft);
if split {
body = body
@@ -245,11 +263,11 @@ impl App {
if !fields.is_empty() {
let mut group = column([]).spacing(1);
if self.page == Page::Appearance && self.auto_details {
if self.page == Page::Theme && self.auto_details {
group = group.push(self.auto_controls());
}
for field in fields {
if self.page == Page::Appearance
if self.page == Page::Theme
&& (field.path == "theme.mode"
|| field.path.starts_with("theme.schedule.")
|| field.path.ends_with(".file"))
@@ -258,13 +276,13 @@ impl App {
}
group = group.push(self.field(field));
}
if self.page == Page::Appearance {
if self.page == Page::Theme {
group = group.push(
button::custom(
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Advanced customization", 13.))
.push(self.label(ferese_config::tr!("Advanced customization"), 13.))
.push(visuals::action_icon(
if self.advanced_theme {
"M6 15l6-6 6 6"
@@ -298,17 +316,17 @@ impl App {
match self.page {
Page::Connections => body = body.push(self.connections_view()),
Page::LockScreen => {
body = body.push(self.note("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens."));
body = body.push(self.note(ferese_config::tr!("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens.")));
body = body.push(self.settings_button(
"Preview lock screen",
ferese_config::tr!("Preview lock screen"),
"M7 10V7a5 5 0 0 1 10 0v3 M5 10h14v11H5z M12 14v3",
Some(Message::PreviewLock),
false,
));
body = body.push(self.note("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items."));
body = body.push(self.note(ferese_config::tr!("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items.")));
}
Page::Startup => {
body = body.push(self.note("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it."));
body = body.push(self.note(ferese_config::tr!("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it.")));
for index in 0..self.draft.records("autostart") {
let prefix = format!("autostart.{index}");
@@ -338,7 +356,7 @@ impl App {
.push(
container(
self.settings_icon_button(
"Remove login item",
ferese_config::tr!("Remove login item"),
"M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7",
(!self.saving)
.then_some(Message::Remove("autostart".into(), index)),
@@ -356,16 +374,19 @@ impl App {
row([])
.spacing(8)
.push(
text_input("Program and arguments", self.new_command.clone())
.font(self.font)
.size(12)
.padding([4, 8])
.style(visuals::input_style(palette))
.on_input(Message::NewCommand),
text_input(
ferese_config::tr!("Program and arguments"),
self.new_command.clone(),
)
.font(self.font)
.size(12)
.padding([4, 8])
.style(visuals::input_style(palette))
.on_input(Message::NewCommand),
)
.push(
self.settings_button(
"Add login item",
ferese_config::tr!("Add login item"),
"M12 5v14 M5 12h14",
(!self.saving && !self.new_command.trim().is_empty())
.then_some(Message::AddCommand),
@@ -375,7 +396,7 @@ impl App {
);
}
Page::Shortcuts => {
body = body.push(self.note("Keyboard, mouse and swipe bindings share the same actions. Mouse shortcuts can use MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward or WheelUp/Down/Left/Right with modifiers. Use pointer-move or pointer-resize for dragging. Swipes use Swipe3Up, Swipe3Down, Swipe3Left or Swipe3Right (also supports 4 or 5 fingers). Actions such as toggle-overview and none need no argument."));
body = body.push(self.note(ferese_config::tr!("Keyboard, mouse and swipe bindings share the same actions. Mouse shortcuts can use MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward or WheelUp/Down/Left/Right with modifiers. Use pointer-move or pointer-resize for dragging. Swipes use Swipe3Up, Swipe3Down, Swipe3Left or Swipe3Right (also supports 4 or 5 fingers). Actions such as toggle-overview and none need no argument.")));
let mut gestures = row([]).spacing(6);
for (keys, label) in [
("Swipe3Up", "Customize swipe up"),
@@ -447,7 +468,7 @@ impl App {
);
}
if self.draft.records("bindings") == 0 {
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload."));
body = body.push(self.note(ferese_config::tr!("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload.")));
}
}
Page::Displays => body = body.push(self.displays_view()),
@@ -459,7 +480,7 @@ impl App {
if compact {
page_heading = page_heading.push(
container(self.settings_icon_button(
"Show settings navigation",
ferese_config::tr!("Show settings navigation"),
"M3 5h18v14H3z M9 5v14",
Some(Message::Sidebar(true)),
))
@@ -478,7 +499,7 @@ impl App {
content = content.push(
container(
row(self.page.subpages().iter().map(|&(page, title)| {
button::custom(self.label(title, 13.))
button::custom(self.label(ferese_config::locale::text(title), 13.))
.name(format!("{}: {title}", self.page.navigation_page().title()))
.padding([7, 12])
.class(visuals::panel_button(palette, self.page == page))
@@ -570,7 +591,7 @@ impl App {
))
.padding(4)
.on_press_maybe((self.undo.is_some() && !self.saving).then_some(Message::Undo)),
self.label("Undo last change", 11.),
self.label(ferese_config::tr!("Undo last change"), 11.),
widget::tooltip::Position::Top,
))
.push(widget::tooltip(
@@ -580,11 +601,11 @@ impl App {
))
.padding(4)
.on_press_maybe((!self.saving).then_some(Message::Reload)),
self.label("Reload configuration", 11.),
self.label(ferese_config::tr!("Reload configuration"), 11.),
widget::tooltip::Position::Top,
));
let footer: Element<'_, Message> = if self.page == Page::Connections {
self.note("Connections are managed by the system. Passwords are not saved in your Ferese config.")
self.note(ferese_config::tr!("Connections are managed by the system. Passwords are not saved in your Ferese config."))
} else if self.page == Page::Bar {
container(
row([])
@@ -597,13 +618,13 @@ impl App {
}))
.push(widget::Space::new().width(Length::Fill))
.push(self.settings_button(
"Undo",
ferese_config::tr!("Undo"),
"M3 10h8 M3 10V3 M3 10c3-7 17-6 17 3a7 7 0 0 1-7 7",
(self.undo.is_some() && !self.saving).then_some(Message::Undo),
false,
))
.push(self.settings_button(
"Reload configuration",
ferese_config::tr!("Reload configuration"),
"M21 3v6h-6 M3 21v-6h6 M3 9a9 9 0 0 1 15-6l3 6 M21 15a9 9 0 0 1-15 6l-3-6",
(!self.saving).then_some(Message::Reload),
false,
+76 -67
View File
@@ -35,7 +35,7 @@ impl std::fmt::Display for StyleChoice {
}
}
const CATALOG: [CatalogItem; 15] = [
const CATALOG: [CatalogItem; 16] = [
CatalogItem(ItemKind::Overview),
CatalogItem(ItemKind::Workspaces {
style: ferese_panel::WorkspaceStyle::Dots,
@@ -44,6 +44,7 @@ const CATALOG: [CatalogItem; 15] = [
CatalogItem(ItemKind::Media),
CatalogItem(ItemKind::Activities),
CatalogItem(ItemKind::QuickSettings),
CatalogItem(ItemKind::Tray),
CatalogItem(ItemKind::NightLight),
CatalogItem(ItemKind::Network),
CatalogItem(ItemKind::Audio),
@@ -58,7 +59,7 @@ const CATALOG: [CatalogItem; 15] = [
impl App {
pub(super) fn panel_navigation(&self) -> Element<'static, Message> {
self.settings_button(
"Back to Panels",
ferese_config::tr!("Back to Panels"),
"M15 18l-6-6 6-6",
Some(Message::PanelPage(PanelPage::Panels)),
false,
@@ -71,52 +72,53 @@ impl App {
let mut rows = widget::column([]).spacing(12);
match self.panel_page {
PanelPage::Panels => {
rows =
rows.push(
widget::container(
widget::row([])
.spacing(12)
.align_y(cosmic::iced::Alignment::Center)
.push(crate::visuals::action_icon(
if edge == Edge::Top {
"M3 5h18v14H3z M3 9h18"
} else {
"M3 5h18v14H3z M3 15h18"
},
palette.accent,
))
.push(
widget::column([])
.spacing(3)
.push(self.panel_heading(
if edge == Edge::Top {
"Top panel"
} else {
"Bottom panel"
},
14.,
))
.push(self.note("Shown on all displays"))
.width(cosmic::iced::Length::Fill),
)
.push(self.settings_button(
"Edit panel",
"M4 20l4-1 12-12-3-3L5 16z",
Some(Message::PanelPage(PanelPage::Arrange)),
false,
))
.push(self.settings_icon_button(
"Remove custom panel",
rows = rows.push(
widget::container(
widget::row([])
.spacing(12)
.align_y(cosmic::iced::Alignment::Center)
.push(crate::visuals::action_icon(
if edge == Edge::Top {
"M3 5h18v14H3z M3 9h18"
} else {
"M3 5h18v14H3z M3 15h18"
},
palette.accent,
))
.push(
widget::column([])
.spacing(3)
.push(self.panel_heading(
if edge == Edge::Top {
"Top panel"
} else {
"Bottom panel"
},
14.,
))
.push(self.note(ferese_config::tr!("Shown on all displays")))
.width(cosmic::iced::Length::Fill),
)
.push(self.settings_button(
ferese_config::tr!("Edit panel"),
"M4 20l4-1 12-12-3-3L5 16z",
Some(Message::PanelPage(PanelPage::Arrange)),
false,
))
.push(
self.settings_icon_button(
ferese_config::tr!("Remove custom panel"),
"M4 7h16 M9 7V4h6v3 M6 7l1 13h10l1-13 M10 11v5 M14 11v5",
self.draft.item("panels").is_some().then(|| {
Message::Change(store::Edit::Unset("panels".into()))
}),
)),
)
.padding(14)
.width(cosmic::iced::Length::Fill)
.class(crate::visuals::surface(palette.card, 14.)),
);
),
),
)
.padding(14)
.width(cosmic::iced::Length::Fill)
.class(crate::visuals::surface(palette.card, 14.)),
);
let preset = self.preview_panel().ok().and_then(|panel| panel.preset());
let position = widget::row([Edge::Top, Edge::Bottom].into_iter().map(|position| {
let label = if position == Edge::Top {
@@ -152,7 +154,7 @@ impl App {
);
let mut appearance = widget::column([])
.spacing(16)
.push(self.panel_heading("Appearance", 15.))
.push(self.panel_heading(ferese_config::tr!("Appearance"), 15.))
.push(self.panel_style_choices(preset.unwrap_or_default()))
.push(self.panel_divider())
.push(self.panel_opacity_controls())
@@ -166,7 +168,7 @@ impl App {
self.draft.boolean("panels.0.border", false),
palette,
)
.name("Outline")
.name(ferese_config::tr!("Outline"))
.on_press(Message::PanelEdit(Action::SetBorder(
!self.draft.boolean("panels.0.border", false),
))),
@@ -192,7 +194,7 @@ impl App {
"Join the island to the screen edge with curved shoulders.",
widget::container(
ferese_theme::controls::switch(attached, palette)
.name("Attach to screen edge")
.name(ferese_config::tr!("Attach to screen edge"))
.on_press(Message::PanelEdit(Action::SetMorphAttached(
!attached,
))),
@@ -208,7 +210,7 @@ impl App {
"Expand the island when you hover over it.",
widget::container(
ferese_theme::controls::switch(enabled, palette)
.name("Preview on hover")
.name(ferese_config::tr!("Preview on hover"))
.on_press(Message::PanelEdit(Action::SetPeekOnHover(
!enabled,
))),
@@ -248,7 +250,7 @@ impl App {
"Show +n for additional activities.",
widget::container(
ferese_theme::controls::switch(show_badge, palette)
.name("Show activity count")
.name(ferese_config::tr!("Show activity count"))
.on_press(Message::Change(store::set(
"activities.show_badge",
!show_badge,
@@ -272,10 +274,10 @@ impl App {
let mut settings = widget::column([]).spacing(4).push(
widget::container(
widget::row![
self.panel_heading("Size & spacing", 15.)
self.panel_heading(ferese_config::tr!("Size & spacing"), 15.)
.width(cosmic::iced::Length::Fill),
self.settings_button(
"Reset to style",
ferese_config::tr!("Reset to style"),
"M3 12a9 9 0 1 0 3-6 M3 3v6h6",
pins.then_some(Message::PanelEdit(Action::ResetShape)),
false
@@ -355,7 +357,7 @@ impl App {
);
let mut shell = widget::column([])
.spacing(4)
.push(self.panel_heading("Shell appearance", 15.));
.push(self.panel_heading(ferese_config::tr!("Shell appearance"), 15.));
for field in crate::schema::fields(Page::Bar)
.into_iter()
.filter(|field| field.path.starts_with("theme."))
@@ -427,7 +429,7 @@ impl App {
"Background opacity",
"Reduce transparency is on. Turn it off in Accessibility to change opacity.",
self.settings_button(
"Accessibility",
ferese_config::tr!("Accessibility"),
"M9 18l6-6-6-6",
Some(Message::Page(Page::Accessibility)),
false,
@@ -482,7 +484,7 @@ impl App {
.width(48),
)
.push(self.settings_icon_button(
"Use shell opacity",
ferese_config::tr!("Use shell opacity"),
"M3 12a9 9 0 1 0 3-6 M3 3v6h6",
custom.then_some(Message::PanelEdit(Action::ClearOpacity)),
));
@@ -632,8 +634,10 @@ impl App {
cosmic::iced::widget::responsive(move |size| {
let editor = widget::column([])
.spacing(12)
.push(self.panel_heading("Arrangement", 15.))
.push(self.note("Drag items to reorder. Select an item or group to edit."))
.push(self.panel_heading(ferese_config::tr!("Arrangement"), 15.))
.push(self.note(ferese_config::tr!(
"Drag items to reorder. Select an item or group to edit."
)))
.push(
widget::scrollable(self.panel_editor(&panel))
.id(widget::Id::new("settings-content"))
@@ -710,9 +714,9 @@ impl App {
"M4 4h16v16H4z M14 4v16 M7 8h4 M7 12h4",
palette.muted,
))
.push(self.panel_heading("Make it your own", 15.))
.push(self.panel_heading(ferese_config::tr!("Make it your own"), 15.))
.push(self.note(
"Select an item to adjust how it appears, or a group to edit its placement and spacing.",
ferese_config::tr!("Select an item to adjust how it appears, or a group to edit its placement and spacing."),
)),
)
.padding([40, 8])
@@ -779,7 +783,7 @@ impl App {
)
.push(
self.settings_button(
"Add group",
ferese_config::tr!("Add group"),
"M12 5v14 M5 12h14",
(panel.group_count() < ferese_panel::MAX_GROUPS)
.then_some(Message::PanelEdit(Action::AddGroup(placement))),
@@ -881,8 +885,10 @@ impl App {
}
let mut contents = widget::column([]).spacing(4).push(heading);
if items.is_empty() {
contents = contents
.push(widget::container(self.note("Drop items here")).padding([8, 6]));
contents = contents.push(
widget::container(self.note(ferese_config::tr!("Drop items here")))
.padding([8, 6]),
);
} else {
contents = contents.push(
widget::flex_row(items)
@@ -922,12 +928,14 @@ impl App {
}
if zone.groups.is_empty() {
groups = groups.push(
widget::container(self.note("Drop an item here, or add a group."))
.id(crate::panel_drag::empty_zone_id(placement))
.width(Length::Fill)
.height(64)
.padding(12)
.class(crate::visuals::surface(palette.card, 10.)),
widget::container(
self.note(ferese_config::tr!("Drop an item here, or add a group.")),
)
.id(crate::panel_drag::empty_zone_id(placement))
.width(Length::Fill)
.height(64)
.padding(12)
.class(crate::visuals::surface(palette.card, 10.)),
);
}
zones = zones.push(
@@ -951,6 +959,7 @@ pub(super) fn group_name(id: &str) -> String {
pub(super) fn item_icon(kind: ItemKind, tint: cosmic::iced::Color) -> widget::icon::Icon {
use ferese_theme::icons;
let source = match kind {
ItemKind::Tray => icons::APPLICATION,
ItemKind::Overview => icons::FERESE,
ItemKind::Workspaces { .. } => icons::OVERVIEW,
ItemKind::FocusedWindow => icons::DISPLAY,
+12 -9
View File
@@ -23,7 +23,7 @@ impl App {
.width(Length::Fill),
)
.push(self.settings_icon_button(
"Deselect",
ferese_config::tr!("Deselect"),
"M6 6l12 12 M6 18L18 6",
Some(Message::PanelClearSelection),
)),
@@ -58,7 +58,7 @@ impl App {
rows = rows.push(
widget::column([])
.spacing(8)
.push(self.label("Placement", 13.))
.push(self.label(ferese_config::tr!("Placement"), 13.))
.push(crate::visuals::panel_select(
cosmic::iced::widget::pick_list(
crate::panel_edit::destinations(panel),
@@ -166,8 +166,10 @@ impl App {
};
let mut choices = widget::column([])
.spacing(6)
.push(self.label("Workspace style", 13.))
.push(self.note("A sample of active, occupied and empty workspaces."));
.push(self.label(ferese_config::tr!("Workspace style"), 13.))
.push(self.note(ferese_config::tr!(
"A sample of active, occupied and empty workspaces."
)));
for style in ferese_panel::WorkspaceStyle::ALL {
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 22.);
choices = choices.push(
@@ -210,7 +212,7 @@ impl App {
.push(
widget::column([])
.spacing(8)
.push(self.label("Placement", 13.))
.push(self.label(ferese_config::tr!("Placement"), 13.))
.push(crate::visuals::panel_select(
cosmic::iced::widget::pick_list(
Zone::ALL.to_vec(),
@@ -307,8 +309,9 @@ impl App {
),
);
if !group.items.is_empty() {
rows = rows
.push(self.note("Move or remove this group’s items before removing the group."));
rows = rows.push(self.note(ferese_config::tr!(
"Move or remove this group’s items before removing the group."
)));
}
rows.into()
}
@@ -322,8 +325,8 @@ impl App {
) -> Element<'static, Message> {
widget::row([])
.spacing(8)
.push(self.settings_icon_button("Up", "M6 15l6-6 6 6", earlier))
.push(self.settings_icon_button("Down", "M6 9l6 6 6-6", later))
.push(self.settings_icon_button(ferese_config::tr!("Up"), "M6 15l6-6 6 6", earlier))
.push(self.settings_icon_button(ferese_config::tr!("Down"), "M6 9l6 6 6-6", later))
.push(self.settings_icon_button(
remove_label,
"M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7",
+9 -4
View File
@@ -7,6 +7,7 @@ use ferese_theme::panel::{Sample, Spacing, frame};
use crate::{App, Element, Message, panel_controls, visuals};
const SAMPLE_AVAILABILITY: Availability = Availability {
tray: true,
capture: true,
activities: true,
media: true,
@@ -47,8 +48,11 @@ impl App {
.push(
row([])
.align_y(Alignment::Center)
.push(self.label("Composition preview", 14.).width(Length::Fill))
.push(self.note("Sample content")),
.push(
self.label(ferese_config::tr!("Composition preview"), 14.)
.width(Length::Fill),
)
.push(self.note(ferese_config::tr!("Sample content"))),
)
.push(
container(self.panel_preview_frame(panel.clone()))
@@ -121,7 +125,7 @@ impl App {
if panel.style == ferese_panel::PanelStyle::Morph {
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
let height = panel.resolved_geometry().height;
let hub = container(self.label("● 10:09", 13.))
let hub = container(self.label(ferese_config::tr!("● 10:09"), 13.))
.id("morph-preview-body")
.center_x(panel.morph.idle_width as f32)
.center_y(height);
@@ -383,7 +387,7 @@ impl App {
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 20.);
let control =
button::custom(container(sample).id(format!("preview-icon:{}", item.id.0)))
.name("Edit Workspaces")
.name(ferese_config::tr!("Edit Workspaces"))
.padding([0, 2])
.height(control_height)
.on_press(Message::PanelSelect(item.id.clone()))
@@ -408,6 +412,7 @@ impl App {
ItemKind::Network => icons::WIFI_FULL,
ItemKind::Audio => icons::VOLUME_HIGH,
ItemKind::Recording => icons::RECORD,
ItemKind::Tray => icons::APPLICATION,
ItemKind::CaptureStatus => icons::DISPLAY,
ItemKind::Notifications => icons::NOTIFICATIONS,
ItemKind::Battery { .. } => icons::BATTERY_75,
+113 -12
View File
@@ -1,6 +1,10 @@
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Page {
Appearance,
General,
Theme,
Tray,
Configuration,
Accessibility,
Wallpaper,
Bar,
@@ -16,8 +20,12 @@ pub enum Page {
}
impl Page {
pub const NAVIGATION: [(&'static str, &'static [Self]); 3] = [
("Desktop", &[Self::Appearance, Self::Wallpaper, Self::Bar]),
pub const NAVIGATION: [(&'static str, &'static [Self]); 5] = [
("Appearance", &[Self::Appearance, Self::General]),
(
"Desktop",
&[Self::Theme, Self::Wallpaper, Self::Bar, Self::Tray],
),
(
"Interaction",
&[Self::Windows, Self::Keyboard, Self::Accessibility],
@@ -32,6 +40,7 @@ impl Page {
Self::Startup,
],
),
("Advanced", &[Self::Configuration]),
];
/// Subpages share one entry in navigation and search.
@@ -66,8 +75,12 @@ impl Page {
}
#[cfg(test)]
pub const ALL: [Self; 13] = [
pub const ALL: [Self; 17] = [
Self::Appearance,
Self::General,
Self::Theme,
Self::Tray,
Self::Configuration,
Self::Accessibility,
Self::Wallpaper,
Self::Bar,
@@ -83,8 +96,15 @@ impl Page {
];
pub fn title(self) -> &'static str {
ferese_config::locale::text(self.title_key())
}
fn title_key(self) -> &'static str {
match self {
Self::Appearance => "Appearance",
Self::General => "General",
Self::Tray => "Tray icons",
Self::Configuration => "All options",
Self::Theme => "Theme",
Self::Accessibility => "Accessibility",
Self::Wallpaper => "Wallpaper",
Self::Bar => "Panel & Shell",
@@ -101,8 +121,14 @@ impl Page {
}
pub fn subtitle(self) -> &'static str {
match self {
Self::Appearance => "A desktop that feels like yours.",
ferese_config::locale::text(match self {
Self::Appearance => "All Ferese settings, grouped by function.",
Self::General => "Language and desktop preferences.",
Self::Tray => "Choose which status and application icons appear in the panel.",
Self::Configuration => {
"Every supported configuration option, including advanced settings."
}
Self::Theme => "A desktop that feels like yours.",
Self::Accessibility => "Make the desktop easier to see and use.",
Self::Wallpaper => "Set the scene for your workspace.",
Self::Bar => "Arrange your panel and change the shell's appearance.",
@@ -115,7 +141,7 @@ impl Page {
Self::Startup => "Ready when you sign in.",
Self::Displays => "A place for every screen.",
Self::Connections => "Wi-Fi networks and Bluetooth devices.",
}
})
}
pub fn icon(self) -> &'static str {
@@ -123,7 +149,11 @@ impl Page {
Self::Accessibility => {
"M12 3a2 2 0 1 0 0 4a2 2 0 1 0 0-4 M4 9h16 M12 9v6 M12 15l-5 6 M12 15l5 6"
}
Self::Appearance => {
Self::Appearance | Self::General | Self::Configuration => {
"M4 5h16 M4 12h16 M4 19h16 M8 3v4 M16 10v4 M10 17v4"
}
Self::Tray => "M3 5h18v14H3z M6 9h3v3H6z M12 9h3v3h-3z M18 9h.01",
Self::Theme => {
"M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 0-4h-1a1 1 0 0 1 0-2h3a6 6 0 0 0 0-12z M7 9h.01 M10 6h.01 M15 6h.01 M18 10h.01"
}
Self::Wallpaper => "M4 4h16v16H4z M4 16l5-5 4 4 3-3 4 4 M15 8h.01",
@@ -157,11 +187,31 @@ impl Page {
fn matches_own(self, query: &str) -> bool {
let query = query.to_lowercase();
self.title().to_lowercase().contains(&query)
|| self.title_key().to_lowercase().contains(&query)
|| ferese_config::locale::in_language(
ferese_config::locale::Language::Chinese,
self.title_key(),
)
.to_lowercase()
.contains(&query)
|| self.subtitle().to_lowercase().contains(&query)
|| fields(self).iter().any(|f| {
format!("{} {} {}", f.label, f.description, f.path)
.to_lowercase()
.contains(&query)
format!(
"{} {} {} {} {}",
f.label,
f.description,
f.path,
ferese_config::locale::in_language(
ferese_config::locale::Language::Chinese,
&f.label
),
ferese_config::locale::in_language(
ferese_config::locale::Language::Chinese,
&f.description
)
)
.to_lowercase()
.contains(&query)
})
}
}
@@ -375,7 +425,7 @@ pub fn fields(page: Page) -> Vec<Field> {
false,
),
],
Page::Appearance => vec![
Page::Theme => vec![
choice(
"theme.mode",
"Appearance",
@@ -910,6 +960,57 @@ pub fn fields(page: Page) -> Vec<Field> {
"Keep the shortcut guide until you are comfortable with Ferese.",
true,
)],
Page::Startup | Page::Displays | Page::Connections => vec![],
Page::General => vec![choice(
"ui.language",
"Language",
"Changes the Ferese interface language. Applies immediately.",
"auto",
&[
("auto", "Follow system"),
("zh-CN", "简体中文"),
("en", "English"),
],
)],
Page::Tray => vec![
toggle(
"status.tray.enabled",
"Show tray icons",
"Show application icons in panel tray items.",
true,
),
toggle(
"status.tray.show_passive",
"Show passive icons",
"Include applications without active status.",
false,
),
range(
"status.tray.icon_size",
"Tray icon size",
"Size of application tray icons.",
RangeSpec {
default: 16.,
min: 8.,
max: 48.,
step: 1.,
suffix: " px",
integer: true,
},
),
Field::new(
"status.tray.hidden_items",
"Hidden applications",
"Application IDs to hide, separated by spaces.",
Kind::Text {
default: "",
argv: true,
},
),
],
Page::Appearance
| Page::Configuration
| Page::Startup
| Page::Displays
| Page::Connections => vec![],
}
}
+9 -9
View File
@@ -56,7 +56,7 @@ impl App {
let labels = column([])
.spacing(2)
.width(Length::Fill)
.push(self.label("Theme overrides", 13.))
.push(self.label(ferese_config::tr!("Theme overrides"), 13.))
.push(widget::tooltip(
self.label(filename, 11.)
.class(cosmic::theme::Text::Color(palette.muted)),
@@ -106,7 +106,7 @@ impl App {
button::custom(visuals::action_icon("M6 6l12 12M6 18L18 6", palette.muted))
.padding(4)
.on_press(Message::Change(Edit::Unset(path.into()))),
self.label("Remove override", 11.),
self.label(ferese_config::tr!("Remove override"), 11.),
widget::tooltip::Position::Bottom,
));
}
@@ -152,7 +152,7 @@ impl App {
column([])
.spacing(2)
.width(Length::Fill)
.push(self.label("Automatic appearance", 13.))
.push(self.label(ferese_config::tr!("Automatic appearance"), 13.))
.push(self.note(description)),
)
.push(sources),
@@ -221,18 +221,18 @@ impl App {
);
if appearance != Some(Dark) && self.undo.is_some() {
heading = heading.push(self.settings_icon_button(
"Undo",
ferese_config::tr!("Undo"),
"M4 10h8 M4 10V3 M4 10c3-7 16-6 16 3a7 7 0 0 1-7 7",
(!self.saving).then_some(Message::Undo),
));
}
let mut custom_heading = row([])
.align_y(Alignment::Center)
.spacing(6)
.push(self.label("Custom themes", 14.).width(Length::Fill));
let mut custom_heading = row([]).align_y(Alignment::Center).spacing(6).push(
self.label(ferese_config::tr!("Custom themes"), 14.)
.width(Length::Fill),
);
if appearance != Some(Dark) {
custom_heading = custom_heading.push(self.settings_button(
"Import…",
ferese_config::tr!("Import…"),
"M3 7V5h6l2 2h10v13H3z M12 10v7 M9 14l3 3 3-3",
Some(Message::ImportTheme),
false,
+111
View File
@@ -0,0 +1,111 @@
//! Read tray applications through the existing watcher without becoming a host.
use super::{Alignment, App, Element, Length, Message, Task, column, row, set};
use ferese_config::locale::text as t;
#[derive(Clone, Debug)]
pub struct Application {
pub id: String,
pub title: String,
}
async fn applications() -> Result<Vec<Application>, String> {
let connection = zbus::Connection::session()
.await
.map_err(|e| e.to_string())?;
let watcher = zbus::Proxy::new(
&connection,
"org.kde.StatusNotifierWatcher",
"/StatusNotifierWatcher",
"org.kde.StatusNotifierWatcher",
)
.await
.map_err(|e| e.to_string())?;
let addresses: Vec<String> = watcher
.get_property("RegisteredStatusNotifierItems")
.await
.map_err(|e| e.to_string())?;
let mut apps = Vec::new();
for address in addresses.into_iter().take(128) {
let (name, path) = address
.find('/')
.map_or((address.as_str(), "/StatusNotifierItem"), |i| {
(&address[..i], &address[i..])
});
let Ok(proxy) =
zbus::Proxy::new(&connection, name, path, "org.kde.StatusNotifierItem").await
else {
continue;
};
let Ok(id) = proxy.get_property::<String>("Id").await else {
continue;
};
let title = proxy
.get_property::<String>("Title")
.await
.unwrap_or_else(|_| id.clone());
if !apps.iter().any(|a: &Application| a.id == id) {
apps.push(Application { id, title });
}
}
apps.sort_by(|a, b| a.title.cmp(&b.title));
Ok(apps)
}
impl App {
pub(super) fn load_tray_apps(&self) -> Task<Message> {
cosmic::task::future(async { Message::TrayApps(applications().await.unwrap_or_default()) })
}
pub(super) fn tray_application_controls(&self) -> Element<'_, Message> {
let hidden = self
.draft
.item("status.tray.hidden_items")
.and_then(serde_json::Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect::<Vec<_>>()
})
.unwrap_or_default();
let mut apps = self.tray_apps.clone();
for id in &hidden {
if !apps.iter().any(|a| &a.id == id) {
apps.push(Application {
id: id.clone(),
title: id.clone(),
});
}
}
let mut out = column([])
.spacing(8)
.push(self.panel_heading(t("Application icons"), 15.));
for app in apps {
let visible = !hidden.contains(&app.id);
let mut next = hidden.clone();
if visible {
next.push(app.id);
} else {
next.retain(|id| id != &app.id);
}
out = out.push(
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label(app.title, 13.).width(Length::Fill))
.push(
ferese_theme::controls::switch(
visible,
super::visuals::Palette::from_resolved(&self.resolved.presented),
)
.on_press(Message::Change(set(
"status.tray.hidden_items",
serde_json::json!(next),
))),
),
);
}
out.push(self.settings_button(
t("Refresh applications"),
"M3 10a8 8 0 1 1 1 7 M3 3v7h7",
Some(Message::RefreshTrayApps),
false,
))
.into()
}
}
@@ -343,7 +343,7 @@ impl App {
let preview = &self.wallpaper.active;
let mut body = column([])
.spacing(8)
.push(self.label("Current wallpaper", 13.));
.push(self.label(ferese_config::tr!("Current wallpaper"), 13.));
let current: Element<'_, Message> = if let Some(handle) = &preview.handle {
widget::image(handle.clone())
.border_radius(palette.radius.min(10.))
@@ -376,7 +376,7 @@ impl App {
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Apply to", 12.))
.push(self.label(ferese_config::tr!("Apply to"), 12.))
.push(ferese_theme::controls::select(
cosmic::iced::widget::pick_list(
Target::ALL,
@@ -389,7 +389,7 @@ impl App {
))
.push(widget::Space::new().width(Length::Fill))
.push(self.settings_button(
"Choose image…",
ferese_config::tr!("Choose image…"),
"M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01",
Some(Message::PickWallpaper),
false,
@@ -444,10 +444,10 @@ impl App {
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Default wallpapers", 13.))
.push(self.label(ferese_config::tr!("Default wallpapers"), 13.))
.push(widget::Space::new().width(Length::Fill))
.push(self.settings_button(
"Match appearance",
ferese_config::tr!("Match appearance"),
"M20 7v5h-5 M4 17v-5h5 M6 7a7 7 0 0 1 12-2l2 2 M18 17a7 7 0 0 1-12 2l-2-2",
Some(Message::RestoreDefaultWallpapers),
false,