Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
@@ -1,4 +1,5 @@
|
||||
use cosmic::{iced::Color, widget::image};
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::widget::image;
|
||||
use ferese_config::Document;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -89,11 +90,7 @@ impl Appearance {
|
||||
}
|
||||
|
||||
pub fn clock_format(&self) -> &'static str {
|
||||
if self.twelve_hour {
|
||||
"%I:%M %p"
|
||||
} else {
|
||||
"%H:%M"
|
||||
}
|
||||
if self.twelve_hour { "%I:%M %p" } else { "%H:%M" }
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> cosmic::Theme {
|
||||
@@ -112,14 +109,12 @@ impl Appearance {
|
||||
}
|
||||
|
||||
fn account_picture(user: &str) -> Option<image::Handle> {
|
||||
let mut candidates =
|
||||
vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)];
|
||||
let mut candidates = vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)];
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
candidates.push(std::path::PathBuf::from(home).join(".face"));
|
||||
}
|
||||
candidates.into_iter().find_map(|path| {
|
||||
let pixels =
|
||||
cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?;
|
||||
let pixels = cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?;
|
||||
let side = pixels.width().min(pixels.height());
|
||||
if side == 0 {
|
||||
return None;
|
||||
@@ -132,13 +127,11 @@ fn account_picture(user: &str) -> Option<image::Handle> {
|
||||
side,
|
||||
)
|
||||
.to_image();
|
||||
let mut pixels =
|
||||
::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3);
|
||||
let mut pixels = ::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3);
|
||||
// Clip the decoded thumbnail, so the avatar stays circular even on
|
||||
// renderers that do not support rounded image clipping.
|
||||
for (x, y, pixel) in pixels.enumerate_pixels_mut() {
|
||||
let distance =
|
||||
((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt();
|
||||
let distance = ((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt();
|
||||
let coverage = (128. - distance).clamp(0., 1.);
|
||||
pixel[3] = (pixel[3] as f32 * coverage).round() as u8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! PAM stays on a worker thread. No password is placed in argv, logs or IPC.
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[repr(C)]
|
||||
@@ -14,8 +15,7 @@ struct Response {
|
||||
}
|
||||
#[repr(C)]
|
||||
struct Conversation {
|
||||
callback:
|
||||
unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int,
|
||||
callback: unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int,
|
||||
data: *mut c_void,
|
||||
}
|
||||
struct Credentials {
|
||||
@@ -39,8 +39,7 @@ unsafe extern "C" fn converse(
|
||||
unsafe {
|
||||
*out = std::ptr::null_mut();
|
||||
let credentials = &mut *(data as *mut Credentials);
|
||||
let responses =
|
||||
libc::calloc(count as usize, std::mem::size_of::<Response>()) as *mut Response;
|
||||
let responses = libc::calloc(count as usize, std::mem::size_of::<Response>()) as *mut Response;
|
||||
if responses.is_null() {
|
||||
return CONV_ERR;
|
||||
}
|
||||
@@ -51,13 +50,11 @@ unsafe extern "C" fn converse(
|
||||
} else {
|
||||
match (*msg).style {
|
||||
1 => {
|
||||
credentials.password_prompts =
|
||||
credentials.password_prompts.saturating_add(1);
|
||||
(credentials.password_prompts == 1)
|
||||
.then_some(credentials.password.as_slice())
|
||||
credentials.password_prompts = credentials.password_prompts.saturating_add(1);
|
||||
(credentials.password_prompts == 1).then_some(credentials.password.as_slice())
|
||||
} // Never send the password as a second-factor response.
|
||||
2 => Some(credentials.user.as_bytes_with_nul()), // PAM_PROMPT_ECHO_ON
|
||||
3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO
|
||||
3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@@ -65,11 +62,7 @@ unsafe extern "C" fn converse(
|
||||
if !bytes.is_empty() {
|
||||
let allocation = libc::malloc(bytes.len()) as *mut c_char;
|
||||
if !allocation.is_null() {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
bytes.as_ptr(),
|
||||
allocation.cast(),
|
||||
bytes.len(),
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), allocation.cast(), bytes.len());
|
||||
(*responses.add(i)).text = allocation;
|
||||
continue;
|
||||
}
|
||||
@@ -139,12 +132,7 @@ fn authenticate_with(user: &str, password: &str, config: Option<&CStr>) -> bool
|
||||
let Ok(library) = libloading::Library::new("libpam.so.0") else {
|
||||
return false;
|
||||
};
|
||||
type Start = unsafe extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const Conversation,
|
||||
*mut *mut c_void,
|
||||
) -> c_int;
|
||||
type Start = unsafe extern "C" fn(*const c_char, *const c_char, *const Conversation, *mut *mut c_void) -> c_int;
|
||||
type StartConf = unsafe extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
|
||||
@@ -4,22 +4,15 @@ mod appearance;
|
||||
mod auth;
|
||||
mod runtime;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::event::{PlatformSpecific, wayland};
|
||||
use cosmic::iced::platform_specific::shell::commands::session_lock;
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{
|
||||
self, Event, Length, Subscription,
|
||||
event::{self, PlatformSpecific, wayland},
|
||||
window,
|
||||
},
|
||||
theme,
|
||||
widget::{self, column, container, image, row},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use cosmic::iced::{Event, Length, Subscription, event, window};
|
||||
use cosmic::widget::{column, container, image, row};
|
||||
use cosmic::{Element, iced, theme, widget};
|
||||
use wayland_client::protocol::wl_output::WlOutput;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
@@ -31,8 +24,7 @@ fn main() -> iced::Result {
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let preview = args == ["--preview"]
|
||||
|| (args.len() == 3 && args[0] == "--preview" && args[1] == "--config");
|
||||
let preview = args == ["--preview"] || (args.len() == 3 && args[0] == "--preview" && args[1] == "--config");
|
||||
let config = if preview && args.len() == 3 {
|
||||
Some(std::path::PathBuf::from(&args[2]))
|
||||
} else {
|
||||
@@ -201,9 +193,7 @@ impl cosmic::Application for Locker {
|
||||
wayland::Event::Output(..) | wayland::Event::SessionLock(..),
|
||||
))
|
||||
| Event::Window(window::Event::Opened { .. })
|
||||
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => {
|
||||
Some(Message::Event(Box::new(event)))
|
||||
}
|
||||
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => Some(Message::Event(Box::new(event))),
|
||||
_ => None,
|
||||
}),
|
||||
iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick),
|
||||
@@ -212,9 +202,7 @@ impl cosmic::Application for Locker {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
))) => {
|
||||
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers))) => {
|
||||
self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK);
|
||||
}
|
||||
Message::Tick => {
|
||||
@@ -222,8 +210,7 @@ impl cosmic::Application for Locker {
|
||||
self.clock = now.strftime(self.appearance.clock_format()).to_string();
|
||||
self.date = now.strftime("%A, %B %-d").to_string();
|
||||
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2))
|
||||
{
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) {
|
||||
self.state = AuthState::Ready;
|
||||
}
|
||||
}
|
||||
@@ -242,10 +229,7 @@ impl cosmic::Application for Locker {
|
||||
return focus();
|
||||
}
|
||||
|
||||
if !self.confirmation.confirmed()
|
||||
|| !self.state.may_submit()
|
||||
|| self.password.is_empty()
|
||||
{
|
||||
if !self.confirmation.confirmed() || !self.state.may_submit() || self.password.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -269,9 +253,10 @@ impl cosmic::Application for Locker {
|
||||
self.status = "Password not accepted. Try again.";
|
||||
return focus();
|
||||
}
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Output(event, output),
|
||||
))) if !self.preview => {
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||
event,
|
||||
output,
|
||||
)))) if !self.preview => {
|
||||
if matches!(event, wayland::OutputEvent::Removed) {
|
||||
if let Some(id) = self.outputs.remove(&output) {
|
||||
return session_lock::destroy_lock_surface(id);
|
||||
@@ -284,9 +269,9 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::SessionLock(event),
|
||||
))) if !self.preview => match event {
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::SessionLock(
|
||||
event,
|
||||
)))) if !self.preview => match event {
|
||||
wayland::SessionLockEvent::Locked => {
|
||||
if self.confirmation.observe() {
|
||||
use std::io::Write;
|
||||
@@ -296,9 +281,11 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
if !self.started {
|
||||
self.started = true;
|
||||
return Task::batch(self.outputs.iter().map(|(output, id)| {
|
||||
session_lock::get_lock_surface(*id, output.clone())
|
||||
}));
|
||||
return Task::batch(
|
||||
self.outputs
|
||||
.iter()
|
||||
.map(|(output, id)| session_lock::get_lock_surface(*id, output.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
wayland::SessionLockEvent::Focused(..) => return focus(),
|
||||
@@ -353,14 +340,7 @@ impl Locker {
|
||||
fn screen_content(&self, size: iced::Size) -> Element<'_, Message> {
|
||||
let avatar_size = (size.height * 0.08).clamp(56., 88.);
|
||||
let clock_size = (size.height * 0.165)
|
||||
.min(
|
||||
size.width
|
||||
* if self.appearance.twelve_hour {
|
||||
0.075
|
||||
} else {
|
||||
0.12
|
||||
},
|
||||
)
|
||||
.min(size.width * if self.appearance.twelve_hour { 0.075 } else { 0.12 })
|
||||
.clamp(40., 180.) as u16;
|
||||
let margin = if size.width < 640. { 24. } else { 36. };
|
||||
let top = (size.height * 0.09).max(24.);
|
||||
@@ -368,9 +348,7 @@ impl Locker {
|
||||
let mut input = widget::text_input("Password", self.password.as_str())
|
||||
.password()
|
||||
.padding([6, 12])
|
||||
.style(ferese_theme::controls::lock_input(
|
||||
a.radius, a.accent, a.panel,
|
||||
))
|
||||
.style(ferese_theme::controls::lock_input(a.radius, a.accent, a.panel))
|
||||
.font(a.font)
|
||||
.id(widget::Id::new("password"));
|
||||
|
||||
@@ -422,15 +400,8 @@ impl Locker {
|
||||
self.label(&self.user, 16).class(iced::Color::WHITE),
|
||||
widget::Space::new().height(6),
|
||||
input,
|
||||
self.label(
|
||||
if self.caps_lock {
|
||||
"Caps Lock is on"
|
||||
} else {
|
||||
self.status
|
||||
},
|
||||
12
|
||||
)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.68)),
|
||||
self.label(if self.caps_lock { "Caps Lock is on" } else { self.status }, 12)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.68)),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_x(iced::Alignment::Center)
|
||||
@@ -453,17 +424,10 @@ impl Locker {
|
||||
}
|
||||
|
||||
if a.show_date {
|
||||
clock = clock.push(
|
||||
self.label(&self.date, 18)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.72)),
|
||||
);
|
||||
clock = clock.push(self.label(&self.date, 18).class(iced::Color::WHITE.scale_alpha(0.72)));
|
||||
}
|
||||
|
||||
let clock_height = if a.show_clock {
|
||||
clock_size as f32 * 1.3
|
||||
} else {
|
||||
0.
|
||||
} + if a.show_date { 24. } else { 0. };
|
||||
let clock_height = if a.show_clock { clock_size as f32 * 1.3 } else { 0. } + if a.show_date { 24. } else { 0. };
|
||||
let gap = (size.height * 0.44 - top - clock_height).max(24.);
|
||||
let foreground = container(
|
||||
column![
|
||||
@@ -497,14 +461,12 @@ impl Locker {
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
widget::container::Style {
|
||||
text_color: Some(iced::Color::WHITE.scale_alpha(0.65)),
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
0.025, 0.035, 0.06, a.dim,
|
||||
))),
|
||||
..Default::default()
|
||||
}
|
||||
.class(theme::Container::custom(move |_| widget::container::Style {
|
||||
text_color: Some(iced::Color::WHITE.scale_alpha(0.65)),
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
0.025, 0.035, 0.06, a.dim,
|
||||
))),
|
||||
..Default::default()
|
||||
}));
|
||||
let background: Element<'_, Message> = image(a.wallpaper.clone())
|
||||
.width(Length::Fill)
|
||||
@@ -524,10 +486,7 @@ fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
|
||||
|
||||
fn ferese_symbol() -> widget::icon::Icon {
|
||||
widget::icon(
|
||||
widget::icon::from_svg_bytes(
|
||||
include_bytes!("../../../packaging/icons/ferese.svg").as_slice(),
|
||||
)
|
||||
.symbolic(true),
|
||||
widget::icon::from_svg_bytes(include_bytes!("../../../packaging/icons/ferese.svg").as_slice()).symbolic(true),
|
||||
)
|
||||
.size(30)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
//! Startup handshake: the invoking command exits successfully only after the
|
||||
//! compositor's confirmation. The UI child remains alive for authentication.
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
process::{Command, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, QueueHandle,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::wl_registry,
|
||||
};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::wl_registry;
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle};
|
||||
struct Registry;
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for Registry {
|
||||
fn event(
|
||||
@@ -23,10 +20,8 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for Registry {
|
||||
}
|
||||
}
|
||||
pub fn check_protocol() -> Result<(), String> {
|
||||
let connection =
|
||||
Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?;
|
||||
let (globals, _) = registry_queue_init::<Registry>(&connection)
|
||||
.map_err(|e| format!("Wayland registry: {e}"))?;
|
||||
let connection = Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?;
|
||||
let (globals, _) = registry_queue_init::<Registry>(&connection).map_err(|e| format!("Wayland registry: {e}"))?;
|
||||
if globals
|
||||
.contents()
|
||||
.clone_list()
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
mod prompt;
|
||||
mod session;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const AGENT_PATH: &str = "/dev/ferese/PolkitAgent";
|
||||
@@ -45,9 +42,8 @@ impl Agent {
|
||||
cookie: String,
|
||||
identities: Vec<Identity>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let uid = choose_identity(&identities).ok_or_else(|| {
|
||||
zbus::fdo::Error::Failed("No supported authentication identity".into())
|
||||
})?;
|
||||
let uid = choose_identity(&identities)
|
||||
.ok_or_else(|| zbus::fdo::Error::Failed("No supported authentication identity".into()))?;
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
|
||||
{
|
||||
@@ -60,9 +56,7 @@ impl Agent {
|
||||
|
||||
let result = tokio::task::spawn_blocking({
|
||||
let cookie = cookie.clone();
|
||||
move || {
|
||||
session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
|
||||
}
|
||||
move || session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?;
|
||||
@@ -116,10 +110,7 @@ fn session_subject() -> Result<(String, HashMap<String, Value<'static>>), String
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut details = HashMap::new();
|
||||
details.insert("pid".to_owned(), Value::from(std::process::id()));
|
||||
details.insert(
|
||||
"uid".to_owned(),
|
||||
Value::from(unsafe { libc::geteuid() } as i32),
|
||||
);
|
||||
details.insert("uid".to_owned(), Value::from(unsafe { libc::geteuid() } as i32));
|
||||
details.insert("start-time".to_owned(), Value::from(started));
|
||||
Ok(("unix-process".into(), details))
|
||||
}
|
||||
@@ -130,15 +121,11 @@ async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.serve_at(AGENT_PATH, Agent::default())?
|
||||
.build()
|
||||
.await?;
|
||||
let authority =
|
||||
zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?;
|
||||
let authority = zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?;
|
||||
let locale = std::env::var("LANG").unwrap_or_default();
|
||||
|
||||
authority
|
||||
.call::<_, _, ()>(
|
||||
"RegisterAuthenticationAgent",
|
||||
&(subject, locale, AGENT_PATH),
|
||||
)
|
||||
.call::<_, _, ()>("RegisterAuthenticationAgent", &(subject, locale, AGENT_PATH))
|
||||
.await?;
|
||||
std::future::pending::<()>().await;
|
||||
|
||||
@@ -174,9 +161,6 @@ mod tests {
|
||||
HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]),
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
choose_identity(&[identity(0), identity(current)]),
|
||||
Some(current)
|
||||
);
|
||||
assert_eq!(choose_identity(&[identity(0), identity(current)]), Some(current));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
use crate::PromptEvent;
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Color, Length, Size, Subscription, window},
|
||||
theme,
|
||||
widget::{self, button, column, container, row},
|
||||
};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{BufRead, BufReader, Write},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Color, Length, Size, Subscription, window};
|
||||
use cosmic::widget::{button, column, container, row};
|
||||
use cosmic::{Element, theme, widget};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::PromptEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Appearance {
|
||||
surface: Color,
|
||||
@@ -103,9 +100,7 @@ fn content_height(description: &str, status: Option<&str>) -> f32 {
|
||||
.lines()
|
||||
.map(|line| line.chars().count().div_ceil(48).max(1))
|
||||
.sum();
|
||||
let status_height = status.map_or(0, |message| {
|
||||
14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3)
|
||||
});
|
||||
let status_height = status.map_or(0, |message| 14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3));
|
||||
(214 + 18 * lines.saturating_sub(1) + status_height).min(360) as f32
|
||||
}
|
||||
|
||||
@@ -163,10 +158,7 @@ fn focus() -> Task<Message> {
|
||||
|
||||
impl Prompt {
|
||||
fn resize_to_content(&mut self) -> Task<Message> {
|
||||
let height = content_height(
|
||||
&self.description,
|
||||
self.error.as_deref().or(self.info.as_deref()),
|
||||
);
|
||||
let height = content_height(&self.description, self.error.as_deref().or(self.info.as_deref()));
|
||||
if (height - self.height).abs() < 1.0 {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -180,12 +172,7 @@ impl Prompt {
|
||||
|
||||
impl cosmic::Application for Prompt {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = (
|
||||
Arc<Mutex<VecDeque<PromptEvent>>>,
|
||||
String,
|
||||
String,
|
||||
Appearance,
|
||||
);
|
||||
type Flags = (Arc<Mutex<VecDeque<PromptEvent>>>, String, String, Appearance);
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "dev.ferese.Authentication";
|
||||
|
||||
@@ -198,21 +185,14 @@ impl cosmic::Application for Prompt {
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<cosmic::iced::theme::Style> {
|
||||
Some(
|
||||
self.appearance
|
||||
.palette()
|
||||
.application_style(if self.material.is_some() {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
} else {
|
||||
self.appearance.surface
|
||||
}),
|
||||
)
|
||||
Some(self.appearance.palette().application_style(if self.material.is_some() {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
} else {
|
||||
self.appearance.surface
|
||||
}))
|
||||
}
|
||||
|
||||
fn init(
|
||||
mut core: Core,
|
||||
(incoming, description, user, appearance): Self::Flags,
|
||||
) -> (Self, Task<Message>) {
|
||||
fn init(mut core: Core, (incoming, description, user, appearance): Self::Flags) -> (Self, Task<Message>) {
|
||||
core.window.show_headerbar = false;
|
||||
core.window.border_padding = Some(0);
|
||||
core.window.content_container = false;
|
||||
@@ -257,11 +237,8 @@ impl cosmic::Application for Prompt {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::WindowOpened(id) => {
|
||||
return cosmic::iced::window::run(
|
||||
id,
|
||||
ferese_theme::material::ModalMaterial::attach,
|
||||
)
|
||||
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
||||
return cosmic::iced::window::run(id, ferese_theme::material::ModalMaterial::attach)
|
||||
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
||||
}
|
||||
Message::MaterialAttached(result) => {
|
||||
self.material = result.ok();
|
||||
@@ -323,9 +300,7 @@ impl cosmic::Application for Prompt {
|
||||
.id(widget::Id::new("auth-response"))
|
||||
.font(palette.font)
|
||||
.padding([7, 12])
|
||||
.style(ferese_theme::controls::authentication_input(
|
||||
palette.palette(),
|
||||
));
|
||||
.style(ferese_theme::controls::authentication_input(palette.palette()));
|
||||
if !self.echo {
|
||||
input = input.password();
|
||||
}
|
||||
@@ -347,10 +322,7 @@ impl cosmic::Application for Prompt {
|
||||
.height(40)
|
||||
.center_x(40)
|
||||
.center_y(40)
|
||||
.class(ferese_theme::controls::surface(
|
||||
palette.accent.scale_alpha(0.13),
|
||||
12.,
|
||||
)),
|
||||
.class(ferese_theme::controls::surface(palette.accent.scale_alpha(0.13), 12.,)),
|
||||
column![
|
||||
ferese_theme::text("Authentication required", palette.font).size(18),
|
||||
ferese_theme::text(format!("Confirm as {}", self.user), palette.font)
|
||||
@@ -384,10 +356,7 @@ impl cosmic::Application for Prompt {
|
||||
body = body.push(
|
||||
row![
|
||||
button::custom(ferese_theme::text("Cancel", palette.font).size(13))
|
||||
.class(ferese_theme::controls::button_style(
|
||||
palette.palette(),
|
||||
false
|
||||
))
|
||||
.class(ferese_theme::controls::button_style(palette.palette(), false))
|
||||
.padding([8, 14])
|
||||
.on_press(Message::Cancel),
|
||||
action
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
use crate::PromptEvent;
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use glib::gobject_ffi;
|
||||
use libloading::Library;
|
||||
use std::{
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
process::{Child, ChildStdin, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Sender},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::PromptEvent;
|
||||
|
||||
enum SessionEvent {
|
||||
Request(String, bool),
|
||||
Info(String),
|
||||
@@ -38,21 +36,15 @@ impl Api {
|
||||
unsafe {
|
||||
let agent = Library::new("libpolkit-agent-1.so.0").map_err(|e| e.to_string())?;
|
||||
let polkit = Library::new("libpolkit-gobject-1.so.0").map_err(|e| e.to_string())?;
|
||||
let user_new = *polkit
|
||||
.get(b"polkit_unix_user_new\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let session_new = *agent
|
||||
.get(b"polkit_agent_session_new\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let user_new = *polkit.get(b"polkit_unix_user_new\0").map_err(|e| e.to_string())?;
|
||||
let session_new = *agent.get(b"polkit_agent_session_new\0").map_err(|e| e.to_string())?;
|
||||
let initiate = *agent
|
||||
.get(b"polkit_agent_session_initiate\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let response = *agent
|
||||
.get(b"polkit_agent_session_response\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let cancel = *agent
|
||||
.get(b"polkit_agent_session_cancel\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let cancel = *agent.get(b"polkit_agent_session_cancel\0").map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Self {
|
||||
_agent: agent,
|
||||
@@ -72,9 +64,7 @@ pub fn check() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn username(uid: u32) -> String {
|
||||
let output = Command::new("getent")
|
||||
.args(["passwd", &uid.to_string()])
|
||||
.output();
|
||||
let output = Command::new("getent").args(["passwd", &uid.to_string()]).output();
|
||||
output
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
@@ -122,32 +112,21 @@ fn prompt_process(
|
||||
Ok((child, writer, rx))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn request(
|
||||
_session: Object,
|
||||
prompt: *const c_char,
|
||||
echo: c_int,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
unsafe extern "C" fn request(_session: Object, prompt: *const c_char, echo: c_int, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let prompt = unsafe { CStr::from_ptr(prompt) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let prompt = unsafe { CStr::from_ptr(prompt) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Request(prompt, echo != 0));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn info(_session: Object, text: *const c_char, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let text = unsafe { CStr::from_ptr(text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Info(text));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn error(_session: Object, text: *const c_char, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let text = unsafe { CStr::from_ptr(text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Error(text));
|
||||
}
|
||||
|
||||
@@ -156,12 +135,7 @@ unsafe extern "C" fn completed(_session: Object, success: c_int, data: *mut c_vo
|
||||
let _ = tx.send(SessionEvent::Completed(success != 0));
|
||||
}
|
||||
|
||||
unsafe fn connect(
|
||||
session: Object,
|
||||
name: &'static CStr,
|
||||
callback: gobject_ffi::GCallback,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
unsafe fn connect(session: Object, name: &'static CStr, callback: gobject_ffi::GCallback, data: *mut c_void) {
|
||||
unsafe {
|
||||
gobject_ffi::g_signal_connect_data(
|
||||
session.cast(),
|
||||
@@ -215,7 +189,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -226,7 +200,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -237,7 +211,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
completed as unsafe extern "C" fn(Object, c_int, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -276,10 +250,7 @@ fn run_session(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !stopped
|
||||
&& (cancelled.load(Ordering::Acquire)
|
||||
|| child.try_wait().map_err(|e| e.to_string())?.is_some())
|
||||
{
|
||||
if !stopped && (cancelled.load(Ordering::Acquire) || child.try_wait().map_err(|e| e.to_string())?.is_some()) {
|
||||
unsafe { (api.cancel)(session) };
|
||||
stopped = true;
|
||||
}
|
||||
@@ -293,12 +264,7 @@ fn run_session(
|
||||
Ok(outcome.filter(|_| !stopped))
|
||||
}
|
||||
|
||||
pub fn authenticate(
|
||||
uid: u32,
|
||||
message: String,
|
||||
cookie: String,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> Result<bool, String> {
|
||||
pub fn authenticate(uid: u32, message: String, cookie: String, cancelled: Arc<AtomicBool>) -> Result<bool, String> {
|
||||
let api = Api::load()?;
|
||||
let cookie = CString::new(cookie).map_err(|_| "Invalid authentication cookie")?;
|
||||
let context = glib::MainContext::new();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{process::Command, sync::OnceLock};
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub fn families() -> &'static [String] {
|
||||
static FAMILIES: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
+123
-192
@@ -4,22 +4,20 @@ mod store;
|
||||
mod visuals;
|
||||
mod watch;
|
||||
|
||||
use cosmic::{
|
||||
ApplicationExt, Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Length},
|
||||
widget::{self, button, column, container, row, scrollable, slider, text_input, toggler},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, column, container, row, scrollable, slider, text_input, toggler};
|
||||
use cosmic::{ApplicationExt, Element, widget};
|
||||
use schema::{Field, Kind, Page};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use store::{Edit, Snapshot, set};
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let path = match args.next() {
|
||||
Some(arg) if arg == "--config" => {
|
||||
PathBuf::from(args.next().expect("--config requires a path"))
|
||||
}
|
||||
Some(arg) if arg == "--config" => PathBuf::from(args.next().expect("--config requires a path")),
|
||||
Some(_) => {
|
||||
eprintln!("Usage: ferese-settings [--config PATH]");
|
||||
return Ok(());
|
||||
@@ -195,9 +193,7 @@ impl cosmic::Application for App {
|
||||
return self.change(set(&format!("desktop_widgets.notes.{index}.text"), value));
|
||||
}
|
||||
}
|
||||
Message::AddNote
|
||||
if !self.saving && self.draft.records("desktop_widgets.notes") < 32 =>
|
||||
{
|
||||
Message::AddNote if !self.saving && self.draft.records("desktop_widgets.notes") < 32 => {
|
||||
let id = format!(
|
||||
"note-{}",
|
||||
std::time::SystemTime::now()
|
||||
@@ -223,9 +219,7 @@ impl cosmic::Application for App {
|
||||
}
|
||||
Message::DragWindow => return self.core.drag(None),
|
||||
Message::ExternalConfig(result) => match result {
|
||||
Ok(snapshot)
|
||||
if snapshot.source == self.current.source
|
||||
|| snapshot.source == self.draft.source => {}
|
||||
Ok(snapshot) if snapshot.source == self.current.source || snapshot.source == self.draft.source => {}
|
||||
Ok(snapshot) => {
|
||||
if self.saving
|
||||
|| self.note_editors.values().any(|editor| editor.dirty)
|
||||
@@ -233,9 +227,7 @@ impl cosmic::Application for App {
|
||||
|| !self.ranges.is_empty()
|
||||
|| !self.pending.is_empty()
|
||||
{
|
||||
self.status =
|
||||
"Config changed externally · Reload when your edits are finished"
|
||||
.into();
|
||||
self.status = "Config changed externally · Reload when your edits are finished".into();
|
||||
} else {
|
||||
self.current = snapshot.clone();
|
||||
self.draft = snapshot;
|
||||
@@ -307,9 +299,7 @@ impl cosmic::Application for App {
|
||||
match shlex::split(&value).filter(|v| !v.is_empty()) {
|
||||
Some(args) => set(
|
||||
&field.path,
|
||||
args.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect::<Vec<_>>(),
|
||||
args.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
|
||||
),
|
||||
None => {
|
||||
self.error = Some("Enter a program and arguments with balanced quotes. Commands are not run through a shell.".into());
|
||||
@@ -348,11 +338,7 @@ impl cosmic::Application for App {
|
||||
}
|
||||
.into();
|
||||
|
||||
return Task::batch([
|
||||
self.update_theme(),
|
||||
self.flush(),
|
||||
self.load_thumbnail(),
|
||||
]);
|
||||
return Task::batch([self.update_theme(), self.flush(), self.load_thumbnail()]);
|
||||
}
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
@@ -410,13 +396,20 @@ impl cosmic::Application for App {
|
||||
"--file-filter=All files | *",
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("Could not open the file chooser: {error}. Install zenity or enter an image path."))
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Could not open the file chooser: {error}. Install zenity or enter an image path."
|
||||
)
|
||||
})
|
||||
.and_then(|output| {
|
||||
if output.status.code() == Some(1) {
|
||||
return Ok(None); // User cancelled.
|
||||
}
|
||||
if !output.status.success() {
|
||||
return Err(format!("File chooser failed: {}", String::from_utf8_lossy(&output.stderr).trim()));
|
||||
return Err(format!(
|
||||
"File chooser failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
let path = String::from_utf8(output.stdout)
|
||||
.map_err(|_| "The image path is not valid UTF-8.".to_owned())?;
|
||||
@@ -458,31 +451,23 @@ impl cosmic::Application for App {
|
||||
return self.load_thumbnail();
|
||||
}
|
||||
Message::NewCommand(value) => self.new_command = value,
|
||||
Message::AddCommand if !self.saving => {
|
||||
match shlex::split(&self.new_command).filter(|a| !a.is_empty()) {
|
||||
Some(args) => {
|
||||
self.new_command.clear();
|
||||
return self.change(Edit::Add(
|
||||
"autostart".into(),
|
||||
vec![
|
||||
(
|
||||
"command".into(),
|
||||
args.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
),
|
||||
("enabled".into(), true.into()),
|
||||
("restart".into(), true.into()),
|
||||
],
|
||||
));
|
||||
}
|
||||
None => {
|
||||
self.error =
|
||||
Some("Enter a program and arguments with balanced quotes.".into())
|
||||
}
|
||||
Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) {
|
||||
Some(args) => {
|
||||
self.new_command.clear();
|
||||
return self.change(Edit::Add(
|
||||
"autostart".into(),
|
||||
vec![
|
||||
(
|
||||
"command".into(),
|
||||
args.into_iter().map(serde_json::Value::from).collect::<Vec<_>>().into(),
|
||||
),
|
||||
("enabled".into(), true.into()),
|
||||
("restart".into(), true.into()),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
None => self.error = Some("Enter a program and arguments with balanced quotes.".into()),
|
||||
},
|
||||
Message::AddSwipe(keys) if !self.saving => {
|
||||
let (action, argument) = match keys {
|
||||
"Swipe3Up" => ("workspace-next", None),
|
||||
@@ -490,10 +475,7 @@ impl cosmic::Application for App {
|
||||
"Swipe3Left" => ("focus", Some("right")),
|
||||
_ => ("focus", Some("left")),
|
||||
};
|
||||
let mut fields = vec![
|
||||
("keys".into(), keys.into()),
|
||||
("action".into(), action.into()),
|
||||
];
|
||||
let mut fields = vec![("keys".into(), keys.into()), ("action".into(), action.into())];
|
||||
|
||||
if let Some(argument) = argument {
|
||||
fields.push(("argument".into(), argument.into()));
|
||||
@@ -596,8 +578,7 @@ impl cosmic::Application for App {
|
||||
|
||||
if !self.search.is_empty() {
|
||||
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
||||
body = body
|
||||
.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||
body = body.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||
}
|
||||
for page in Page::ALL.into_iter().filter(|p| p.matches(&self.search)) {
|
||||
body = body.push(
|
||||
@@ -682,8 +663,7 @@ impl cosmic::Application for App {
|
||||
.unwrap_or("Choose an image to preview your wallpaper.")
|
||||
}));
|
||||
}
|
||||
body =
|
||||
body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||
body = body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||
}
|
||||
let fields = schema::fields(self.page);
|
||||
|
||||
@@ -692,30 +672,21 @@ impl cosmic::Application for App {
|
||||
for field in fields {
|
||||
group = group.push(self.field(field));
|
||||
}
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
|
||||
match self.page {
|
||||
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(
|
||||
button::standard("Preview lock screen").on_press(Message::PreviewLock),
|
||||
);
|
||||
body = body.push(button::standard("Preview lock screen").on_press(Message::PreviewLock));
|
||||
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."));
|
||||
}
|
||||
Page::Desktop => {
|
||||
let can_change_list =
|
||||
!self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||||
let can_change_list = !self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||||
body = body.push(self.label("Sticky notes", 16.));
|
||||
body = body.push(self.note("Edit here; notes save after you pause typing. Desktop cards stay behind windows and are click-through."));
|
||||
for index in 0..self.draft.records("desktop_widgets.notes") {
|
||||
let id = self
|
||||
.draft
|
||||
.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||
let id = self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||
let mut group = column([]).spacing(8);
|
||||
|
||||
for field in schema::note_fields(index) {
|
||||
@@ -729,23 +700,13 @@ impl cosmic::Application for App {
|
||||
.height(160)
|
||||
.font(self.font)
|
||||
.size(14.)
|
||||
.on_action(move |action| {
|
||||
Message::NoteAction(id.clone(), action)
|
||||
}),
|
||||
.on_action(move |action| Message::NoteAction(id.clone(), action)),
|
||||
);
|
||||
}
|
||||
group =
|
||||
group.push(button::destructive("Remove note").on_press_maybe(
|
||||
can_change_list.then_some(Message::Remove(
|
||||
"desktop_widgets.notes".into(),
|
||||
index,
|
||||
)),
|
||||
));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(12)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
group = group.push(button::destructive("Remove note").on_press_maybe(
|
||||
can_change_list.then_some(Message::Remove("desktop_widgets.notes".into(), index)),
|
||||
));
|
||||
body = body.push(container(group).padding(12).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
body = body.push(
|
||||
button::standard("Add note").on_press_maybe(
|
||||
@@ -783,19 +744,12 @@ impl cosmic::Application for App {
|
||||
Kind::Toggle(true),
|
||||
)))
|
||||
.push(
|
||||
container(
|
||||
button::destructive("Remove item").on_press_maybe(
|
||||
(!self.saving)
|
||||
.then_some(Message::Remove("autostart".into(), index)),
|
||||
),
|
||||
)
|
||||
container(button::destructive("Remove item").on_press_maybe(
|
||||
(!self.saving).then_some(Message::Remove("autostart".into(), index)),
|
||||
))
|
||||
.padding(12),
|
||||
);
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
body = body.push(
|
||||
row([])
|
||||
@@ -804,12 +758,9 @@ impl cosmic::Application for App {
|
||||
text_input("Program and arguments", self.new_command.clone())
|
||||
.on_input(Message::NewCommand),
|
||||
)
|
||||
.push(
|
||||
button::standard("Add login item").on_press_maybe(
|
||||
(!self.saving && !self.new_command.trim().is_empty())
|
||||
.then_some(Message::AddCommand),
|
||||
),
|
||||
),
|
||||
.push(button::standard("Add login item").on_press_maybe(
|
||||
(!self.saving && !self.new_command.trim().is_empty()).then_some(Message::AddCommand),
|
||||
)),
|
||||
);
|
||||
}
|
||||
Page::Shortcuts => {
|
||||
@@ -828,10 +779,10 @@ impl cosmic::Application for App {
|
||||
});
|
||||
|
||||
if !exists {
|
||||
gestures =
|
||||
gestures.push(button::standard(label).on_press_maybe(
|
||||
(!self.saving).then_some(Message::AddSwipe(keys)),
|
||||
));
|
||||
gestures = gestures.push(
|
||||
button::standard(label)
|
||||
.on_press_maybe((!self.saving).then_some(Message::AddSwipe(keys))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,11 +810,7 @@ impl cosmic::Application for App {
|
||||
"Command name, direction, or action argument.",
|
||||
"",
|
||||
)));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
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."));
|
||||
@@ -873,29 +820,44 @@ impl cosmic::Application for App {
|
||||
body = body.push(self.note("Display profiles update connected outputs live. Keep your config open for adding profiles or changing display positions."));
|
||||
for profile in 0..self.draft.records("output_profiles") {
|
||||
let prefix = format!("output_profiles.{profile}");
|
||||
body = body.push(
|
||||
self.label(
|
||||
self.draft
|
||||
.string(&format!("{prefix}.name"), "Display profile"),
|
||||
17.,
|
||||
),
|
||||
);
|
||||
body =
|
||||
body.push(self.label(self.draft.string(&format!("{prefix}.name"), "Display profile"), 17.));
|
||||
|
||||
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
||||
let prefix = format!("{prefix}.outputs.{output}");
|
||||
let group = column([]).spacing(1)
|
||||
.push(self.field(schema::text(format!("{prefix}.match"), "Display", "Output name or matching pattern.", "")))
|
||||
.push(self.field(schema::text(format!("{prefix}.mode"), "Resolution", "For example: 2560x1440@60. Leave unchanged to retain automatic selection.", "")))
|
||||
.push(self.field(schema::range(format!("{prefix}.scale"), "Scale", "Logical size of text and controls.", 1., 0.75, 3., 0.25, "×", false)));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let group = column([])
|
||||
.spacing(1)
|
||||
.push(self.field(schema::text(
|
||||
format!("{prefix}.match"),
|
||||
"Display",
|
||||
"Output name or matching pattern.",
|
||||
"",
|
||||
)))
|
||||
.push(self.field(schema::text(
|
||||
format!("{prefix}.mode"),
|
||||
"Resolution",
|
||||
"For example: 2560x1440@60. Leave unchanged to retain automatic selection.",
|
||||
"",
|
||||
)))
|
||||
.push(self.field(schema::range(
|
||||
format!("{prefix}.scale"),
|
||||
"Scale",
|
||||
"Logical size of text and controls.",
|
||||
1.,
|
||||
0.75,
|
||||
3.,
|
||||
0.25,
|
||||
"×",
|
||||
false,
|
||||
)));
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
}
|
||||
if self.draft.records("output_profiles") == 0 {
|
||||
body = body.push(self.note("No saved profiles. Ferese currently configures connected displays automatically."));
|
||||
body =
|
||||
body.push(self.note(
|
||||
"No saved profiles. Ferese currently configures connected displays automatically.",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -947,14 +909,7 @@ impl cosmic::Application for App {
|
||||
},
|
||||
palette.muted,
|
||||
),
|
||||
self.label(
|
||||
if self.saving {
|
||||
"Saving…"
|
||||
} else {
|
||||
&self.status
|
||||
},
|
||||
11.,
|
||||
),
|
||||
self.label(if self.saving { "Saving…" } else { &self.status }, 11.),
|
||||
widget::tooltip::Position::Top,
|
||||
))
|
||||
.push(widget::Space::new().width(Length::Fill))
|
||||
@@ -986,21 +941,16 @@ impl cosmic::Application for App {
|
||||
.push(sidebar)
|
||||
.push(
|
||||
container(
|
||||
container(
|
||||
column([])
|
||||
.push(content.height(Length::Fill))
|
||||
.push(footer)
|
||||
.spacing(6),
|
||||
)
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: 20.,
|
||||
right: 20.,
|
||||
bottom: 8.,
|
||||
left: 20.,
|
||||
})
|
||||
.max_width(840)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
container(column([]).push(content.height(Length::Fill)).push(footer).spacing(6))
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: 20.,
|
||||
right: 20.,
|
||||
bottom: 8.,
|
||||
left: 20.,
|
||||
})
|
||||
.max_width(840)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
@@ -1022,11 +972,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn note_index(&self, id: &str) -> Option<usize> {
|
||||
(0..self.draft.records("desktop_widgets.notes")).find(|index| {
|
||||
self.draft
|
||||
.string(&format!("desktop_widgets.notes.{index}.id"), "note")
|
||||
== id
|
||||
})
|
||||
(0..self.draft.records("desktop_widgets.notes"))
|
||||
.find(|index| self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note") == id)
|
||||
}
|
||||
|
||||
fn sync_notes(&mut self) {
|
||||
@@ -1036,14 +983,11 @@ impl App {
|
||||
let prefix = format!("desktop_widgets.notes.{index}");
|
||||
let id = self.draft.string(&format!("{prefix}.id"), "note");
|
||||
let text = self.draft.string(&format!("{prefix}.text"), "");
|
||||
let editor = self
|
||||
.note_editors
|
||||
.entry(id.clone())
|
||||
.or_insert_with(|| NoteEditor {
|
||||
content: widget::text_editor::Content::with_text(&text),
|
||||
revision: 0,
|
||||
dirty: false,
|
||||
});
|
||||
let editor = self.note_editors.entry(id.clone()).or_insert_with(|| NoteEditor {
|
||||
content: widget::text_editor::Content::with_text(&text),
|
||||
revision: 0,
|
||||
dirty: false,
|
||||
});
|
||||
if !editor.dirty && editor.content.text() != text {
|
||||
editor.content = widget::text_editor::Content::with_text(&text);
|
||||
}
|
||||
@@ -1121,9 +1065,7 @@ impl App {
|
||||
|
||||
fn field(&self, field: Field) -> Element<'static, Message> {
|
||||
let palette = visuals::Palette::from_document(Some(&self.draft.doc));
|
||||
let mut labels = column([])
|
||||
.spacing(3)
|
||||
.push(self.label(field.label.clone(), 13.));
|
||||
let mut labels = column([]).spacing(3).push(self.label(field.label.clone(), 13.));
|
||||
if !field.description.is_empty() {
|
||||
labels = labels.push(
|
||||
self.label(field.description.clone(), 11.)
|
||||
@@ -1205,12 +1147,10 @@ impl App {
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(14)
|
||||
.push(
|
||||
slider(min..=max, value, move |value| {
|
||||
Message::Range(field.clone(), value)
|
||||
})
|
||||
.step(step)
|
||||
.on_release(Message::Release(release))
|
||||
.width(145),
|
||||
slider(min..=max, value, move |value| Message::Range(field.clone(), value))
|
||||
.step(step)
|
||||
.on_release(Message::Release(release))
|
||||
.width(145),
|
||||
)
|
||||
.push(self.label(display, 12.).width(65))
|
||||
.into()
|
||||
@@ -1254,10 +1194,7 @@ impl App {
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(
|
||||
container(widget::Space::new().width(24).height(24))
|
||||
.class(visuals::surface(swatch, 6.)),
|
||||
)
|
||||
.push(container(widget::Space::new().width(24).height(24)).class(visuals::surface(swatch, 6.)))
|
||||
.push(input)
|
||||
.into()
|
||||
} else {
|
||||
@@ -1342,8 +1279,9 @@ impl App {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::Application;
|
||||
|
||||
use super::*;
|
||||
fn app() -> App {
|
||||
App::init(
|
||||
Core::default(),
|
||||
@@ -1361,10 +1299,7 @@ mod tests {
|
||||
app.draft
|
||||
.edit(&Edit::Add(
|
||||
"desktop_widgets.notes".into(),
|
||||
vec![
|
||||
("id".into(), "test".into()),
|
||||
("text".into(), "first\nsecond".into()),
|
||||
],
|
||||
vec![("id".into(), "test".into()), ("text".into(), "first\nsecond".into())],
|
||||
))
|
||||
.unwrap();
|
||||
app.sync_notes();
|
||||
@@ -1377,10 +1312,7 @@ mod tests {
|
||||
let _ = app.update(Message::SaveNote("test".into(), 2));
|
||||
assert!(app.saving);
|
||||
assert!(!app.note_editors["test"].dirty);
|
||||
assert_eq!(
|
||||
app.draft.string("desktop_widgets.notes.0.text", ""),
|
||||
"first\nsecond"
|
||||
);
|
||||
assert_eq!(app.draft.string("desktop_widgets.notes.0.text", ""), "first\nsecond");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1424,8 +1356,7 @@ mod tests {
|
||||
let _ = app.update(Message::ExternalConfig(Ok(external.clone())));
|
||||
assert_eq!(app.current.source, external.source);
|
||||
assert_eq!(app.draft.number("animations.speed", 0.), 0.5);
|
||||
app.inputs
|
||||
.insert("theme.colors.accent".into(), "#ffffff".into());
|
||||
app.inputs.insert("theme.colors.accent".into(), "#ffffff".into());
|
||||
let newer = Snapshot::parse("animations {\n speed 0.8\n}\n".into()).unwrap();
|
||||
let _ = app.update(Message::ExternalConfig(Ok(newer)));
|
||||
assert_eq!(app.current.source, external.source);
|
||||
|
||||
@@ -129,12 +129,7 @@ pub struct Field {
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub fn new(
|
||||
path: impl Into<String>,
|
||||
label: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
pub fn new(path: impl Into<String>, label: impl Into<String>, description: impl Into<String>, kind: Kind) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
label: label.into(),
|
||||
@@ -180,15 +175,7 @@ pub fn text(
|
||||
description: impl Into<String>,
|
||||
default: &'static str,
|
||||
) -> Field {
|
||||
Field::new(
|
||||
path,
|
||||
label,
|
||||
description,
|
||||
Kind::Text {
|
||||
default,
|
||||
argv: false,
|
||||
},
|
||||
)
|
||||
Field::new(path, label, description, Kind::Text { default, argv: false })
|
||||
}
|
||||
|
||||
fn choice(
|
||||
@@ -400,18 +387,8 @@ pub fn note_fields(index: usize) -> Vec<Field> {
|
||||
"Drag the title and edit directly. Off makes the card click-through.",
|
||||
Kind::Toggle(true),
|
||||
),
|
||||
Field::new(
|
||||
format!("{prefix}.enabled"),
|
||||
"Show note",
|
||||
"",
|
||||
Kind::Toggle(true),
|
||||
),
|
||||
text(
|
||||
format!("{prefix}.title"),
|
||||
"Title",
|
||||
"Empty hides the title.",
|
||||
"Note",
|
||||
),
|
||||
Field::new(format!("{prefix}.enabled"), "Show note", "", Kind::Toggle(true)),
|
||||
text(format!("{prefix}.title"), "Title", "Empty hides the title.", "Note"),
|
||||
text(
|
||||
format!("{prefix}.anchor"),
|
||||
"Position",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_config::Document;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub const LIMIT: usize = 60 * 1024;
|
||||
|
||||
@@ -45,10 +44,7 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
pub fn string(&self, path: &str, fallback: &str) -> String {
|
||||
self.item(path)
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(fallback)
|
||||
.to_owned()
|
||||
self.item(path).and_then(Value::as_str).unwrap_or(fallback).to_owned()
|
||||
}
|
||||
|
||||
pub fn number(&self, path: &str, fallback: f64) -> f64 {
|
||||
@@ -60,9 +56,7 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
pub fn records(&self, path: &str) -> usize {
|
||||
self.item(path)
|
||||
.and_then(Value::as_array)
|
||||
.map_or(0, |a| a.len())
|
||||
self.item(path).and_then(Value::as_array).map_or(0, |a| a.len())
|
||||
}
|
||||
|
||||
pub fn argv(&self, path: &str) -> String {
|
||||
@@ -172,9 +166,7 @@ fn save_with(
|
||||
let snapshot = Snapshot::parse(desired.to_owned())?;
|
||||
let unchanged = || -> Result<(), String> {
|
||||
if Snapshot::read(path)?.source != expected {
|
||||
return Err(
|
||||
"Your config changed in another app. Reload settings before saving.".into(),
|
||||
);
|
||||
return Err("Your config changed in another app. Reload settings before saving.".into());
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
@@ -188,17 +180,13 @@ fn save_with(
|
||||
.set_permissions(metadata.permissions())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
staged
|
||||
.write_all(desired.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
staged.write_all(desired.as_bytes()).map_err(|e| e.to_string())?;
|
||||
staged.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
validate(staged.path())?;
|
||||
unchanged()?;
|
||||
// Keep the last working source, including comments and unknown keys.
|
||||
let mut backup = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
|
||||
backup
|
||||
.write_all(expected.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
backup.write_all(expected.as_bytes()).map_err(|e| e.to_string())?;
|
||||
backup.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
backup
|
||||
.persist(path.with_extension("kdl.settings-backup"))
|
||||
@@ -212,8 +200,7 @@ pub fn reload_running() -> bool {
|
||||
let Some(runtime) = std::env::var_os("XDG_RUNTIME_DIR") else {
|
||||
return false;
|
||||
};
|
||||
let Ok(mut socket) =
|
||||
std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock"))
|
||||
let Ok(mut socket) = std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -293,17 +280,11 @@ window-rule app-id="mine" floating=#true
|
||||
|
||||
#[test]
|
||||
fn nested_array_edits_and_login_items_roundtrip() {
|
||||
let mut snapshot = Snapshot::parse(
|
||||
"output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into(),
|
||||
)
|
||||
.unwrap();
|
||||
snapshot
|
||||
.edit(&set("output_profiles.0.outputs.0.scale", 1.75))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snapshot.number("output_profiles.0.outputs.0.scale", 1.),
|
||||
1.75
|
||||
);
|
||||
let mut snapshot =
|
||||
Snapshot::parse("output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into())
|
||||
.unwrap();
|
||||
snapshot.edit(&set("output_profiles.0.outputs.0.scale", 1.75)).unwrap();
|
||||
assert_eq!(snapshot.number("output_profiles.0.outputs.0.scale", 1.), 1.75);
|
||||
snapshot
|
||||
.edit(&Edit::Add(
|
||||
"autostart".into(),
|
||||
@@ -331,28 +312,18 @@ window-rule app-id="mine" floating=#true
|
||||
|
||||
#[test]
|
||||
fn nested_note_lists_keep_existing_clock_settings() {
|
||||
let mut snapshot = Snapshot::parse(
|
||||
"desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut snapshot =
|
||||
Snapshot::parse("desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into()).unwrap();
|
||||
snapshot
|
||||
.edit(&Edit::Add(
|
||||
"desktop_widgets.notes".into(),
|
||||
vec![
|
||||
("id".into(), "first".into()),
|
||||
("text".into(), "one\ntwo".into()),
|
||||
],
|
||||
vec![("id".into(), "first".into()), ("text".into(), "one\ntwo".into())],
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.records("desktop_widgets.notes"), 1);
|
||||
assert!(snapshot.boolean("desktop_widgets.clock.enabled", false));
|
||||
assert_eq!(
|
||||
snapshot.string("desktop_widgets.notes.0.text", ""),
|
||||
"one\ntwo"
|
||||
);
|
||||
snapshot
|
||||
.edit(&Edit::Remove("desktop_widgets.notes".into(), 0))
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.string("desktop_widgets.notes.0.text", ""), "one\ntwo");
|
||||
snapshot.edit(&Edit::Remove("desktop_widgets.notes".into(), 0)).unwrap();
|
||||
assert_eq!(snapshot.records("desktop_widgets.notes"), 0);
|
||||
}
|
||||
|
||||
@@ -364,11 +335,7 @@ window-rule app-id="mine" floating=#true
|
||||
let path = directory.path().join("config.kdl");
|
||||
let validate = |snapshot: &Snapshot| {
|
||||
fs::write(&path, &snapshot.source).unwrap();
|
||||
let result = Command::new(&binary)
|
||||
.arg("--check-config")
|
||||
.arg(&path)
|
||||
.output()
|
||||
.unwrap();
|
||||
let result = Command::new(&binary).arg("--check-config").arg(&path).output().unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}\n{}",
|
||||
@@ -386,9 +353,7 @@ window-rule app-id="mine" floating=#true
|
||||
let values: Vec<Value> = match field.kind {
|
||||
Kind::Font => vec!["".into(), "sans-serif".into()],
|
||||
Kind::Toggle(_) => vec![true.into(), false.into()],
|
||||
Kind::Range {
|
||||
min, max, integer, ..
|
||||
} => {
|
||||
Kind::Range { min, max, integer, .. } => {
|
||||
if integer {
|
||||
vec![(min as i64).into(), (max as i64).into()]
|
||||
} else {
|
||||
@@ -396,9 +361,7 @@ window-rule app-id="mine" floating=#true
|
||||
}
|
||||
}
|
||||
Kind::Text { default, .. } => vec![default.into()],
|
||||
Kind::Choice { choices, .. } => {
|
||||
choices.iter().map(|(v, _)| (*v).into()).collect()
|
||||
}
|
||||
Kind::Choice { choices, .. } => choices.iter().map(|(v, _)| (*v).into()).collect(),
|
||||
};
|
||||
for value in values {
|
||||
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use crate::{
|
||||
Message,
|
||||
schema::Page,
|
||||
store::{Edit, Snapshot, set},
|
||||
};
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Color, Length},
|
||||
widget::icon as svg_icon,
|
||||
};
|
||||
|
||||
pub use ferese_theme::controls::{
|
||||
button_style, navigation_style, settings_input as input_style, surface,
|
||||
};
|
||||
use cosmic::Element;
|
||||
use cosmic::iced::{Color, Length};
|
||||
use cosmic::widget::icon as svg_icon;
|
||||
pub use ferese_theme::controls::{button_style, navigation_style, settings_input as input_style, surface};
|
||||
pub use ferese_theme::{Palette, mix, surface_shade};
|
||||
|
||||
use crate::Message;
|
||||
use crate::schema::Page;
|
||||
use crate::store::{Edit, Snapshot, set};
|
||||
|
||||
pub fn color(value: &str, fallback: Color) -> Color {
|
||||
ferese_theme::parse_color(value).unwrap_or(fallback)
|
||||
}
|
||||
@@ -32,9 +26,7 @@ pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
||||
}
|
||||
|
||||
pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
||||
ferese_theme::font(Some(
|
||||
&snapshot.string("theme.typography.font_family", "Inter"),
|
||||
))
|
||||
ferese_theme::font(Some(&snapshot.string("theme.typography.font_family", "Inter")))
|
||||
}
|
||||
|
||||
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
||||
@@ -91,20 +83,14 @@ pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
||||
("theme.surface.bar.text_primary", preset.text, "#F0F3FA"),
|
||||
("theme.surface.bar.text_muted", preset.muted, "#AAB4C7"),
|
||||
("theme.focus_ring.gradient.from", preset.accent, "#3D7BE6"),
|
||||
(
|
||||
"theme.focus_ring.gradient.to",
|
||||
preset.gradient_end,
|
||||
"#3D7BE6",
|
||||
),
|
||||
("theme.focus_ring.gradient.to", preset.gradient_end, "#3D7BE6"),
|
||||
]
|
||||
.into_iter()
|
||||
.all(|(path, expected, fallback)| {
|
||||
if index == 0 && snapshot.item(path).is_none() {
|
||||
return true;
|
||||
}
|
||||
snapshot
|
||||
.string(path, fallback)
|
||||
.eq_ignore_ascii_case(expected)
|
||||
snapshot.string(path, fallback).eq_ignore_ascii_case(expected)
|
||||
}) && snapshot.number("theme.focus_ring.gradient.angle", 0.) == 0.
|
||||
}
|
||||
|
||||
@@ -164,19 +150,14 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
||||
let accent = hex(p.accent);
|
||||
let muted = hex(p.muted);
|
||||
let gap = snapshot.number("layout.inner_gap", 8.).clamp(0., 32.);
|
||||
let radius = snapshot
|
||||
.number("theme.geometry.window_radius", 14.)
|
||||
.clamp(0., 28.);
|
||||
let radius = snapshot.number("theme.geometry.window_radius", 14.).clamp(0., 28.);
|
||||
let bar_y = snapshot.number("theme.geometry.top_bar_margin_top", 0.) * 0.5 + 12.;
|
||||
let bar_margin = snapshot.number("theme.geometry.top_bar_margin_horizontal", 0.) * 0.5 + 14.;
|
||||
let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65;
|
||||
let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65;
|
||||
let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" {
|
||||
snapshot
|
||||
.number(
|
||||
"theme.material.opacity",
|
||||
ferese_config::DEFAULT_MATERIAL_OPACITY,
|
||||
)
|
||||
.number("theme.material.opacity", ferese_config::DEFAULT_MATERIAL_OPACITY)
|
||||
.clamp(0., 1.)
|
||||
} else {
|
||||
1.
|
||||
@@ -235,16 +216,8 @@ mod tests {
|
||||
let snapshot = configured_preset(index);
|
||||
let palette = Palette::from_document(Some(&snapshot.doc));
|
||||
for background in [palette.background, palette.sidebar, palette.card] {
|
||||
assert!(
|
||||
contrast(palette.text, background) >= 4.5,
|
||||
"{} text",
|
||||
preset.name
|
||||
);
|
||||
assert!(
|
||||
contrast(palette.muted, background) >= 4.5,
|
||||
"{} muted text",
|
||||
preset.name
|
||||
);
|
||||
assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name);
|
||||
assert!(contrast(palette.muted, background) >= 4.5, "{} muted text", preset.name);
|
||||
}
|
||||
for other in PRESETS.iter().skip(index + 1) {
|
||||
assert_ne!(preset.accent, other.accent);
|
||||
@@ -263,17 +236,12 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!preset_selected(&snapshot, index));
|
||||
let mut snapshot = configured_preset(index);
|
||||
snapshot
|
||||
.edit(&set("theme.focus_ring.gradient.to", "#123456"))
|
||||
.unwrap();
|
||||
snapshot.edit(&set("theme.focus_ring.gradient.to", "#123456")).unwrap();
|
||||
assert!(!preset_selected(&snapshot, index));
|
||||
}
|
||||
assert!(preset_selected(&Snapshot::parse(String::new()).unwrap(), 0));
|
||||
assert!(preset(usize::MAX).is_empty());
|
||||
assert!(!preset_selected(
|
||||
&Snapshot::parse(String::new()).unwrap(),
|
||||
usize::MAX
|
||||
));
|
||||
assert!(!preset_selected(&Snapshot::parse(String::new()).unwrap(), usize::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -283,10 +251,7 @@ mod tests {
|
||||
|
||||
for (index, item) in PRESETS.iter().enumerate() {
|
||||
let snapshot = configured_preset(index);
|
||||
assert_eq!(
|
||||
native_theme(Some(&snapshot)).cosmic().is_dark,
|
||||
item.name != "Ayu Light"
|
||||
);
|
||||
assert_eq!(native_theme(Some(&snapshot)).cosmic().is_dark, item.name != "Ayu Light");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Watch the parent directory so atomic editor saves keep working.
|
||||
use crate::{Message, store::Snapshot};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::iced::futures::{SinkExt, Stream};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use crate::Message;
|
||||
use crate::store::Snapshot;
|
||||
|
||||
pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let path = path.clone();
|
||||
@@ -21,9 +25,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let mut watcher = match watcher {
|
||||
Ok(watcher) => watcher,
|
||||
Err(error) => {
|
||||
let _ = output
|
||||
.send(Message::ExternalConfig(Err(error.to_string())))
|
||||
.await;
|
||||
let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -32,9 +34,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
path.parent().unwrap_or(std::path::Path::new(".")),
|
||||
RecursiveMode::NonRecursive,
|
||||
) {
|
||||
let _ = output
|
||||
.send(Message::ExternalConfig(Err(error.to_string())))
|
||||
.await;
|
||||
let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let snapshot = tokio::task::spawn_blocking(move || Snapshot::read(&file))
|
||||
.await
|
||||
.unwrap_or_else(|e| Err(e.to_string()));
|
||||
if output
|
||||
.send(Message::ExternalConfig(snapshot))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if output.send(Message::ExternalConfig(snapshot)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
if events.recv().await.is_none() {
|
||||
@@ -64,9 +60,10 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::iced::futures::{StreamExt, pin_mut};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn watches_atomic_replacement_and_retains_subscription_after_bad_kdl() {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
@@ -79,10 +76,7 @@ mod tests {
|
||||
std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap();
|
||||
let stream = changes(&path);
|
||||
pin_mut!(stream);
|
||||
assert!(matches!(
|
||||
stream.next().await,
|
||||
Some(Message::ExternalConfig(Ok(_)))
|
||||
));
|
||||
assert!(matches!(stream.next().await, Some(Message::ExternalConfig(Ok(_)))));
|
||||
for source in ["bad [", "animations {\n speed 0.75\n}\n"] {
|
||||
let temporary = directory.path().join("replacement");
|
||||
std::fs::write(&temporary, source).unwrap();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
os::unix::net::UnixStream,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub(super) fn call(command: &str) -> Result<Value, String> {
|
||||
request(command, json!({}))
|
||||
@@ -33,10 +32,7 @@ impl SessionConnection {
|
||||
}
|
||||
|
||||
fn request(&self, command: &str, args: Value) -> Result<Value, String> {
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Compositor connection failed")?;
|
||||
let mut stream = self.stream.lock().map_err(|_| "Compositor connection failed")?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
@@ -45,8 +41,7 @@ impl SessionConnection {
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
@@ -89,24 +84,21 @@ impl Approval {
|
||||
}
|
||||
|
||||
pub fn prepare(&self) -> Result<(), String> {
|
||||
if matches!(self.mode, Mode::Shutdown | Mode::Suspend)
|
||||
&& login_inhibitors(self.mode)? != self.external
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
if matches!(self.mode, Mode::Shutdown | Mode::Suspend) && login_inhibitors(self.mode)? != self.external {
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
if self.mode == Mode::Shutdown {
|
||||
self.connection.as_ref().ok_or("Missing session-ending lease")?.request(
|
||||
"validate-session-end",
|
||||
json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}),
|
||||
)?;
|
||||
self.connection
|
||||
.as_ref()
|
||||
.ok_or("Missing session-ending lease")?
|
||||
.request(
|
||||
"validate-session-end",
|
||||
json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}),
|
||||
)?;
|
||||
} else if self.mode == Mode::Suspend {
|
||||
let state = call("get-session-state")?;
|
||||
if state["inhibitor-revision"].as_u64() != Some(u64::from(self.revision)) {
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -129,19 +121,12 @@ fn login_inhibitors(mode: Mode) -> Result<Vec<String>, String> {
|
||||
"org.freedesktop.login1.Manager",
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let inhibitors: Vec<(String, String, String, String, u32, u32)> = proxy
|
||||
.call("ListInhibitors", &())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let what = if mode == Mode::Suspend {
|
||||
"sleep"
|
||||
} else {
|
||||
"shutdown"
|
||||
};
|
||||
let inhibitors: Vec<(String, String, String, String, u32, u32)> =
|
||||
proxy.call("ListInhibitors", &()).map_err(|error| error.to_string())?;
|
||||
let what = if mode == Mode::Suspend { "sleep" } else { "shutdown" };
|
||||
let mut reasons = inhibitors
|
||||
.into_iter()
|
||||
.filter(|(flags, _, _, inhibition, _, _)| {
|
||||
inhibition == "block" && flags.split(':').any(|flag| flag == what)
|
||||
})
|
||||
.filter(|(flags, _, _, inhibition, _, _)| inhibition == "block" && flags.split(':').any(|flag| flag == what))
|
||||
.map(|(_, app, reason, _, _, _)| format!("{app}: {reason}"))
|
||||
.collect::<Vec<_>>();
|
||||
reasons.sort();
|
||||
@@ -181,9 +166,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result<Approval, String> {
|
||||
}
|
||||
}
|
||||
let flags = if mode == Mode::Suspend { 4 } else { 1 };
|
||||
let entries = state["inhibitors"]
|
||||
.as_array()
|
||||
.ok_or("Invalid inhibitor list")?;
|
||||
let entries = state["inhibitors"].as_array().ok_or("Invalid inhibitor list")?;
|
||||
let mut reasons = entries
|
||||
.iter()
|
||||
.filter(|entry| entry["flags"].as_u64().unwrap_or(0) & flags != 0)
|
||||
@@ -192,9 +175,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result<Approval, String> {
|
||||
.as_str()
|
||||
.filter(|app| !app.is_empty())
|
||||
.unwrap_or("Application");
|
||||
let reason = entry["reason"]
|
||||
.as_str()
|
||||
.unwrap_or("Requested session inhibition");
|
||||
let reason = entry["reason"].as_str().unwrap_or("Requested session inhibition");
|
||||
format!("{app}: {reason}")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -93,8 +93,7 @@ impl Default for ShellTheme {
|
||||
|
||||
impl ShellTheme {
|
||||
pub(crate) fn palette(self) -> ferese_theme::Palette {
|
||||
let color =
|
||||
|[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.);
|
||||
let color = |[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.);
|
||||
let surface = color(self.surface_base);
|
||||
ferese_theme::Palette {
|
||||
background: surface,
|
||||
@@ -330,14 +329,8 @@ pub(crate) fn load() -> ShellConfig {
|
||||
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
|
||||
match ferese_config::from_str::<FereseConfig>(source) {
|
||||
Ok(config) => {
|
||||
config
|
||||
.notifications
|
||||
.validate()
|
||||
.map_err(ferese_config::Error::from)?;
|
||||
config
|
||||
.desktop_widgets
|
||||
.validate()
|
||||
.map_err(ferese_config::Error::from)?;
|
||||
config.notifications.validate().map_err(ferese_config::Error::from)?;
|
||||
config.desktop_widgets.validate().map_err(ferese_config::Error::from)?;
|
||||
let mut theme = shell_theme(&config.theme);
|
||||
theme.material_radius = nonnegative_or(
|
||||
config
|
||||
@@ -387,19 +380,15 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
|
||||
ShellTheme {
|
||||
material_radius: defaults.material_radius,
|
||||
bar_background: {
|
||||
let mut background =
|
||||
parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background);
|
||||
let mut background = parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background);
|
||||
background[3] = alpha;
|
||||
background
|
||||
},
|
||||
bar_text_primary: parse_color(&theme.surface.bar.text_primary)
|
||||
.unwrap_or(defaults.bar_text_primary),
|
||||
bar_text_muted: parse_color(&theme.surface.bar.text_muted)
|
||||
.unwrap_or(defaults.bar_text_muted),
|
||||
bar_text_primary: parse_color(&theme.surface.bar.text_primary).unwrap_or(defaults.bar_text_primary),
|
||||
bar_text_muted: parse_color(&theme.surface.bar.text_muted).unwrap_or(defaults.bar_text_muted),
|
||||
surface_base: parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base),
|
||||
surface_popover: {
|
||||
let mut color =
|
||||
parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base);
|
||||
let mut color = parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base);
|
||||
color[3] = alpha;
|
||||
color
|
||||
},
|
||||
@@ -424,8 +413,7 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
|
||||
control_gap: nonnegative_or(theme.geometry.control_gap, defaults.control_gap),
|
||||
shadow_offset_y: finite_or(theme.shadow.soft.offset_y, defaults.shadow_offset_y),
|
||||
shadow_blur: nonnegative_or(theme.shadow.soft.blur, defaults.shadow_blur),
|
||||
shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity)
|
||||
.clamp(0.0, 1.0),
|
||||
shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,16 +544,8 @@ mod tests {
|
||||
.clock;
|
||||
assert!(clock.enabled);
|
||||
assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight);
|
||||
assert!(
|
||||
super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n")
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
super::parse_source(
|
||||
"desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n").is_err());
|
||||
assert!(super::parse_source("desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n").is_err());
|
||||
}
|
||||
use super::*;
|
||||
|
||||
@@ -603,10 +583,7 @@ theme {
|
||||
config.theme.typography.font_family.as_deref(),
|
||||
Some("JetBrainsMono Nerd Font")
|
||||
);
|
||||
assert_eq!(
|
||||
config.theme.background.path,
|
||||
Some(PathBuf::from("/tmp/wallpaper.png"))
|
||||
);
|
||||
assert_eq!(config.theme.background.path, Some(PathBuf::from("/tmp/wallpaper.png")));
|
||||
assert_eq!(config.theme.background.mode, WallpaperMode::Fit);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
sync::mpsc::{self, Receiver, Sender, TryRecvError},
|
||||
thread,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::os::fd::{FromRawFd, RawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
|
||||
use std::thread;
|
||||
|
||||
use ferese_protocols::shell::v1::client::{
|
||||
ferese_shell_manager_v1::FereseShellManagerV1,
|
||||
ferese_shell_v1::{self, FereseShellV1},
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::wl_registry,
|
||||
};
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_manager_v1::FereseShellManagerV1;
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_v1;
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_v1::FereseShellV1;
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::wl_registry;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ShellSnapshot {
|
||||
@@ -116,11 +110,7 @@ impl ShellControl {
|
||||
match self.updates.try_recv() {
|
||||
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
|
||||
Ok(ControlUpdate::LogoutCancelled(serial)) => {
|
||||
if poll
|
||||
.logout
|
||||
.as_ref()
|
||||
.is_some_and(|(pending, _)| *pending == serial)
|
||||
{
|
||||
if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) {
|
||||
poll.logout = None;
|
||||
}
|
||||
poll.logout_cancelled.push(serial);
|
||||
@@ -174,8 +164,8 @@ impl ShellControl {
|
||||
}
|
||||
|
||||
fn control_connection() -> Result<Connection, Box<dyn Error>> {
|
||||
let raw_fd = std::env::var_os("FERESE_SHELL_CONTROL_SOCKET")
|
||||
.ok_or("Ferese did not provide a shell-control connection")?;
|
||||
let raw_fd =
|
||||
std::env::var_os("FERESE_SHELL_CONTROL_SOCKET").ok_or("Ferese did not provide a shell-control connection")?;
|
||||
let fd = raw_fd
|
||||
.to_str()
|
||||
.ok_or("FERESE_SHELL_CONTROL_SOCKET is not valid UTF-8")?
|
||||
@@ -299,26 +289,14 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
fullscreen,
|
||||
});
|
||||
}
|
||||
ferese_shell_v1::Event::SnapshotEnd { serial }
|
||||
if state.serial.take() == Some(serial) =>
|
||||
{
|
||||
ferese_shell_v1::Event::SnapshotEnd { serial } if state.serial.take() == Some(serial) => {
|
||||
state.pending.workspaces.sort_unstable_by_key(|workspace| {
|
||||
ferese_core::workspace_order_key(
|
||||
&workspace.name,
|
||||
ferese_core::WorkspaceId(workspace.id),
|
||||
)
|
||||
ferese_core::workspace_order_key(&workspace.name, ferese_core::WorkspaceId(workspace.id))
|
||||
});
|
||||
let _ = state
|
||||
.sender
|
||||
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
||||
let _ = state.sender.send(ControlUpdate::Snapshot(state.pending.clone()));
|
||||
}
|
||||
ferese_shell_v1::Event::LogoutRequested {
|
||||
serial,
|
||||
output_name,
|
||||
} => {
|
||||
let _ = state
|
||||
.sender
|
||||
.send(ControlUpdate::Logout(serial, output_name));
|
||||
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
||||
let _ = state.sender.send(ControlUpdate::Logout(serial, output_name));
|
||||
}
|
||||
ferese_shell_v1::Event::LogoutCancelled { serial } => {
|
||||
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
|
||||
|
||||
@@ -6,8 +6,7 @@ pub(super) struct Entry {
|
||||
|
||||
pub(super) fn load() -> Result<Vec<Entry>, String> {
|
||||
let entries: Vec<Entry> =
|
||||
serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?)
|
||||
.map_err(|error| error.to_string())?;
|
||||
serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?).map_err(|error| error.to_string())?;
|
||||
if entries.len() > 256
|
||||
|| entries
|
||||
.iter()
|
||||
|
||||
+179
-518
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,5 @@
|
||||
//! Draw-only popup transform; layout stays stable while its presentation animates.
|
||||
use cosmic::iced::advanced::Renderer as _;
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
|
||||
use cosmic::{Element, Theme};
|
||||
|
||||
@@ -42,8 +41,7 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
static SETTINGS: std::sync::OnceLock<std::sync::RwLock<Settings>> = std::sync::OnceLock::new();
|
||||
static SHELL_RADIUS: std::sync::atomic::AtomicU32 =
|
||||
std::sync::atomic::AtomicU32::new(14.0_f32.to_bits());
|
||||
static SHELL_RADIUS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(14.0_f32.to_bits());
|
||||
|
||||
/// All shell shapes share one radius, capped to fit smaller controls.
|
||||
pub(crate) fn radius(limit: f32) -> f32 {
|
||||
@@ -97,17 +95,14 @@ impl PopupMotion {
|
||||
}
|
||||
|
||||
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
|
||||
let duration = self
|
||||
.settings
|
||||
.duration(if self.target == 1.0 { 200.0 } else { 140.0 });
|
||||
let duration = self.settings.duration(if self.target == 1.0 { 200.0 } else { 140.0 });
|
||||
if duration.is_zero() {
|
||||
return self.target;
|
||||
}
|
||||
let Some(started) = self.started else {
|
||||
return self.start;
|
||||
};
|
||||
let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32())
|
||||
.min(1.0);
|
||||
let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32()).min(1.0);
|
||||
if t == 1.0 {
|
||||
return self.target;
|
||||
}
|
||||
@@ -145,14 +140,7 @@ pub(crate) fn button<'a, M: Clone + 'a>(
|
||||
let active = progress.clone();
|
||||
let hovered = progress.clone();
|
||||
let paint = move |progress: f32, pressed: bool| {
|
||||
ferese_theme::controls::shell_button(
|
||||
foreground,
|
||||
selected,
|
||||
opacity,
|
||||
radius(14.),
|
||||
progress,
|
||||
pressed,
|
||||
)
|
||||
ferese_theme::controls::shell_button(foreground, selected, opacity, radius(14.), progress, pressed)
|
||||
};
|
||||
let content = button
|
||||
.class(cosmic::theme::Button::Custom {
|
||||
@@ -181,12 +169,7 @@ struct HoverState {
|
||||
}
|
||||
|
||||
impl HoverState {
|
||||
fn advance(
|
||||
&mut self,
|
||||
target: f32,
|
||||
now: std::time::Instant,
|
||||
duration: std::time::Duration,
|
||||
) -> bool {
|
||||
fn advance(&mut self, target: f32, now: std::time::Instant, duration: std::time::Duration) -> bool {
|
||||
if self.target != target {
|
||||
self.start = self.current;
|
||||
self.target = target;
|
||||
@@ -196,8 +179,7 @@ impl HoverState {
|
||||
1.0
|
||||
} else {
|
||||
self.started.map_or(1.0, |start| {
|
||||
(now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32())
|
||||
.min(1.0)
|
||||
(now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32()).min(1.0)
|
||||
})
|
||||
};
|
||||
self.current = self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t);
|
||||
@@ -222,9 +204,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
tree: &widget::Tree,
|
||||
cursor: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
self.content
|
||||
.as_widget()
|
||||
.a11y_nodes(layout, &tree.children[0], cursor)
|
||||
self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor)
|
||||
}
|
||||
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
@@ -298,17 +278,10 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.progress
|
||||
.set(tree.state.downcast_ref::<HoverState>().current);
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
self.progress.set(tree.state.downcast_ref::<HoverState>().current);
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
@@ -319,13 +292,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
@@ -348,22 +317,13 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<cosmic::iced::advanced::overlay::Element<'a, M, Theme, cosmic::Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(&mut tree.children[0], layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn animated<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
progress: f32,
|
||||
regions: Regions,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn animated<'a, M: 'a>(content: Element<'a, M>, progress: f32, regions: Regions, radius: f32) -> Element<'a, M> {
|
||||
Element::new(Motion {
|
||||
content,
|
||||
progress,
|
||||
@@ -445,9 +405,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
}
|
||||
*self.regions.lock().unwrap() = collector.into_regions(self.translation(), self.radius);
|
||||
let cursor = self.cursor(cursor);
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.update(tree, event, layout, cursor, renderer, clipboard, shell, viewport);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
@@ -463,15 +423,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
let v = self.translation();
|
||||
let transform = Transformation::translate(v.x, v.y);
|
||||
renderer.with_transformation(transform, |renderer| {
|
||||
self.content.as_widget().draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
self.cursor(cursor),
|
||||
viewport,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, self.cursor(cursor), viewport)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,13 +437,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
self.cursor(cursor),
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, self.cursor(cursor), viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
@@ -499,9 +449,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
renderer: &cosmic::Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
self.content.as_widget_mut().operate(tree, layout, renderer, operation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,11 +505,7 @@ mod tests {
|
||||
fn centered_modal_material_tracks_surface_position_and_animation() {
|
||||
let mut collector = CollectRegions(Vec::new());
|
||||
let card = Rectangle::new((380.0, 420.0).into(), (440.0, 220.0).into());
|
||||
widget::Operation::container(
|
||||
&mut collector,
|
||||
Some(&widget::Id::new("ferese-blur-card")),
|
||||
card,
|
||||
);
|
||||
widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card);
|
||||
assert_eq!(
|
||||
collector.into_regions(Vector::new(0.0, -2.0), 14.0),
|
||||
vec![[380, 418, 440, 220, 14]]
|
||||
@@ -572,29 +516,16 @@ mod tests {
|
||||
fn popup_waits_for_configuration_then_opens_and_closes_faster() {
|
||||
let now = std::time::Instant::now();
|
||||
let mut motion = PopupMotion::new(Settings::default());
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_secs(30)),
|
||||
0.0
|
||||
);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_secs(30)), 0.0);
|
||||
motion.begin(now);
|
||||
// Later configure/focus notifications must not restart the clock.
|
||||
motion.begin(now + std::time::Duration::from_millis(90));
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(200)),
|
||||
1.0
|
||||
);
|
||||
assert!((motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(200)), 1.0);
|
||||
motion.retarget(0.0, now + std::time::Duration::from_millis(200));
|
||||
assert!(motion.closing());
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(340)),
|
||||
0.0
|
||||
);
|
||||
assert!((motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(340)), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -613,10 +544,7 @@ mod tests {
|
||||
let before = motion.progress_at(reopen);
|
||||
motion.retarget(1.0, reopen);
|
||||
assert_eq!(motion.progress_at(reopen), before);
|
||||
assert_eq!(
|
||||
motion.progress_at(reopen + std::time::Duration::from_secs(1)),
|
||||
1.0
|
||||
);
|
||||
assert_eq!(motion.progress_at(reopen + std::time::Duration::from_secs(1)), 1.0);
|
||||
for settings in [
|
||||
Settings {
|
||||
reduced_motion: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Atomic, conflict-checked note edits; always called from a worker thread.
|
||||
use std::{fs, io::Write, path::Path};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Edit {
|
||||
@@ -63,13 +65,10 @@ pub fn save(path: &Path, edits: &[Edit]) -> Result<(), String> {
|
||||
file.as_file()
|
||||
.set_permissions(fs::metadata(path).map_err(|e| e.to_string())?.permissions())
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.write_all(updated.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.write_all(updated.as_bytes()).map_err(|e| e.to_string())?;
|
||||
file.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
if fs::read_to_string(path).map_err(|e| e.to_string())? != source {
|
||||
return Err(
|
||||
"Config changed while saving; your note remains open. Edit again to retry.".into(),
|
||||
);
|
||||
return Err("Config changed while saving; your note remains open. Edit again to retry.".into());
|
||||
}
|
||||
file.persist(path).map_err(|e| e.to_string())?;
|
||||
fs::File::open(parent)
|
||||
@@ -131,10 +130,7 @@ desktop-widgets {
|
||||
let source = fs::read_to_string(&path).unwrap();
|
||||
assert!(source.contains("// keep this"));
|
||||
assert!(source.contains("#123456"));
|
||||
let notes = crate::config::parse_source(&source)
|
||||
.unwrap()
|
||||
.desktop_widgets
|
||||
.notes;
|
||||
let notes = crate::config::parse_source(&source).unwrap().desktop_widgets.notes;
|
||||
assert_eq!(notes[0].text, "one\ntwo");
|
||||
assert_eq!((notes[0].margin_x, notes[0].margin_y), (50, 60));
|
||||
assert_eq!(notes[0].anchor, ferese_core::desktop::Anchor::TopLeft);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use super::*;
|
||||
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
||||
use cosmic::widget::{column, scrollable};
|
||||
|
||||
use super::*;
|
||||
|
||||
const POPUP_WIDTH: u32 = 368;
|
||||
|
||||
fn popup_height(notice: ¬ifications::Notice, count: usize) -> u32 {
|
||||
let body = if notice.body.is_empty() && count == 1 {
|
||||
0
|
||||
} else {
|
||||
22
|
||||
};
|
||||
let body = if notice.body.is_empty() && count == 1 { 0 } else { 22 };
|
||||
let actions = if notice.live && notice.actions.iter().any(|(key, _)| key != "default") {
|
||||
39
|
||||
} else {
|
||||
@@ -33,18 +30,13 @@ fn history_height(center: ¬ifications::Center) -> u32 {
|
||||
} else {
|
||||
1
|
||||
};
|
||||
popup_height(notice, count)
|
||||
+ wrapped_lines.saturating_sub(1) as u32 * 17
|
||||
+ if count > 1 { 12 } else { 0 }
|
||||
popup_height(notice, count) + wrapped_lines.saturating_sub(1) as u32 * 17 + if count > 1 { 12 } else { 0 }
|
||||
};
|
||||
let cards: u32 = groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
if group.len() > 1 && center.expanded_apps.contains(&group[0].app) {
|
||||
32 + group
|
||||
.iter()
|
||||
.map(|notice| card_height(notice, 1))
|
||||
.sum::<u32>()
|
||||
32 + group.iter().map(|notice| card_height(notice, 1)).sum::<u32>()
|
||||
+ group.len().saturating_sub(1) as u32 * 8
|
||||
} else {
|
||||
card_height(group[0], group.len())
|
||||
@@ -188,9 +180,10 @@ impl FereseShell {
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
self.snapshot.outputs.iter().any(|output| {
|
||||
output.focused && Some(output.name.as_str()) == entry.name.as_deref()
|
||||
})
|
||||
self.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.any(|output| output.focused && Some(output.name.as_str()) == entry.name.as_deref())
|
||||
})
|
||||
.or_else(|| self.outputs.first())
|
||||
.map(|entry| entry.output.clone());
|
||||
@@ -213,9 +206,7 @@ impl FereseShell {
|
||||
.notifications
|
||||
.popup_groups()
|
||||
.iter()
|
||||
.map(|(notice, _, count)| {
|
||||
popup_height(notice, *count) + if *count > 1 { 12 } else { 0 }
|
||||
})
|
||||
.map(|(notice, _, count)| popup_height(notice, *count) + if *count > 1 { 12 } else { 0 })
|
||||
.sum::<u32>()
|
||||
+ count.saturating_sub(1) as u32 * 8
|
||||
+ 8;
|
||||
@@ -299,8 +290,7 @@ impl FereseShell {
|
||||
let id = notice.id;
|
||||
let mut bold = *SHELL_FONT.get().unwrap().read().unwrap();
|
||||
bold.weight = cosmic::iced::font::Weight::Bold;
|
||||
let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty()
|
||||
{
|
||||
let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty() {
|
||||
accented_icon(ferese_theme::icons::FERESE, 18, accent, accent)
|
||||
} else if notice.icon.starts_with('/') {
|
||||
icon::icon(icon::from_path(notice.icon.clone().into())).size(18)
|
||||
@@ -308,18 +298,17 @@ impl FereseShell {
|
||||
icon::from_name(notice.icon.as_str()).size(18).icon()
|
||||
}
|
||||
.opacity(opacity);
|
||||
let icon_well =
|
||||
container(app_icon)
|
||||
.center_x(30)
|
||||
.center_y(30)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(15.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
let icon_well = container(app_icon)
|
||||
.center_x(30)
|
||||
.center_y(30)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(15.0).into(),
|
||||
..Default::default()
|
||||
}));
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let icon: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
||||
let badge_fill = ferese_theme::composite(
|
||||
color(palette.accent),
|
||||
@@ -329,8 +318,7 @@ impl FereseShell {
|
||||
palette.surface_popover
|
||||
}),
|
||||
);
|
||||
let (badge_fill, badge_text) =
|
||||
ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
||||
let (badge_fill, badge_text) = ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
||||
let badge = container(
|
||||
text(count.to_string())
|
||||
.size(10)
|
||||
@@ -375,15 +363,10 @@ impl FereseShell {
|
||||
.align_y(alignment::Vertical::Center)
|
||||
.push(icon)
|
||||
.push(
|
||||
container(
|
||||
text(app)
|
||||
.size(11)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
.clip(true),
|
||||
container(text(app).size(11).font(bold).class(theme::Text::Color(muted)))
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
.clip(true),
|
||||
)
|
||||
.push(
|
||||
text(age_label(notice.received_at.elapsed()))
|
||||
@@ -446,13 +429,13 @@ impl FereseShell {
|
||||
let mut actions = row([]);
|
||||
for (index, (key, label)) in available.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
actions = actions.push(
|
||||
container(cosmic::iced::widget::Space::new().width(1).height(38))
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
actions =
|
||||
actions.push(container(cosmic::iced::widget::Space::new().width(1).height(38)).class(
|
||||
theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(divider)),
|
||||
..Default::default()
|
||||
})),
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
actions = actions.push(card_button(
|
||||
container(
|
||||
@@ -472,17 +455,12 @@ impl FereseShell {
|
||||
}
|
||||
face = face
|
||||
.push(
|
||||
container(
|
||||
cosmic::iced::widget::Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(1),
|
||||
)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
container::Style {
|
||||
container(cosmic::iced::widget::Space::new().width(Length::Fill).height(1)).class(
|
||||
theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(divider)),
|
||||
..Default::default()
|
||||
}
|
||||
})),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.push(actions);
|
||||
}
|
||||
@@ -518,23 +496,19 @@ impl FereseShell {
|
||||
// bottom edges, after the card's measured height.
|
||||
let back = move |inset: f32| {
|
||||
container(
|
||||
container(
|
||||
cosmic::iced::widget::Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(6),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.045 * opacity,
|
||||
..foreground
|
||||
})),
|
||||
border: Border {
|
||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||
container(cosmic::iced::widget::Space::new().width(Length::Fill).height(6))
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.045 * opacity,
|
||||
..foreground
|
||||
})),
|
||||
border: Border {
|
||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
})),
|
||||
)
|
||||
.padding([0.0, inset])
|
||||
};
|
||||
@@ -552,10 +526,7 @@ impl FereseShell {
|
||||
Message::ToggleNotificationHistory
|
||||
}));
|
||||
} else if notice.live && notice.actions.iter().any(|(key, _)| key == "default") {
|
||||
area = area.on_press(cosmic::Action::App(Message::InvokeNotification(
|
||||
id,
|
||||
"default".into(),
|
||||
)));
|
||||
area = area.on_press(cosmic::Action::App(Message::InvokeNotification(id, "default".into())));
|
||||
}
|
||||
area.into()
|
||||
}
|
||||
@@ -589,11 +560,7 @@ impl FereseShell {
|
||||
} else {
|
||||
format!(
|
||||
"{count} {} · {} {}",
|
||||
if count == 1 {
|
||||
"notification"
|
||||
} else {
|
||||
"notifications"
|
||||
},
|
||||
if count == 1 { "notification" } else { "notifications" },
|
||||
groups.len(),
|
||||
if groups.len() == 1 { "app" } else { "apps" }
|
||||
)
|
||||
@@ -619,11 +586,7 @@ impl FereseShell {
|
||||
well,
|
||||
28,
|
||||
));
|
||||
let dnd_color = if self.notifications.dnd {
|
||||
accent
|
||||
} else {
|
||||
muted
|
||||
};
|
||||
let dnd_color = if self.notifications.dnd { accent } else { muted };
|
||||
let dnd_icon = if self.notifications.dnd {
|
||||
ferese_theme::icons::NOTIFICATIONS_OFF
|
||||
} else {
|
||||
@@ -674,40 +637,34 @@ impl FereseShell {
|
||||
self.notifications.dnd,
|
||||
1.0,
|
||||
));
|
||||
let controls =
|
||||
container(dnd)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(base)),
|
||||
border: Border {
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
let controls = container(dnd)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(base)),
|
||||
border: Border {
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
}));
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let mut content = column([]).spacing(10).push(header).push(controls);
|
||||
if count == 0 {
|
||||
let empty = column([])
|
||||
.spacing(10)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.push(
|
||||
container(accented_icon(
|
||||
ferese_theme::icons::NOTIFICATIONS,
|
||||
26,
|
||||
muted,
|
||||
accent,
|
||||
))
|
||||
.center_x(56)
|
||||
.center_y(56)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(28.0).into(),
|
||||
container(accented_icon(ferese_theme::icons::NOTIFICATIONS, 26, muted, accent))
|
||||
.center_x(56)
|
||||
.center_y(56)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(28.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
})),
|
||||
)
|
||||
.push(
|
||||
text("You're all caught up")
|
||||
@@ -720,11 +677,7 @@ impl FereseShell {
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
content = content.push(
|
||||
container(empty)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill),
|
||||
);
|
||||
content = content.push(container(empty).center_x(Length::Fill).center_y(Length::Fill));
|
||||
} else {
|
||||
content = content.push(
|
||||
row([])
|
||||
@@ -786,12 +739,7 @@ impl FereseShell {
|
||||
}
|
||||
entries = entries.push(cards);
|
||||
} else {
|
||||
entries = entries.push(self.notification_card(
|
||||
latest,
|
||||
opacity,
|
||||
true,
|
||||
group.len(),
|
||||
));
|
||||
entries = entries.push(self.notification_card(latest, opacity, true, group.len()));
|
||||
}
|
||||
}
|
||||
content = content.push(
|
||||
@@ -806,8 +754,7 @@ impl FereseShell {
|
||||
.height(history_height(&self.notifications) as f32)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: (!compositor_material)
|
||||
.then_some(Background::Color(color(palette.surface_popover))),
|
||||
background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))),
|
||||
border: Border {
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: 1.0,
|
||||
@@ -819,9 +766,7 @@ impl FereseShell {
|
||||
motion::animated(
|
||||
panel,
|
||||
progress,
|
||||
surface
|
||||
.map(|surface| surface.regions.clone())
|
||||
.unwrap_or_default(),
|
||||
surface.map(|surface| surface.regions.clone()).unwrap_or_default(),
|
||||
palette.material_radius,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
mpsc::{self, SyncSender},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const PATH: &str = "/org/freedesktop/Notifications";
|
||||
const INTERFACE: &str = "org.freedesktop.Notifications";
|
||||
@@ -97,9 +93,7 @@ impl Server {
|
||||
let mut registry = self.registry.lock().unwrap();
|
||||
let replacing = registry.live.contains_key(&replaces_id);
|
||||
if !replacing && registry.live.len() >= HISTORY_LIMIT {
|
||||
return Err(zbus::fdo::Error::LimitsExceeded(
|
||||
"Too many active notifications".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::LimitsExceeded("Too many active notifications".into()));
|
||||
}
|
||||
let id = if replacing {
|
||||
replaces_id
|
||||
@@ -201,10 +195,7 @@ impl Service {
|
||||
Self::connect(config, None)
|
||||
}
|
||||
|
||||
fn connect(
|
||||
config: &ferese_core::notifications::NotificationConfig,
|
||||
address: Option<String>,
|
||||
) -> Self {
|
||||
fn connect(config: &ferese_core::notifications::NotificationConfig, address: Option<String>) -> Self {
|
||||
let timeout = Arc::new(std::sync::atomic::AtomicU32::new(config.timeout_ms));
|
||||
let worker_timeout = timeout.clone();
|
||||
let (sender, events) = tokio::sync::mpsc::channel(256);
|
||||
@@ -237,25 +228,13 @@ impl Service {
|
||||
if !registry.lock().unwrap().close(id, revision, reason) {
|
||||
continue;
|
||||
}
|
||||
connection.emit_signal(
|
||||
None::<&str>,
|
||||
PATH,
|
||||
INTERFACE,
|
||||
"NotificationClosed",
|
||||
&(id, reason),
|
||||
)
|
||||
connection.emit_signal(None::<&str>, PATH, INTERFACE, "NotificationClosed", &(id, reason))
|
||||
}
|
||||
Command::Action(id, revision, action) => {
|
||||
if registry.lock().unwrap().live.get(&id) != Some(&revision) {
|
||||
continue;
|
||||
}
|
||||
connection.emit_signal(
|
||||
None::<&str>,
|
||||
PATH,
|
||||
INTERFACE,
|
||||
"ActionInvoked",
|
||||
&(id, action),
|
||||
)
|
||||
connection.emit_signal(None::<&str>, PATH, INTERFACE, "ActionInvoked", &(id, action))
|
||||
}
|
||||
};
|
||||
if let Err(error) = result {
|
||||
@@ -337,9 +316,7 @@ impl Center {
|
||||
} else {
|
||||
toast.closing.map_or_else(
|
||||
|| (toast.born.elapsed().as_secs_f32() / duration.as_secs_f32()).min(1.0),
|
||||
|closing| {
|
||||
(1.0 - closing.elapsed().as_secs_f32() / duration.as_secs_f32()).max(0.0)
|
||||
},
|
||||
|closing| (1.0 - closing.elapsed().as_secs_f32() / duration.as_secs_f32()).max(0.0),
|
||||
)
|
||||
};
|
||||
Some((notice, p * p * (3.0 - 2.0 * p)))
|
||||
@@ -347,18 +324,15 @@ impl Center {
|
||||
}
|
||||
|
||||
pub fn animating(&self) -> bool {
|
||||
self.toasts.iter().any(|toast| {
|
||||
toast.closing.is_some() || toast.born.elapsed() < super::motion::notification_duration()
|
||||
})
|
||||
self.toasts
|
||||
.iter()
|
||||
.any(|toast| toast.closing.is_some() || toast.born.elapsed() < super::motion::notification_duration())
|
||||
}
|
||||
|
||||
pub fn popup_groups(&self) -> Vec<(&Notice, f32, usize)> {
|
||||
let mut groups: Vec<(&Notice, f32, usize)> = Vec::new();
|
||||
for (notice, opacity) in self.visible() {
|
||||
if let Some(group) = groups
|
||||
.iter_mut()
|
||||
.find(|(head, _, _)| head.app == notice.app)
|
||||
{
|
||||
if let Some(group) = groups.iter_mut().find(|(head, _, _)| head.app == notice.app) {
|
||||
group.2 += 1;
|
||||
} else {
|
||||
groups.push((notice, opacity, 1));
|
||||
@@ -451,10 +425,7 @@ impl Center {
|
||||
}
|
||||
|
||||
pub fn invoke(&mut self, id: u32, action: String) {
|
||||
if let Some(notice) = self
|
||||
.entries
|
||||
.iter()
|
||||
.find(|notice| notice.id == id && notice.live)
|
||||
if let Some(notice) = self.entries.iter().find(|notice| notice.id == id && notice.live)
|
||||
&& notice.actions.iter().any(|(key, _)| key == &action)
|
||||
{
|
||||
let resident = notice.resident;
|
||||
@@ -593,9 +564,9 @@ impl Center {
|
||||
self.close(id, 1);
|
||||
}
|
||||
self.toasts.retain(|toast| {
|
||||
toast.closing.is_none_or(|closing| {
|
||||
now.duration_since(closing) < super::motion::notification_duration()
|
||||
})
|
||||
toast
|
||||
.closing
|
||||
.is_none_or(|closing| now.duration_since(closing) < super::motion::notification_duration())
|
||||
});
|
||||
let visible: HashSet<_> = self.toasts.iter().map(|toast| toast.id).collect();
|
||||
self.entries
|
||||
@@ -605,9 +576,10 @@ impl Center {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ferese_core::notifications::NotificationConfig;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (Center, std::sync::mpsc::Receiver<Command>) {
|
||||
let (_, events) = tokio::sync::mpsc::channel(256);
|
||||
let (commands, receiver) = mpsc::sync_channel(256);
|
||||
@@ -736,10 +708,7 @@ mod tests {
|
||||
center.last_tick = Instant::now() - Duration::from_secs(10);
|
||||
center.tick();
|
||||
assert!(!center.entries[0].live);
|
||||
assert!(matches!(
|
||||
commands.try_recv().unwrap(),
|
||||
Command::Close(1, 1, 1)
|
||||
));
|
||||
assert!(matches!(commands.try_recv().unwrap(), Command::Close(1, 1, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -786,10 +755,7 @@ mod tests {
|
||||
center.invoke(1, "invalid".into());
|
||||
assert!(commands.try_recv().is_err());
|
||||
center.invoke(1, "open".into());
|
||||
assert!(matches!(
|
||||
commands.try_recv().unwrap(),
|
||||
Command::Action(1, 1, _)
|
||||
));
|
||||
assert!(matches!(commands.try_recv().unwrap(), Command::Action(1, 1, _)));
|
||||
assert!(center.entries[0].live);
|
||||
}
|
||||
|
||||
@@ -811,13 +777,7 @@ mod tests {
|
||||
..Default::default()
|
||||
});
|
||||
assert!(center.dnd);
|
||||
assert_eq!(
|
||||
center
|
||||
.service
|
||||
.timeout
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
10000
|
||||
);
|
||||
assert_eq!(center.service.timeout.load(std::sync::atomic::Ordering::Relaxed), 10000);
|
||||
center.receive(notice(1));
|
||||
center.configure(NotificationConfig {
|
||||
show_popups: false,
|
||||
@@ -870,8 +830,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
let proxy = zbus::blocking::Proxy::new(&connection, INTERFACE, PATH, INTERFACE).unwrap();
|
||||
let info: (String, String, String, String) =
|
||||
proxy.call("GetServerInformation", &()).unwrap();
|
||||
let info: (String, String, String, String) = proxy.call("GetServerInformation", &()).unwrap();
|
||||
assert_eq!(info.0, "Ferese");
|
||||
let notify = |replace| -> u32 {
|
||||
proxy
|
||||
@@ -905,22 +864,14 @@ mod tests {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let signal = signals.next().unwrap();
|
||||
sender
|
||||
.send(signal.body().deserialize::<(u32, u32)>().unwrap())
|
||||
.unwrap();
|
||||
sender.send(signal.body().deserialize::<(u32, u32)>().unwrap()).unwrap();
|
||||
});
|
||||
proxy.call::<_, _, ()>("CloseNotification", &(id,)).unwrap();
|
||||
let Event::Close(closed, revision) = received(&service) else {
|
||||
panic!("Expected close");
|
||||
};
|
||||
service
|
||||
.commands
|
||||
.send(Command::Close(closed, revision, 3))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
receiver.recv_timeout(Duration::from_secs(3)).unwrap(),
|
||||
(id, 3)
|
||||
);
|
||||
service.commands.send(Command::Close(closed, revision, 3)).unwrap();
|
||||
assert_eq!(receiver.recv_timeout(Duration::from_secs(3)).unwrap(), (id, 3));
|
||||
worker.join().unwrap();
|
||||
assert!(proxy.call::<_, _, ()>("CloseNotification", &(id,)).is_err());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! The bar only controls the recorder and consumes bounded status messages.
|
||||
//! Encoding and portal negotiation run in the separate ferese-record process.
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{ChildStdin, Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
path::PathBuf,
|
||||
process::{ChildStdin, Command, Stdio},
|
||||
sync::mpsc::{self, Receiver},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub enum State {
|
||||
@@ -126,28 +126,22 @@ impl Recorder {
|
||||
match event {
|
||||
Event::Update(update) => match update.state.as_str() {
|
||||
"selecting" if self.control.is_some() => self.state = State::Selecting,
|
||||
"recording" if self.control.is_some() => {
|
||||
self.state = State::Recording(Instant::now())
|
||||
}
|
||||
"recording" if self.control.is_some() => self.state = State::Recording(Instant::now()),
|
||||
"saving" => self.state = State::Saving,
|
||||
"saved" => {
|
||||
self.state = update.path.map(State::Saved).unwrap_or_else(|| {
|
||||
State::Error("Recorder returned no saved file".into())
|
||||
})
|
||||
self.state = update
|
||||
.path
|
||||
.map(State::Saved)
|
||||
.unwrap_or_else(|| State::Error("Recorder returned no saved file".into()))
|
||||
}
|
||||
"cancelled" => self.state = State::Idle,
|
||||
"error" => {
|
||||
self.state = State::Error(
|
||||
update.message.unwrap_or_else(|| "Recording failed".into()),
|
||||
)
|
||||
self.state = State::Error(update.message.unwrap_or_else(|| "Recording failed".into()))
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::Exited => {
|
||||
if matches!(
|
||||
self.state,
|
||||
State::Selecting | State::Recording(_) | State::Saving
|
||||
) {
|
||||
if matches!(self.state, State::Selecting | State::Recording(_) | State::Saving) {
|
||||
self.state = State::Error("The recorder stopped unexpectedly".into());
|
||||
}
|
||||
done = true;
|
||||
|
||||
@@ -3,17 +3,12 @@ mod bluetooth;
|
||||
mod dbus_cache;
|
||||
mod network;
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
Arc, Condvar, Mutex,
|
||||
mpsc::{self, SyncSender},
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::{Arc, Condvar, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, thread};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Snapshot {
|
||||
@@ -146,10 +141,7 @@ impl Service {
|
||||
let (updates, rx) = tokio::sync::watch::channel(None);
|
||||
// Polling and writes have separate workers: a missing D-Bus service must
|
||||
// never hold up volume/brightness changes. Publish only coherent polls.
|
||||
let shared = Arc::new((
|
||||
Mutex::new((0u64, false, None::<String>, false)),
|
||||
Condvar::new(),
|
||||
));
|
||||
let shared = Arc::new((Mutex::new((0u64, false, None::<String>, false)), Condvar::new()));
|
||||
let polling = shared.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
@@ -249,8 +241,7 @@ pub fn available(program: &str) -> bool {
|
||||
return executable(Path::new(program));
|
||||
}
|
||||
|
||||
env::var_os("PATH")
|
||||
.is_some_and(|paths| env::split_paths(&paths).any(|path| executable(&path.join(program))))
|
||||
env::var_os("PATH").is_some_and(|paths| env::split_paths(&paths).any(|path| executable(&path.join(program))))
|
||||
}
|
||||
|
||||
fn run(program: &str, args: &[&str]) -> Result<String, String> {
|
||||
@@ -302,10 +293,7 @@ impl StatusBus {
|
||||
}
|
||||
}
|
||||
|
||||
fn query<T>(
|
||||
&mut self,
|
||||
query: impl FnOnce(&zbus::blocking::Connection) -> zbus::Result<T>,
|
||||
) -> Option<T> {
|
||||
fn query<T>(&mut self, query: impl FnOnce(&zbus::blocking::Connection) -> zbus::Result<T>) -> Option<T> {
|
||||
if self.connection.is_none() {
|
||||
let builder = if self.system {
|
||||
zbus::blocking::connection::Builder::system()
|
||||
@@ -313,11 +301,7 @@ impl StatusBus {
|
||||
zbus::blocking::connection::Builder::session()
|
||||
};
|
||||
|
||||
self.connection = builder
|
||||
.ok()?
|
||||
.method_timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.ok();
|
||||
self.connection = builder.ok()?.method_timeout(Duration::from_secs(2)).build().ok();
|
||||
}
|
||||
|
||||
match query(self.connection.as_ref()?) {
|
||||
@@ -397,8 +381,7 @@ pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
||||
}
|
||||
|
||||
fn audio() -> Option<Audio> {
|
||||
let (volume, muted) =
|
||||
parse_audio(&run("wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"]).ok()?)?;
|
||||
let (volume, muted) = parse_audio(&run("wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"]).ok()?)?;
|
||||
let info = run("wpctl", &["inspect", "@DEFAULT_AUDIO_SINK@"]).ok()?;
|
||||
let output = info
|
||||
.lines()
|
||||
@@ -406,17 +389,11 @@ fn audio() -> Option<Audio> {
|
||||
.map(|s| s.trim_matches('"').to_owned())
|
||||
.unwrap_or_else(|| "Default output".into());
|
||||
|
||||
Some(Audio {
|
||||
volume,
|
||||
muted,
|
||||
output,
|
||||
})
|
||||
Some(Audio { volume, muted, output })
|
||||
}
|
||||
|
||||
fn read(path: &Path, name: &str) -> Option<String> {
|
||||
fs::read_to_string(path.join(name))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_owned())
|
||||
fs::read_to_string(path.join(name)).ok().map(|s| s.trim().to_owned())
|
||||
}
|
||||
|
||||
fn battery() -> Option<Battery> {
|
||||
@@ -425,10 +402,7 @@ fn battery() -> Option<Battery> {
|
||||
let mut batteries = entries
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
read(p, "type").as_deref() == Some("Battery")
|
||||
&& read(p, "scope").as_deref() != Some("Device")
|
||||
})
|
||||
.filter(|p| read(p, "type").as_deref() == Some("Battery") && read(p, "scope").as_deref() != Some("Device"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
batteries.sort();
|
||||
@@ -447,13 +421,7 @@ fn power_profiles(bus: &mut StatusBus) -> Option<PowerProfiles> {
|
||||
"org.freedesktop.UPower.PowerProfiles",
|
||||
"/org/freedesktop/UPower/PowerProfiles",
|
||||
)
|
||||
.or_else(|_| {
|
||||
read_power_profiles(
|
||||
connection,
|
||||
"net.hadess.PowerProfiles",
|
||||
"/net/hadess/PowerProfiles",
|
||||
)
|
||||
})
|
||||
.or_else(|_| read_power_profiles(connection, "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -464,10 +432,8 @@ fn read_power_profiles(
|
||||
) -> zbus::Result<PowerProfiles> {
|
||||
let proxy = zbus::blocking::Proxy::new(connection, destination, path, destination)?;
|
||||
let active = proxy.get_property::<String>("ActiveProfile")?;
|
||||
let profiles = proxy
|
||||
.get_property::<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>(
|
||||
"Profiles",
|
||||
)?;
|
||||
let profiles =
|
||||
proxy.get_property::<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>("Profiles")?;
|
||||
let names = ["power-saver", "balanced", "performance"];
|
||||
let available = names.map(|name| {
|
||||
profiles.iter().any(|profile| {
|
||||
@@ -481,13 +447,7 @@ fn read_power_profiles(
|
||||
}
|
||||
|
||||
fn brightness() -> Option<u8> {
|
||||
parse_brightness(
|
||||
&run(
|
||||
"brightnessctl",
|
||||
&["--class=backlight", "--machine-readable", "info"],
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
parse_brightness(&run("brightnessctl", &["--class=backlight", "--machine-readable", "info"]).ok()?)
|
||||
}
|
||||
|
||||
fn parse_brightness(value: &str) -> Option<u8> {
|
||||
@@ -501,11 +461,7 @@ fn parse_brightness(value: &str) -> Option<u8> {
|
||||
let current = fields.next()?.parse::<f64>().ok()?;
|
||||
let _percentage = fields.next()?;
|
||||
let max = fields.next()?.parse::<f64>().ok()?;
|
||||
(fields.next().is_none()
|
||||
&& max.is_finite()
|
||||
&& max > 0.0
|
||||
&& current.is_finite()
|
||||
&& current >= 0.0)
|
||||
(fields.next().is_none() && max.is_finite() && max > 0.0 && current.is_finite() && current >= 0.0)
|
||||
.then(|| (100.0 * current / max).round().clamp(0.0, 100.0) as u8)
|
||||
}
|
||||
|
||||
@@ -554,28 +510,15 @@ fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
||||
),
|
||||
Action::Mute(on) => run(
|
||||
"wpctl",
|
||||
&[
|
||||
"set-mute",
|
||||
"@DEFAULT_AUDIO_SINK@",
|
||||
if *on { "1" } else { "0" },
|
||||
],
|
||||
&["set-mute", "@DEFAULT_AUDIO_SINK@", if *on { "1" } else { "0" }],
|
||||
),
|
||||
Action::Brightness(value) => run(
|
||||
"brightnessctl",
|
||||
&[
|
||||
"--class=backlight",
|
||||
"set",
|
||||
&format!("{}%", value.clamp(&1, &100)),
|
||||
],
|
||||
),
|
||||
Action::Dnd(on) => run(
|
||||
"swaync-client",
|
||||
&[if *on { "--dnd-on" } else { "--dnd-off" }],
|
||||
&["--class=backlight", "set", &format!("{}%", value.clamp(&1, &100))],
|
||||
),
|
||||
Action::Dnd(on) => run("swaync-client", &[if *on { "--dnd-on" } else { "--dnd-off" }]),
|
||||
Action::Notifications => run("swaync-client", &["--open-panel"]),
|
||||
Action::PowerProfile(profile)
|
||||
if matches!(*profile, "power-saver" | "balanced" | "performance") =>
|
||||
{
|
||||
Action::PowerProfile(profile) if matches!(*profile, "power-saver" | "balanced" | "performance") => {
|
||||
run("powerprofilesctl", &["set", profile])
|
||||
}
|
||||
Action::PowerProfile(_) => return Err("Invalid power profile".to_owned()),
|
||||
@@ -737,10 +680,7 @@ mod tests {
|
||||
assert!(bus.can_power("CanReboot"));
|
||||
assert!(!bus.can_power("CanSuspend"));
|
||||
mode.store(1, Ordering::Relaxed);
|
||||
assert!(
|
||||
!bus.can_power("CanPowerOff"),
|
||||
"capabilities must not be cached"
|
||||
);
|
||||
assert!(!bus.can_power("CanPowerOff"), "capabilities must not be cached");
|
||||
assert!(bus.connection.is_some());
|
||||
assert!(!bus.notification_service_owned());
|
||||
server.request_name("org.erikreider.swaync").unwrap();
|
||||
@@ -785,14 +725,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn status_stream_retains_latest_result_wakes_and_closes() {
|
||||
use cosmic::iced::futures::{
|
||||
Stream,
|
||||
task::{ArcWake, waker},
|
||||
};
|
||||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use cosmic::iced::futures::Stream;
|
||||
use cosmic::iced::futures::task::{ArcWake, waker};
|
||||
|
||||
#[derive(Default)]
|
||||
struct WakeCount(AtomicUsize);
|
||||
@@ -840,10 +777,7 @@ mod tests {
|
||||
panic!("worker result missing");
|
||||
};
|
||||
assert_eq!(update.generation, 3);
|
||||
assert!(matches!(
|
||||
stream.as_mut().poll_next(&mut context),
|
||||
Poll::Ready(None)
|
||||
));
|
||||
assert!(matches!(stream.as_mut().poll_next(&mut context), Poll::Ready(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -864,10 +798,7 @@ mod tests {
|
||||
state.1 = false;
|
||||
shared.1.notify_one();
|
||||
}
|
||||
assert_eq!(
|
||||
receiver.recv_timeout(Duration::from_secs(2)).unwrap(),
|
||||
Some(8)
|
||||
);
|
||||
assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), Some(8));
|
||||
worker.join().unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! BlueZ status from its object graph; never starts discovery or pairing.
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo::ManagedObjects;
|
||||
|
||||
use super::Bluetooth;
|
||||
use zbus::{blocking::Connection, fdo::ManagedObjects};
|
||||
|
||||
pub(super) fn read(connection: &Connection) -> zbus::Result<Option<Bluetooth>> {
|
||||
let objects: ManagedObjects = connection
|
||||
@@ -35,10 +37,7 @@ pub(super) fn read(connection: &Connection) -> zbus::Result<Option<Bluetooth>> {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some((path, _)) = adapters.iter().find(|(_, properties)| {
|
||||
properties
|
||||
.get("Address")
|
||||
.and_then(|value| <&str>::try_from(value).ok())
|
||||
== Some(address)
|
||||
properties.get("Address").and_then(|value| <&str>::try_from(value).ok()) == Some(address)
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -78,8 +77,9 @@ fn snapshot(objects: &ManagedObjects, selected: Option<&str>) -> Option<Bluetoot
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Str};
|
||||
|
||||
use super::*;
|
||||
fn graph() -> ManagedObjects {
|
||||
let mut graph = ManagedObjects::new();
|
||||
for (path, powered) in [("/org/bluez/hci0", true), ("/org/bluez/hci1", false)] {
|
||||
@@ -93,24 +93,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
for (path, adapter, connected, alias) in [
|
||||
(
|
||||
"/org/bluez/hci0/dev_a",
|
||||
"/org/bluez/hci0",
|
||||
true,
|
||||
"Headphones",
|
||||
),
|
||||
(
|
||||
"/org/bluez/hci0/dev_b",
|
||||
"/org/bluez/hci0",
|
||||
false,
|
||||
"Keyboard",
|
||||
),
|
||||
(
|
||||
"/org/bluez/hci1/dev_a",
|
||||
"/org/bluez/hci1",
|
||||
true,
|
||||
"Other adapter",
|
||||
),
|
||||
("/org/bluez/hci0/dev_a", "/org/bluez/hci0", true, "Headphones"),
|
||||
("/org/bluez/hci0/dev_b", "/org/bluez/hci0", false, "Keyboard"),
|
||||
("/org/bluez/hci1/dev_a", "/org/bluez/hci1", true, "Other adapter"),
|
||||
] {
|
||||
graph.insert(
|
||||
OwnedObjectPath::try_from(path).unwrap(),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//! Keep native snapshots until the owning service signals a change.
|
||||
use super::StatusBus;
|
||||
use cosmic::iced::futures::{FutureExt, StreamExt, channel::oneshot};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use cosmic::iced::futures::channel::oneshot;
|
||||
use cosmic::iced::futures::{FutureExt, StreamExt};
|
||||
use zbus::blocking::{Connection, MessageIterator};
|
||||
|
||||
use super::StatusBus;
|
||||
|
||||
struct Signals {
|
||||
revision: Arc<AtomicU64>,
|
||||
alive: Arc<AtomicBool>,
|
||||
@@ -44,9 +45,7 @@ impl Signals {
|
||||
let (stop, stopped) = oneshot::channel::<()>();
|
||||
std::thread::spawn(move || {
|
||||
// Enter a runtime for zbus stream cleanup, including cancellation.
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build();
|
||||
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build();
|
||||
if let Ok(runtime) = runtime {
|
||||
// Dropping the blocking wrappers enters zbus' runtime, so do
|
||||
// this before entering our async runtime.
|
||||
@@ -102,32 +101,19 @@ impl<T: Clone> Cache<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn read(
|
||||
&mut self,
|
||||
action: u64,
|
||||
read: fn(&Connection) -> zbus::Result<Option<T>>,
|
||||
) -> Option<T> {
|
||||
if self
|
||||
.signals
|
||||
.as_ref()
|
||||
.is_some_and(|s| !s.alive.load(Ordering::Acquire))
|
||||
{
|
||||
pub(super) fn read(&mut self, action: u64, read: fn(&Connection) -> zbus::Result<Option<T>>) -> Option<T> {
|
||||
if self.signals.as_ref().is_some_and(|s| !s.alive.load(Ordering::Acquire)) {
|
||||
self.signals = None;
|
||||
self.bus.connection = None;
|
||||
self.value = None;
|
||||
}
|
||||
if self.signals.is_none() {
|
||||
self.signals = self
|
||||
.bus
|
||||
.query(|connection| Signals::new(connection, self.service));
|
||||
self.signals = self.bus.query(|connection| Signals::new(connection, self.service));
|
||||
self.revision = 0;
|
||||
}
|
||||
// If subscription setup fails, read fresh rather than trusting a cache
|
||||
// with no invalidation. Retry setup on the next regular status poll.
|
||||
let revision = self
|
||||
.signals
|
||||
.as_ref()
|
||||
.map(|s| s.revision.load(Ordering::Acquire));
|
||||
let revision = self.signals.as_ref().map(|s| s.revision.load(Ordering::Acquire));
|
||||
if revision.is_none()
|
||||
|| revision != Some(self.revision)
|
||||
|| action != self.action
|
||||
@@ -149,8 +135,9 @@ impl<T: Clone> Cache<T> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::*;
|
||||
struct Counter(Arc<AtomicU64>);
|
||||
#[zbus::interface(name = "org.ferese.StatusCounter")]
|
||||
impl Counter {
|
||||
@@ -193,13 +180,7 @@ mod tests {
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(cache.read(1, read_counter), Some(2));
|
||||
server
|
||||
.emit_signal(
|
||||
None::<&str>,
|
||||
"/test",
|
||||
"org.ferese.StatusCounter",
|
||||
"Changed",
|
||||
&(),
|
||||
)
|
||||
.emit_signal(None::<&str>, "/test", "org.ferese.StatusCounter", "Changed", &())
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while cache.read(1, read_counter) != Some(3) {
|
||||
@@ -247,13 +228,7 @@ mod tests {
|
||||
};
|
||||
let before = signals.revision.load(Ordering::Acquire);
|
||||
server
|
||||
.emit_signal(
|
||||
None::<&str>,
|
||||
"/test",
|
||||
"org.ferese.StatusTest",
|
||||
"Changed",
|
||||
&(),
|
||||
)
|
||||
.emit_signal(None::<&str>, "/test", "org.ferese.StatusTest", "Changed", &())
|
||||
.unwrap();
|
||||
wait(before);
|
||||
let before = signals.revision.load(Ordering::Acquire);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Read NetworkManager directly; no scans or network changes are requested.
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::zvariant::OwnedObjectPath;
|
||||
|
||||
use super::Network;
|
||||
use zbus::{blocking::Connection, zvariant::OwnedObjectPath};
|
||||
|
||||
const SERVICE: &str = "org.freedesktop.NetworkManager";
|
||||
const ROOT: &str = "/org/freedesktop/NetworkManager";
|
||||
@@ -86,8 +88,9 @@ pub(super) fn read(connection: &Connection) -> zbus::Result<Option<Network>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::*;
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
wifi: bool,
|
||||
@@ -120,12 +123,7 @@ mod tests {
|
||||
impl Wireless {
|
||||
#[zbus(property)]
|
||||
fn active_access_point(&self) -> OwnedObjectPath {
|
||||
OwnedObjectPath::try_from(if self.0.lock().unwrap().active {
|
||||
"/ap"
|
||||
} else {
|
||||
"/"
|
||||
})
|
||||
.unwrap()
|
||||
OwnedObjectPath::try_from(if self.0.lock().unwrap().active { "/ap" } else { "/" }).unwrap()
|
||||
}
|
||||
}
|
||||
struct Ap(Arc<Mutex<State>>);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::*;
|
||||
use cosmic::iced::platform_specific::runtime::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
||||
use cosmic::iced::{Alignment, Rectangle};
|
||||
use cosmic::widget::{column, slider};
|
||||
use status::{Action, Snapshot};
|
||||
|
||||
use super::*;
|
||||
|
||||
const DEVICE_LIST_HEIGHT: f32 = 180.0;
|
||||
|
||||
fn device_list_height(count: usize) -> Option<f32> {
|
||||
@@ -131,11 +132,7 @@ impl FereseShell {
|
||||
.map(|(width, _)| {
|
||||
let theme = self.config.theme;
|
||||
Rectangle {
|
||||
x: (width
|
||||
- theme.bar_margin_horizontal * 2
|
||||
- theme.panel_padding.round() as i32
|
||||
- 1)
|
||||
.max(0),
|
||||
x: (width - theme.bar_margin_horizontal * 2 - theme.panel_padding.round() as i32 - 1).max(0),
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: theme.bar_height.round() as i32,
|
||||
@@ -170,8 +167,7 @@ impl FereseShell {
|
||||
},
|
||||
Some(Box::new(Self::view_status_menu)),
|
||||
);
|
||||
Task::batch([destroy, notifications])
|
||||
.chain(cosmic::task::message(cosmic::Action::Surface(action)))
|
||||
Task::batch([destroy, notifications]).chain(cosmic::task::message(cosmic::Action::Surface(action)))
|
||||
}
|
||||
|
||||
pub fn destroy_menu(&mut self) -> Task<Message> {
|
||||
@@ -184,9 +180,7 @@ impl FereseShell {
|
||||
self.notifications.hovered = None;
|
||||
}
|
||||
Task::batch([
|
||||
cosmic::task::message(cosmic::Action::Surface(
|
||||
cosmic::surface::action::destroy_popup(menu.id),
|
||||
)),
|
||||
cosmic::task::message(cosmic::Action::Surface(cosmic::surface::action::destroy_popup(menu.id))),
|
||||
self.sync_notification_surface(),
|
||||
])
|
||||
}
|
||||
@@ -283,8 +277,7 @@ impl FereseShell {
|
||||
status_icon(kind, &self.status)
|
||||
};
|
||||
let selected = self.menu.as_ref().is_some_and(|m| m.kind == kind)
|
||||
|| (kind == Menu::Recording
|
||||
&& matches!(self.recorder.state, recording::State::Recording(_)));
|
||||
|| (kind == Menu::Recording && matches!(self.recorder.state, recording::State::Recording(_)));
|
||||
let foreground = color(if selected {
|
||||
theme.accent
|
||||
} else if enabled {
|
||||
@@ -328,16 +321,16 @@ impl FereseShell {
|
||||
.as_ref()
|
||||
.is_some_and(|n| n.count > 0 && !n.dnd)
|
||||
{
|
||||
content = content.push(container(text("")).width(4).height(4).class(
|
||||
theme::Container::custom(move |_| container::Style {
|
||||
content = content.push(container(text("")).width(4).height(4).class(theme::Container::custom(
|
||||
move |_| container::Style {
|
||||
background: Some(Background::Color(foreground)),
|
||||
border: Border {
|
||||
radius: motion::radius(2.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
},
|
||||
)));
|
||||
}
|
||||
// A fixed button height does not center its child in libcosmic.
|
||||
// center_y/center_x wrap the content in a centering layout, so use
|
||||
@@ -355,10 +348,7 @@ impl FereseShell {
|
||||
} else {
|
||||
status_label(kind, &self.status)
|
||||
})
|
||||
.padding([
|
||||
0.0,
|
||||
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(4.0),
|
||||
])
|
||||
.padding([0.0, ((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(4.0)])
|
||||
.height(metrics.control_height)
|
||||
.on_press_with_rectangle(move |offset, bounds| {
|
||||
if kind == Menu::Recording {
|
||||
@@ -404,11 +394,7 @@ impl FereseShell {
|
||||
.into();
|
||||
}
|
||||
let theme = self.config.theme;
|
||||
let p = if menu.effects.is_some() {
|
||||
1.0
|
||||
} else {
|
||||
menu.progress()
|
||||
};
|
||||
let p = if menu.effects.is_some() { 1.0 } else { menu.progress() };
|
||||
let kind = menu.kind;
|
||||
let primary = color_with_opacity(theme.text_primary, p);
|
||||
let muted = color_with_opacity(theme.text_muted, p);
|
||||
@@ -505,10 +491,7 @@ impl FereseShell {
|
||||
));
|
||||
}
|
||||
|
||||
if self.status.network.is_some()
|
||||
|| self.status.bluetooth.is_some()
|
||||
|| self.status.battery.is_some()
|
||||
{
|
||||
if self.status.network.is_some() || self.status.bluetooth.is_some() || self.status.battery.is_some() {
|
||||
rows = rows.push(control_card(connections.into(), primary, p));
|
||||
}
|
||||
}
|
||||
@@ -690,9 +673,7 @@ impl FereseShell {
|
||||
if menu.kind == Menu::Battery
|
||||
&& let Some(b) = &self.status.battery
|
||||
{
|
||||
let battery_color = if b.percent < self.config.status.low_battery_threshold
|
||||
&& b.status != "Charging"
|
||||
{
|
||||
let battery_color = if b.percent < self.config.status.low_battery_threshold && b.status != "Charging" {
|
||||
Color {
|
||||
a: p,
|
||||
..Color::from_rgb8(230, 172, 90)
|
||||
@@ -734,38 +715,19 @@ impl FereseShell {
|
||||
}
|
||||
|
||||
if menu.kind == Menu::Battery {
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
"Power mode",
|
||||
shell_font(),
|
||||
muted,
|
||||
));
|
||||
rows = rows.push(ferese_theme::menus::section_label("Power mode", shell_font(), muted));
|
||||
if let Some(profiles) = &self.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
(
|
||||
"Power saver",
|
||||
ferese_theme::icons::POWER_SAVER,
|
||||
"power-saver",
|
||||
),
|
||||
("Power saver", ferese_theme::icons::POWER_SAVER, "power-saver"),
|
||||
("Balanced", ferese_theme::icons::POWER_BALANCED, "balanced"),
|
||||
(
|
||||
"Performance",
|
||||
ferese_theme::icons::POWER_PERFORMANCE,
|
||||
"performance",
|
||||
),
|
||||
("Performance", ferese_theme::icons::POWER_PERFORMANCE, "performance"),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if profiles.available[index] {
|
||||
modes = modes.push(power_tile(
|
||||
label,
|
||||
icon,
|
||||
profile,
|
||||
profiles.active == profile,
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
modes = modes.push(power_tile(label, icon, profile, profiles.active == profile, theme, p));
|
||||
}
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
@@ -804,21 +766,13 @@ impl FereseShell {
|
||||
}
|
||||
|
||||
if self.status.reboot || self.status.poweroff || self.status.suspend {
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
"Power",
|
||||
shell_font(),
|
||||
muted,
|
||||
));
|
||||
rows = rows.push(ferese_theme::menus::section_label("Power", shell_font(), muted));
|
||||
rows = rows.push(power);
|
||||
}
|
||||
}
|
||||
|
||||
if !menu.kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
text("Service unavailable")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
rows = rows.push(text("Service unavailable").size(13).class(theme::Text::Color(muted)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,9 +789,8 @@ impl FereseShell {
|
||||
.width(kind.width())
|
||||
.padding(16)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: (!compositor_material).then_some(Background::Color(
|
||||
color_with_opacity(theme.surface_popover, p),
|
||||
)),
|
||||
background: (!compositor_material)
|
||||
.then_some(Background::Color(color_with_opacity(theme.surface_popover, p))),
|
||||
text_color: Some(primary),
|
||||
icon_color: Some(primary),
|
||||
border: Border {
|
||||
@@ -886,11 +839,7 @@ fn status_summary<'a>(
|
||||
.center_y(32)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: if enabled {
|
||||
0.22 * opacity
|
||||
} else {
|
||||
0.06 * opacity
|
||||
},
|
||||
a: if enabled { 0.22 * opacity } else { 0.06 * opacity },
|
||||
..badge_color
|
||||
})),
|
||||
border: Border {
|
||||
@@ -926,11 +875,7 @@ fn status_summary<'a>(
|
||||
summary.into()
|
||||
}
|
||||
|
||||
fn level_meter(
|
||||
value: u8,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element<'static, cosmic::Action<Message>> {
|
||||
let segment = move |amount: u16, filled: bool| {
|
||||
container(text(""))
|
||||
.width(Length::FillPortion(amount))
|
||||
@@ -1015,10 +960,7 @@ fn connection_control<'a>(
|
||||
.height(28)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center);
|
||||
let content = column![icon]
|
||||
.spacing(2)
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center);
|
||||
let content = column![icon].spacing(2).width(Length::Fill).align_x(Alignment::Center);
|
||||
let content = content.push(
|
||||
text(connection_caption(detail))
|
||||
.size(11)
|
||||
@@ -1032,18 +974,12 @@ fn connection_control<'a>(
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
let Some(action) = action else {
|
||||
return container(content)
|
||||
.width(Length::FillPortion(1))
|
||||
.padding(4)
|
||||
.into();
|
||||
return container(content).width(Length::FillPortion(1)).padding(4).into();
|
||||
};
|
||||
let button = button::custom(content)
|
||||
.width(Length::Fill)
|
||||
.padding(4)
|
||||
.name(format!(
|
||||
"{label}: {}, {detail}",
|
||||
if enabled { "on" } else { "off" }
|
||||
))
|
||||
.name(format!("{label}: {}, {detail}", if enabled { "on" } else { "off" }))
|
||||
.on_press(cosmic::Action::App(Message::Control(action)));
|
||||
let tile: Element<'_, cosmic::Action<Message>> = if selected {
|
||||
button
|
||||
@@ -1068,14 +1004,7 @@ fn power_tile<'a>(
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(
|
||||
if selected {
|
||||
theme.accent
|
||||
} else {
|
||||
theme.text_primary
|
||||
},
|
||||
opacity,
|
||||
);
|
||||
let foreground = color_with_opacity(if selected { theme.accent } else { theme.text_primary }, opacity);
|
||||
let tile = container(
|
||||
column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
.spacing(3)
|
||||
@@ -1086,9 +1015,7 @@ fn power_tile<'a>(
|
||||
let btn = button::custom(tile)
|
||||
.width(Length::FillPortion(1))
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
|
||||
profile,
|
||||
))));
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(profile))));
|
||||
|
||||
motion::button(btn, foreground, selected, opacity)
|
||||
}
|
||||
@@ -1116,11 +1043,7 @@ fn power_tile_action<'a>(
|
||||
motion::button(btn, foreground, false, opacity)
|
||||
}
|
||||
|
||||
fn calendar_grid<'a>(
|
||||
offset: i32,
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
fn calendar_grid<'a>(offset: i32, theme: ShellTheme, opacity: f32) -> Element<'a, cosmic::Action<Message>> {
|
||||
let primary = color_with_opacity(theme.text_primary, opacity);
|
||||
ferese_theme::calendar::grid(
|
||||
Zoned::now().date(),
|
||||
@@ -1176,11 +1099,7 @@ fn toggle_row<'a>(
|
||||
palette: ShellTheme,
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
ferese_theme::menus::row(
|
||||
label,
|
||||
shell_switch(label, on, action, palette, opacity),
|
||||
shell_font(),
|
||||
)
|
||||
ferese_theme::menus::row(label, shell_switch(label, on, action, palette, opacity), shell_font())
|
||||
}
|
||||
|
||||
fn slider_row(
|
||||
@@ -1191,24 +1110,16 @@ fn slider_row(
|
||||
icon_accent: Color,
|
||||
opacity: f32,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
let control = slider(
|
||||
if brightness { 1..=100 } else { 0..=100 },
|
||||
value,
|
||||
move |value| {
|
||||
cosmic::Action::App(Message::Control(if brightness {
|
||||
Action::Brightness(value)
|
||||
} else {
|
||||
Action::Volume(value)
|
||||
}))
|
||||
},
|
||||
)
|
||||
let control = slider(if brightness { 1..=100 } else { 0..=100 }, value, move |value| {
|
||||
cosmic::Action::App(Message::Control(if brightness {
|
||||
Action::Brightness(value)
|
||||
} else {
|
||||
Action::Volume(value)
|
||||
}))
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(24)
|
||||
.class(ferese_theme::menus::slider(
|
||||
foreground,
|
||||
opacity,
|
||||
motion::radius(5.),
|
||||
));
|
||||
.class(ferese_theme::menus::slider(foreground, opacity, motion::radius(5.)));
|
||||
row![
|
||||
accented_icon(source, 18, foreground, icon_accent),
|
||||
control,
|
||||
@@ -1222,10 +1133,7 @@ fn slider_row(
|
||||
fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||
match kind {
|
||||
Menu::Network => match &s.network {
|
||||
Some(n) if n.enabled => format!(
|
||||
"Wi-Fi: {}",
|
||||
n.connection.as_deref().unwrap_or("not connected")
|
||||
),
|
||||
Some(n) if n.enabled => format!("Wi-Fi: {}", n.connection.as_deref().unwrap_or("not connected")),
|
||||
_ => "Wi-Fi: off".into(),
|
||||
},
|
||||
Menu::Bluetooth => match &s.bluetooth {
|
||||
@@ -1280,9 +1188,7 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
||||
}
|
||||
}
|
||||
Menu::Bluetooth => match &s.bluetooth {
|
||||
Some(b) if b.enabled && !b.devices.is_empty() => {
|
||||
(ferese_theme::icons::BLUETOOTH_CONNECTED, true)
|
||||
}
|
||||
Some(b) if b.enabled && !b.devices.is_empty() => (ferese_theme::icons::BLUETOOTH_CONNECTED, true),
|
||||
Some(b) if b.enabled => (ferese_theme::icons::BLUETOOTH_ON, true),
|
||||
_ => (ferese_theme::icons::BLUETOOTH_OFF, false),
|
||||
},
|
||||
@@ -1328,14 +1234,8 @@ mod tests {
|
||||
assert_eq!(super::connection_caption("88%"), "88%");
|
||||
assert_eq!(super::connection_caption("Flow84@Lofree"), "Flow84@Lofr…");
|
||||
assert_eq!(super::connection_caption("123456789012"), "123456789012");
|
||||
assert_eq!(
|
||||
super::connection_caption("My very long headphones"),
|
||||
"My very lon…"
|
||||
);
|
||||
assert_eq!(
|
||||
super::connection_caption(" Device\n name "),
|
||||
"Device name"
|
||||
);
|
||||
assert_eq!(super::connection_caption("My very long headphones"), "My very lon…");
|
||||
assert_eq!(super::connection_caption(" Device\n name "), "Device name");
|
||||
let caption = super::connection_caption("耳機耳機耳機耳機耳機耳機耳機");
|
||||
assert_eq!(caption.chars().count(), 12);
|
||||
assert!(caption.ends_with('…'));
|
||||
@@ -1397,41 +1297,26 @@ mod tests {
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
status_icon(Menu::Audio, &s),
|
||||
(ferese_theme::icons::VOLUME_MUTE, false)
|
||||
);
|
||||
assert_eq!(status_icon(Menu::Audio, &s), (ferese_theme::icons::VOLUME_MUTE, false));
|
||||
|
||||
s.battery = Some(status::Battery {
|
||||
percent: 2,
|
||||
status: "Charging".into(),
|
||||
});
|
||||
assert_eq!(
|
||||
status_icon(Menu::Battery, &s).0,
|
||||
ferese_theme::icons::BATTERY_CHARGING
|
||||
);
|
||||
assert_eq!(status_icon(Menu::Battery, &s).0, ferese_theme::icons::BATTERY_CHARGING);
|
||||
|
||||
s.notifications = Some(status::Notifications {
|
||||
count: 3,
|
||||
dnd: true,
|
||||
});
|
||||
s.notifications = Some(status::Notifications { count: 3, dnd: true });
|
||||
assert_eq!(
|
||||
status_icon(Menu::Notifications, &s).0,
|
||||
ferese_theme::icons::NOTIFICATIONS_OFF
|
||||
);
|
||||
assert_eq!(
|
||||
status_label(Menu::Notifications, &s),
|
||||
"Notifications: do not disturb"
|
||||
);
|
||||
assert_eq!(status_label(Menu::Notifications, &s), "Notifications: do not disturb");
|
||||
|
||||
s.bluetooth = Some(status::Bluetooth {
|
||||
enabled: false,
|
||||
devices: vec!["Headphones".into()],
|
||||
});
|
||||
assert_eq!(
|
||||
status_icon(Menu::Bluetooth, &s).0,
|
||||
ferese_theme::icons::BLUETOOTH_OFF
|
||||
);
|
||||
assert_eq!(status_icon(Menu::Bluetooth, &s).0, ferese_theme::icons::BLUETOOTH_OFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::*;
|
||||
use cosmic::iced::widget::Space;
|
||||
use cosmic::widget::column;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum PowerAction {
|
||||
Poweroff,
|
||||
@@ -79,26 +80,17 @@ impl SystemModal {
|
||||
}
|
||||
|
||||
impl FereseShell {
|
||||
pub(super) fn open_system_modal(
|
||||
&mut self,
|
||||
action: PowerAction,
|
||||
output_name: Option<&str>,
|
||||
) -> Task<Message> {
|
||||
pub(super) fn open_system_modal(&mut self, action: PowerAction, output_name: Option<&str>) -> Task<Message> {
|
||||
self.open_modal(Content::Power(action), output_name)
|
||||
}
|
||||
|
||||
pub(super) fn rebuild_system_modal(&mut self) -> Task<Message> {
|
||||
let guide = self
|
||||
.system_modal
|
||||
.as_ref()
|
||||
.and_then(|modal| match &modal.content {
|
||||
Content::Guide(entries)
|
||||
if !modal.motion.closing() && self.config.status.keybinding_guide =>
|
||||
{
|
||||
Some(entries.clone())
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let guide = self.system_modal.as_ref().and_then(|modal| match &modal.content {
|
||||
Content::Guide(entries) if !modal.motion.closing() && self.config.status.keybinding_guide => {
|
||||
Some(entries.clone())
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let destroy = self.destroy_system_modal(true);
|
||||
if let Some(entries) = guide {
|
||||
if self.outputs.is_empty() {
|
||||
@@ -139,11 +131,7 @@ impl FereseShell {
|
||||
.iter()
|
||||
.position(|output| output.name.as_deref() == Some(name))
|
||||
})
|
||||
.or_else(|| {
|
||||
self.outputs
|
||||
.iter()
|
||||
.position(|output| output.bar == self.bar_surface_id)
|
||||
})
|
||||
.or_else(|| self.outputs.iter().position(|output| output.bar == self.bar_surface_id))
|
||||
.unwrap_or(0);
|
||||
let mut tasks = vec![
|
||||
self.destroy_system_modal(true),
|
||||
@@ -243,11 +231,7 @@ impl FereseShell {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn attach_power_material(
|
||||
&mut self,
|
||||
id: window::Id,
|
||||
surface: &wl_surface::WlSurface,
|
||||
) {
|
||||
pub(super) fn attach_power_material(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
|
||||
let Some(modal) = &mut self.system_modal else {
|
||||
return;
|
||||
};
|
||||
@@ -268,8 +252,7 @@ impl FereseShell {
|
||||
for entry in &modal.surfaces {
|
||||
if let Some(effects) = &entry.effects {
|
||||
let regions = entry.regions.lock().unwrap().clone();
|
||||
let _ = effects
|
||||
.set_material_regions(®ions, ferese_surface_effects_v1::Role::Modal);
|
||||
let _ = effects.set_material_regions(®ions, ferese_surface_effects_v1::Role::Modal);
|
||||
let _ = effects.set_opacity(modal.motion.progress());
|
||||
}
|
||||
}
|
||||
@@ -353,12 +336,7 @@ impl FereseShell {
|
||||
let action = match power {
|
||||
PowerAction::Logout(serial) => {
|
||||
if let Some(control) = &self.control {
|
||||
control.confirm_logout(
|
||||
serial,
|
||||
approval.revision,
|
||||
approval.token,
|
||||
approval.force(),
|
||||
);
|
||||
control.confirm_logout(serial, approval.revision, approval.token, approval.force());
|
||||
return self.destroy_system_modal(false);
|
||||
}
|
||||
modal.error = Some("Ferese shell control is unavailable".to_owned());
|
||||
@@ -391,11 +369,7 @@ impl FereseShell {
|
||||
Task::batch([destroy, execute])
|
||||
}
|
||||
|
||||
pub(super) fn finish_power_action(
|
||||
&mut self,
|
||||
id: window::Id,
|
||||
result: Result<(), String>,
|
||||
) -> Task<Message> {
|
||||
pub(super) fn finish_power_action(&mut self, id: window::Id, result: Result<(), String>) -> Task<Message> {
|
||||
let Some((pending_id, action)) = self.pending_power else {
|
||||
return Task::none();
|
||||
};
|
||||
@@ -454,16 +428,10 @@ impl FereseShell {
|
||||
column::with_capacity(inhibitors.reasons.len()).spacing(6),
|
||||
|rows, reason| rows.push(text(reason).size(13)),
|
||||
);
|
||||
rows = rows.push(
|
||||
container(cosmic::widget::scrollable(items).height(Length::Shrink))
|
||||
.max_height(160),
|
||||
);
|
||||
rows = rows
|
||||
.push(container(cosmic::widget::scrollable(items).height(Length::Shrink)).max_height(160));
|
||||
}
|
||||
let label = if modal
|
||||
.inhibitors
|
||||
.as_ref()
|
||||
.is_some_and(|approval| approval.force())
|
||||
{
|
||||
let label = if modal.inhibitors.as_ref().is_some_and(|approval| approval.force()) {
|
||||
format!("{} anyway", action.label())
|
||||
} else {
|
||||
action.label().to_owned()
|
||||
@@ -474,20 +442,14 @@ impl FereseShell {
|
||||
rows.push(
|
||||
row![
|
||||
Space::new().width(Length::Fill),
|
||||
ferese_theme::controls::text_button(
|
||||
"Cancel",
|
||||
shell_font(),
|
||||
palette,
|
||||
false
|
||||
)
|
||||
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||
ferese_theme::controls::text_button(label, shell_font(), palette, true)
|
||||
.on_press_maybe(
|
||||
modal
|
||||
.inhibitors
|
||||
.is_some()
|
||||
.then_some(cosmic::Action::App(Message::ExecutePower))
|
||||
),
|
||||
ferese_theme::controls::text_button("Cancel", shell_font(), palette, false)
|
||||
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||
ferese_theme::controls::text_button(label, shell_font(), palette, true).on_press_maybe(
|
||||
modal
|
||||
.inhibitors
|
||||
.is_some()
|
||||
.then_some(cosmic::Action::App(Message::ExecutePower))
|
||||
),
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
@@ -498,9 +460,7 @@ impl FereseShell {
|
||||
bindings = bindings.push(
|
||||
row![
|
||||
text(&entry.keys).size(13).width(Length::FillPortion(1)),
|
||||
text(&entry.description)
|
||||
.size(13)
|
||||
.width(Length::FillPortion(1)),
|
||||
text(&entry.description).size(13).width(Length::FillPortion(1)),
|
||||
]
|
||||
.spacing(16),
|
||||
);
|
||||
@@ -516,26 +476,16 @@ impl FereseShell {
|
||||
.clamp(40, 400) as f32;
|
||||
column![
|
||||
text("Welcome to Ferese").size(23),
|
||||
ferese_theme::menus::section_label(
|
||||
"Your active shortcuts",
|
||||
shell_font(),
|
||||
palette.muted,
|
||||
)
|
||||
.size(14),
|
||||
container(cosmic::widget::scrollable(bindings).height(Length::Shrink))
|
||||
.max_height(height),
|
||||
ferese_theme::menus::section_label("Your active shortcuts", shell_font(), palette.muted,)
|
||||
.size(14),
|
||||
container(cosmic::widget::scrollable(bindings).height(Length::Shrink)).max_height(height),
|
||||
text("Disable this guide in Settings → Shortcuts. It appears at each login until disabled.")
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
row![
|
||||
Space::new().width(Length::Fill),
|
||||
ferese_theme::controls::text_button(
|
||||
"Got it",
|
||||
shell_font(),
|
||||
palette,
|
||||
true,
|
||||
)
|
||||
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||
ferese_theme::controls::text_button("Got it", shell_font(), palette, true,)
|
||||
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||
],
|
||||
]
|
||||
.spacing(14)
|
||||
@@ -547,10 +497,8 @@ impl FereseShell {
|
||||
.max_width(if modal.is_guide() { 600 } else { 440 })
|
||||
.padding(24)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: (!material).then_some(Background::Color(color_with_opacity(
|
||||
theme.surface_popover,
|
||||
alpha,
|
||||
))),
|
||||
background: (!material)
|
||||
.then_some(Background::Color(color_with_opacity(theme.surface_popover, alpha))),
|
||||
text_color: Some(palette.text),
|
||||
icon_color: Some(palette.text),
|
||||
border: Border {
|
||||
@@ -582,12 +530,7 @@ impl FereseShell {
|
||||
.align_x(cosmic::iced::Alignment::Center)
|
||||
.align_y(cosmic::iced::Alignment::Center)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgba(
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.34 * progress,
|
||||
))),
|
||||
background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.34 * progress))),
|
||||
..Default::default()
|
||||
}));
|
||||
centered.into()
|
||||
@@ -612,10 +555,7 @@ mod tests {
|
||||
PowerAction::from_status(status::Action::Suspend),
|
||||
Some(PowerAction::Suspend)
|
||||
);
|
||||
assert_eq!(
|
||||
PowerAction::from_status(status::Action::Brightness(50)),
|
||||
None
|
||||
);
|
||||
assert_eq!(PowerAction::from_status(status::Action::Brightness(50)), None);
|
||||
assert_eq!(PowerAction::Logout(1).label(), "Log out");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user