Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+7 -14
View File
@@ -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;
}
+8 -20
View File
@@ -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,
+38 -79
View File
@@ -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)
}
+9 -14
View File
@@ -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()