2023-10-05 17:47:23 -06:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2024-07-28 05:12:48 +02:00
|
|
|
mod ipc;
|
|
|
|
|
|
2025-08-06 21:41:38 -04:00
|
|
|
use crate::wayland::{self, WaylandUpdate};
|
|
|
|
|
use cctk::sctk::reexports::calloop;
|
2025-09-12 17:39:37 -04:00
|
|
|
use color_eyre::eyre::WrapErr;
|
2025-03-05 23:08:56 -05:00
|
|
|
use cosmic::app::{Core, Settings, Task};
|
|
|
|
|
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
|
2026-04-28 15:13:02 +02:00
|
|
|
use cosmic::cosmic_config::{self, ConfigSet};
|
|
|
|
|
use cosmic::cosmic_theme::{self, CosmicPalette};
|
|
|
|
|
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
|
2025-08-25 00:25:34 -04:00
|
|
|
use cosmic::iced::event::listen_with;
|
2026-04-28 15:13:02 +02:00
|
|
|
use cosmic::iced::event::wayland::OutputEvent;
|
|
|
|
|
use cosmic::iced::futures::SinkExt;
|
|
|
|
|
use cosmic::iced::platform_specific::runtime::wayland::layer_surface::{
|
|
|
|
|
IcedMargin, IcedOutput, SctkLayerSurfaceSettings,
|
2023-10-06 15:02:25 -06:00
|
|
|
};
|
2026-04-28 15:13:02 +02:00
|
|
|
use cosmic::iced::platform_specific::shell::wayland::commands::layer_surface::{
|
|
|
|
|
Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface,
|
|
|
|
|
};
|
|
|
|
|
use cosmic::iced::platform_specific::shell::wayland::commands::subsurface::reposition_subsurface;
|
|
|
|
|
use cosmic::iced::runtime::core::window::Id as SurfaceId;
|
|
|
|
|
use cosmic::iced::runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
|
|
|
|
|
use cosmic::iced::{
|
2026-07-08 18:27:28 -04:00
|
|
|
self, Alignment, Background, Border, Color, Length, Point, Rectangle, Size, Subscription,
|
|
|
|
|
window,
|
2025-08-11 14:40:54 -04:00
|
|
|
};
|
2026-04-28 15:13:02 +02:00
|
|
|
use cosmic::widget::{id_container, text};
|
|
|
|
|
use cosmic::{Element, executor, surface, theme, widget};
|
2024-08-22 08:17:06 -04:00
|
|
|
use cosmic_greeter_config::Config as CosmicGreeterConfig;
|
2026-03-13 08:35:18 -06:00
|
|
|
use cosmic_greeter_daemon::{UserData, UserFilter};
|
2025-10-27 13:05:21 +01:00
|
|
|
use cosmic_randr_shell::{KdlParseWithError, List};
|
2026-02-24 15:49:22 -05:00
|
|
|
use cosmic_settings_a11y_manager_subscription::{AccessibilityEvent, AccessibilityRequest};
|
2024-07-28 05:12:48 +02:00
|
|
|
use greetd_ipc::Request;
|
2025-08-20 18:06:41 -04:00
|
|
|
use kdl::KdlDocument;
|
2026-04-28 15:13:02 +02:00
|
|
|
use std::collections::{HashMap, hash_map};
|
|
|
|
|
use std::error::Error;
|
|
|
|
|
use std::num::NonZeroU32;
|
2025-08-20 18:06:41 -04:00
|
|
|
use std::process::Stdio;
|
2026-04-28 15:13:02 +02:00
|
|
|
use std::sync::{Arc, LazyLock};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
use std::{fs, io, process};
|
2025-08-07 19:05:31 -04:00
|
|
|
use tokio::process::Child;
|
|
|
|
|
use tokio::time;
|
2025-09-12 17:39:37 -04:00
|
|
|
use tracing::metadata::LevelFilter;
|
|
|
|
|
use tracing::warn;
|
2026-04-28 15:13:02 +02:00
|
|
|
use tracing_subscriber::prelude::*;
|
|
|
|
|
use tracing_subscriber::{EnvFilter, fmt};
|
|
|
|
|
use wayland_client::Proxy;
|
|
|
|
|
use wayland_client::protocol::wl_output::WlOutput;
|
2025-04-10 13:03:53 +02:00
|
|
|
use zbus::{Connection, proxy};
|
2024-02-06 15:03:07 -07:00
|
|
|
|
2026-04-28 15:13:02 +02:00
|
|
|
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT};
|
|
|
|
|
use crate::fl;
|
2024-05-07 10:08:03 -06:00
|
|
|
|
2025-06-26 15:14:43 -04:00
|
|
|
static USERNAME_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("username-id"));
|
|
|
|
|
|
2024-05-17 09:17:02 -06:00
|
|
|
#[proxy(
|
2024-02-06 15:03:07 -07:00
|
|
|
interface = "com.system76.CosmicGreeter",
|
|
|
|
|
default_service = "com.system76.CosmicGreeter",
|
|
|
|
|
default_path = "/com/system76/CosmicGreeter"
|
|
|
|
|
)]
|
|
|
|
|
trait Greeter {
|
|
|
|
|
async fn get_user_data(&self) -> Result<String, zbus::Error>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn user_data_dbus() -> Result<Vec<UserData>, Box<dyn Error>> {
|
|
|
|
|
let connection = Connection::system().await?;
|
|
|
|
|
|
|
|
|
|
// `dbus_proxy` macro creates `MyGreaterProxy` based on `Notifications` trait.
|
|
|
|
|
let proxy = GreeterProxy::new(&connection).await?;
|
|
|
|
|
let reply = proxy.get_user_data().await?;
|
2023-10-05 17:47:23 -06:00
|
|
|
|
2024-02-06 15:03:07 -07:00
|
|
|
let user_datas: Vec<UserData> = ron::from_str(&reply)?;
|
|
|
|
|
Ok(user_datas)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_data_fallback() -> Vec<UserData> {
|
2026-03-13 08:35:18 -06:00
|
|
|
let user_filter = UserFilter::new();
|
|
|
|
|
|
2023-10-05 17:47:23 -06:00
|
|
|
// The pwd::Passwd method is unsafe (but not labelled as such) due to using global state (libc pwent functions).
|
2024-02-06 15:03:07 -07:00
|
|
|
/* unsafe */
|
|
|
|
|
{
|
2023-10-05 17:47:23 -06:00
|
|
|
pwd::Passwd::iter()
|
2026-03-13 08:35:18 -06:00
|
|
|
.filter(|user| user_filter.filter(user))
|
2025-05-09 11:27:14 -06:00
|
|
|
.map(UserData::from)
|
2023-10-05 17:47:23 -06:00
|
|
|
.collect()
|
2024-02-06 15:03:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn main() -> Result<(), Box<dyn Error>> {
|
2025-09-12 17:39:37 -04:00
|
|
|
color_eyre::install().wrap_err("failed to install color_eyre error handler")?;
|
|
|
|
|
|
|
|
|
|
let trace = tracing_subscriber::registry();
|
|
|
|
|
let env_filter = EnvFilter::builder()
|
|
|
|
|
.with_default_directive(LevelFilter::WARN.into())
|
|
|
|
|
.from_env_lossy();
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "systemd")]
|
|
|
|
|
if let Ok(journald) = tracing_journald::layer() {
|
|
|
|
|
trace
|
|
|
|
|
.with(journald)
|
|
|
|
|
.with(env_filter)
|
|
|
|
|
.try_init()
|
|
|
|
|
.wrap_err("failed to initialize logger")?;
|
|
|
|
|
} else {
|
|
|
|
|
trace
|
|
|
|
|
.with(fmt::layer())
|
|
|
|
|
.with(env_filter)
|
|
|
|
|
.try_init()
|
|
|
|
|
.wrap_err("failed to initialize logger")?;
|
|
|
|
|
warn!("failed to connect to journald")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "systemd"))]
|
|
|
|
|
trace
|
|
|
|
|
.with(fmt::layer())
|
|
|
|
|
.with(env_filter)
|
|
|
|
|
.try_init()
|
|
|
|
|
.wrap_err("failed to initialize logger")?;
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2024-05-07 10:07:48 -06:00
|
|
|
crate::localize::localize();
|
2025-02-21 17:05:50 -05:00
|
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
|
|
|
.enable_all()
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut user_datas = match runtime.block_on(user_data_dbus()) {
|
2024-02-06 15:03:07 -07:00
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("failed to load user data from daemon: {}", err);
|
2024-02-06 15:03:07 -07:00
|
|
|
user_data_fallback()
|
|
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
};
|
|
|
|
|
|
2024-02-06 15:03:07 -07:00
|
|
|
// Sort user data by uid
|
|
|
|
|
user_datas.sort_by(|a, b| a.uid.cmp(&b.uid));
|
2024-08-22 08:17:06 -04:00
|
|
|
let (mut greeter_config, greeter_config_handler) = CosmicGreeterConfig::load();
|
|
|
|
|
// Filter out users that were removed from the system since the last time we loaded config
|
|
|
|
|
greeter_config.users.retain(|uid, _| {
|
|
|
|
|
user_datas
|
|
|
|
|
.binary_search_by(|probe| probe.uid.cmp(&uid.get()))
|
|
|
|
|
.is_ok()
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-06 10:32:56 -07:00
|
|
|
enum SessionType {
|
|
|
|
|
X11,
|
|
|
|
|
Wayland,
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 22:08:02 -05:00
|
|
|
let session_dirs = xdg::BaseDirectories::with_prefix("wayland-sessions")
|
2025-09-16 13:08:16 +02:00
|
|
|
.get_data_dirs()
|
2024-02-19 22:08:02 -05:00
|
|
|
.into_iter()
|
|
|
|
|
.map(|dir| (dir, SessionType::Wayland))
|
|
|
|
|
.chain(
|
|
|
|
|
xdg::BaseDirectories::with_prefix("xsessions")
|
2025-09-16 13:08:16 +02:00
|
|
|
.get_data_dirs()
|
2024-02-19 22:08:02 -05:00
|
|
|
.into_iter()
|
|
|
|
|
.map(|dir| (dir, SessionType::X11)),
|
|
|
|
|
);
|
2023-10-05 17:47:23 -06:00
|
|
|
|
|
|
|
|
let sessions = {
|
|
|
|
|
let mut sessions = HashMap::new();
|
2026-01-21 11:16:55 -07:00
|
|
|
let locales = get_languages_from_env();
|
2024-02-06 10:32:56 -07:00
|
|
|
for (session_dir, session_type) in session_dirs {
|
2023-10-05 17:47:23 -06:00
|
|
|
let read_dir = match fs::read_dir(&session_dir) {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to read session directory {:?}: {:?}",
|
|
|
|
|
session_dir,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for dir_entry_res in read_dir {
|
|
|
|
|
let dir_entry = match dir_entry_res {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to read session directory {:?} entry: {:?}",
|
|
|
|
|
session_dir,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-21 11:16:55 -07:00
|
|
|
let entry = match DesktopEntry::from_path(dir_entry.path(), Some(&locales)) {
|
2023-10-05 17:47:23 -06:00
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to read session file {:?}: {:?}",
|
|
|
|
|
dir_entry.path(),
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-21 11:16:55 -07:00
|
|
|
let name = match entry.name(&locales) {
|
2023-10-05 17:47:23 -06:00
|
|
|
Some(some) => some,
|
|
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to read session file {:?}: no Desktop Entry/Name attribute",
|
|
|
|
|
dir_entry.path()
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-21 11:16:55 -07:00
|
|
|
let exec = match entry.exec() {
|
2023-10-05 17:47:23 -06:00
|
|
|
Some(some) => some,
|
|
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to read session file {:?}: no Desktop Entry/Exec attribute",
|
|
|
|
|
dir_entry.path()
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-05 10:43:05 -06:00
|
|
|
let mut command = Vec::new();
|
|
|
|
|
let mut env = Vec::new();
|
|
|
|
|
match session_type {
|
2024-02-06 10:32:56 -07:00
|
|
|
SessionType::X11 => {
|
|
|
|
|
//TODO: xinit may be better, but more complicated to set up
|
2024-06-05 10:43:05 -06:00
|
|
|
command.push("startx".to_string());
|
|
|
|
|
env.push("XDG_SESSION_TYPE=x11".to_string());
|
2024-02-06 10:32:56 -07:00
|
|
|
}
|
|
|
|
|
SessionType::Wayland => {
|
2024-06-05 10:43:05 -06:00
|
|
|
env.push("XDG_SESSION_TYPE=wayland".to_string());
|
2024-02-06 10:32:56 -07:00
|
|
|
}
|
2024-02-06 10:26:53 -07:00
|
|
|
};
|
|
|
|
|
|
2026-01-21 11:16:55 -07:00
|
|
|
if let Some(desktop_names) = entry
|
|
|
|
|
.groups
|
|
|
|
|
.desktop_entry()
|
|
|
|
|
.and_then(|g| g.entry("DesktopNames"))
|
|
|
|
|
{
|
2024-06-05 10:43:05 -06:00
|
|
|
env.push(format!("XDG_CURRENT_DESKTOP={desktop_names}"));
|
2024-06-05 11:05:11 -06:00
|
|
|
if let Some(name) = desktop_names.split(':').next() {
|
|
|
|
|
env.push(format!("XDG_SESSION_DESKTOP={name}"));
|
|
|
|
|
}
|
2024-06-05 10:43:05 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Session exec may contain environmental variables
|
|
|
|
|
command.push("/usr/bin/env".to_string());
|
|
|
|
|
|
|
|
|
|
// To ensure the env is set correctly, we also set it in the session command
|
|
|
|
|
for arg in env.iter() {
|
|
|
|
|
command.push(arg.clone());
|
2024-03-19 21:11:21 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-06 10:26:53 -07:00
|
|
|
match shlex::split(exec) {
|
2024-02-06 10:32:56 -07:00
|
|
|
Some(args) => {
|
|
|
|
|
for arg in args {
|
2024-02-06 10:26:53 -07:00
|
|
|
command.push(arg)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!(
|
2023-10-05 17:47:23 -06:00
|
|
|
"failed to parse session file {:?} Exec field {:?}",
|
|
|
|
|
dir_entry.path(),
|
|
|
|
|
exec
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::info!("session {} using command {:?} env {:?}", name, command, env);
|
2025-10-27 13:05:21 +01:00
|
|
|
if let Some(some) = sessions.insert(name.to_string(), (command, env)) {
|
|
|
|
|
tracing::warn!("session {} overwrote old command {:?}", name, some);
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sessions
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-29 17:51:37 +02:00
|
|
|
let logind_available = cfg!(feature = "logind") && crate::logind::is_available();
|
|
|
|
|
|
2023-10-06 15:02:25 -06:00
|
|
|
let flags = Flags {
|
2025-09-09 16:20:31 +02:00
|
|
|
user_icons: user_datas
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.map(|d| d.icon_opt.take().map(widget::image::Handle::from_bytes))
|
|
|
|
|
.collect(),
|
2024-02-06 15:03:07 -07:00
|
|
|
user_datas,
|
2023-10-06 15:02:25 -06:00
|
|
|
sessions,
|
2024-08-22 08:17:06 -04:00
|
|
|
greeter_config,
|
|
|
|
|
greeter_config_handler,
|
2026-07-29 17:51:37 +02:00
|
|
|
logind_available,
|
2023-10-06 15:02:25 -06:00
|
|
|
};
|
2023-10-05 17:47:23 -06:00
|
|
|
|
2024-02-06 10:58:34 -07:00
|
|
|
let settings = Settings::default().no_main_window(true);
|
2023-10-05 17:47:23 -06:00
|
|
|
|
|
|
|
|
cosmic::app::run::<App>(settings, flags)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Flags {
|
2024-02-06 15:03:07 -07:00
|
|
|
user_datas: Vec<UserData>,
|
2025-09-09 16:20:31 +02:00
|
|
|
user_icons: Vec<Option<widget::image::Handle>>,
|
2024-06-05 10:43:05 -06:00
|
|
|
sessions: HashMap<String, (Vec<String>, Vec<String>)>,
|
2024-08-22 08:17:06 -04:00
|
|
|
greeter_config: CosmicGreeterConfig,
|
|
|
|
|
greeter_config_handler: Option<cosmic_config::Config>,
|
2026-07-29 17:51:37 +02:00
|
|
|
logind_available: bool,
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum SocketState {
|
|
|
|
|
/// Opening GREETD_SOCK
|
|
|
|
|
Pending,
|
|
|
|
|
/// GREETD_SOCK is open
|
2024-07-28 05:12:48 +02:00
|
|
|
Open,
|
2023-10-05 17:47:23 -06:00
|
|
|
/// No GREETD_SOCK variable set
|
|
|
|
|
NotSet,
|
|
|
|
|
/// Failed to open GREETD_SOCK
|
|
|
|
|
Error(Arc<io::Error>),
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-07 10:08:03 -06:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum DialogPage {
|
|
|
|
|
Restart(Instant),
|
|
|
|
|
Shutdown(Instant),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DialogPage {
|
|
|
|
|
fn remaining(instant: Instant) -> Option<Duration> {
|
|
|
|
|
let elapsed = instant.elapsed();
|
|
|
|
|
let timeout = Duration::new(60, 0);
|
|
|
|
|
if elapsed < timeout {
|
|
|
|
|
Some(timeout - elapsed)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-04 22:17:44 -06:00
|
|
|
///TODO: this is custom code that should be better handled by libcosmic
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Dropdown {
|
2025-08-06 21:41:38 -04:00
|
|
|
Accessibility,
|
2024-06-04 22:17:44 -06:00
|
|
|
Keyboard,
|
|
|
|
|
User,
|
|
|
|
|
Session,
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-31 01:34:31 -04:00
|
|
|
struct NameIndexPair {
|
|
|
|
|
/// Selected username
|
|
|
|
|
username: String,
|
|
|
|
|
/// Index of the [`UserData`] for the selected username
|
|
|
|
|
data_idx: Option<usize>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 17:47:23 -06:00
|
|
|
/// Messages that are used specifically by our [`App`].
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum Message {
|
2025-05-09 16:10:03 -06:00
|
|
|
Common(common::Message),
|
|
|
|
|
OutputEvent(OutputEvent, WlOutput),
|
2024-07-28 05:12:48 +02:00
|
|
|
Auth(Option<String>),
|
fix(greeter): do not cancel the session on a non-fatal PAM error message
greetd delivers PAM_ERROR_MSG as an auth message of type Error, and ipc.rs
turned that into Message::Error, which cancels the greetd session. But such a
message describes the current attempt, not the end of the conversation:
pam_fprintd reports "Failed to match fingerprint" this way and then retries
until max-tries is reached.
The consequence at the login screen is that a single failed fingerprint press
ends the conversation. The greeter reconnects and immediately creates a new
session, whose PAM worker calls fprintd's Claim 14 ms later while the previous
worker still holds it, so the claim is denied and no fingerprint prompt ever
appears again. The previous worker only exits when it next tries to talk to the
greeter, which can be arbitrarily far in the future, so delaying the new session
does not help either.
Per the greetd protocol every auth message has to be acknowledged with
PostAuthMessageResponse, and for a non-interactive one the response is None,
which is what the Info arm already does. Route the Error message to its own
Message::AuthError that shows the text and acknowledges it. Message::Error and
the Response::Error path stay as they are, so a failed authentication still
cancels the session.
Drafted with AI assistance (Claude Code); the change and the measurements behind
it were reviewed and tested by me on real hardware.
Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com>
2026-08-09 06:34:15 +02:00
|
|
|
/// Non-fatal error message from PAM (`PAM_ERROR_MSG`), reported by greetd as an auth
|
|
|
|
|
/// message of type `Error`. Unlike [`Message::Error`] the conversation stays alive.
|
|
|
|
|
AuthError(String),
|
2024-08-22 08:17:06 -04:00
|
|
|
ConfigUpdateUser,
|
2024-05-07 10:08:03 -06:00
|
|
|
DialogCancel,
|
|
|
|
|
DialogConfirm,
|
2024-06-04 22:17:44 -06:00
|
|
|
DropdownToggle(Dropdown),
|
2024-07-28 05:12:48 +02:00
|
|
|
Error(String),
|
|
|
|
|
Exit,
|
|
|
|
|
// Sets channel used to communicate with the greetd IPC subscription.
|
|
|
|
|
GreetdChannel(tokio::sync::mpsc::Sender<Request>),
|
2025-08-19 13:22:47 -04:00
|
|
|
/// Refreshes display outputs.
|
|
|
|
|
RandrUpdate {
|
|
|
|
|
/// Available outputs from cosmic-randr.
|
|
|
|
|
randr: Arc<Result<List, cosmic_randr_shell::Error>>,
|
|
|
|
|
},
|
2024-07-28 05:12:48 +02:00
|
|
|
Heartbeat,
|
2024-06-04 22:17:44 -06:00
|
|
|
KeyboardLayout(usize),
|
2024-07-28 05:12:48 +02:00
|
|
|
Login,
|
2024-02-22 20:51:05 -07:00
|
|
|
Reconnect,
|
2025-08-07 19:05:31 -04:00
|
|
|
Reload(cosmic::Theme),
|
2025-08-25 00:25:34 -04:00
|
|
|
RepositionMenu(window::Id, Size),
|
2024-05-07 10:08:03 -06:00
|
|
|
Restart,
|
2024-07-28 05:12:48 +02:00
|
|
|
Session(String),
|
2024-05-07 10:08:03 -06:00
|
|
|
Shutdown,
|
2024-07-28 05:12:48 +02:00
|
|
|
Socket(SocketState),
|
2025-03-05 23:08:56 -05:00
|
|
|
Surface(surface::Action),
|
2024-07-28 05:12:48 +02:00
|
|
|
Suspend,
|
|
|
|
|
Username(String),
|
2025-06-26 15:14:43 -04:00
|
|
|
EnterUser(bool, String),
|
2025-08-06 21:41:38 -04:00
|
|
|
ScreenReader(bool),
|
|
|
|
|
Magnifier(bool),
|
|
|
|
|
HighContrast(bool),
|
|
|
|
|
InvertColors(bool),
|
|
|
|
|
WaylandUpdate(WaylandUpdate),
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
|
|
|
|
|
2025-05-09 16:10:03 -06:00
|
|
|
impl From<common::Message> for Message {
|
|
|
|
|
fn from(message: common::Message) -> Self {
|
|
|
|
|
Self::Common(message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 17:47:23 -06:00
|
|
|
/// The [`App`] stores application-specific state.
|
|
|
|
|
pub struct App {
|
2025-05-09 16:10:03 -06:00
|
|
|
common: Common<Message>,
|
2023-10-05 17:47:23 -06:00
|
|
|
flags: Flags,
|
2024-07-28 05:12:48 +02:00
|
|
|
greetd_sender: Option<tokio::sync::mpsc::Sender<greetd_ipc::Request>>,
|
2023-10-05 17:47:23 -06:00
|
|
|
socket_state: SocketState,
|
2024-06-04 22:17:44 -06:00
|
|
|
usernames: Vec<(String, String)>,
|
2024-08-31 01:34:31 -04:00
|
|
|
selected_username: NameIndexPair,
|
2023-10-05 17:47:23 -06:00
|
|
|
session_names: Vec<String>,
|
|
|
|
|
selected_session: String,
|
2024-05-07 10:08:03 -06:00
|
|
|
dialog_page_opt: Option<DialogPage>,
|
2024-06-04 22:17:44 -06:00
|
|
|
dropdown_opt: Option<Dropdown>,
|
2025-04-10 13:03:53 +02:00
|
|
|
heartbeat_handle: Option<cosmic::iced::task::Handle>,
|
2025-06-26 15:14:43 -04:00
|
|
|
entering_name: bool,
|
2025-08-07 19:05:31 -04:00
|
|
|
theme_builder: cosmic_theme::ThemeBuilder,
|
2025-08-25 00:25:34 -04:00
|
|
|
surface_id_pairs: Vec<(window::Id, window::Id)>,
|
2025-08-06 21:41:38 -04:00
|
|
|
|
2025-08-19 13:22:47 -04:00
|
|
|
randr_list: Option<cosmic_randr_shell::List>,
|
|
|
|
|
|
2025-08-06 21:41:38 -04:00
|
|
|
accessibility: Accessibility,
|
2025-11-12 20:35:10 +01:00
|
|
|
authenticating: bool,
|
2025-08-06 21:41:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct Accessibility {
|
|
|
|
|
pub wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
|
|
|
|
|
pub wayland_protocol_version: Option<u32>,
|
|
|
|
|
|
|
|
|
|
pub state: cosmic_settings_daemon_config::greeter::GreeterAccessibilityState,
|
|
|
|
|
pub helper: Option<cosmic::cosmic_config::Config>,
|
|
|
|
|
|
2025-08-07 19:05:31 -04:00
|
|
|
pub screen_reader: Option<Child>,
|
2025-08-06 21:41:38 -04:00
|
|
|
pub magnifier: bool,
|
|
|
|
|
pub high_contrast: bool,
|
|
|
|
|
pub invert_colors: bool,
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-06 15:03:07 -07:00
|
|
|
impl App {
|
2025-08-19 13:22:47 -04:00
|
|
|
/// Applies a display configuration via `cosmic-randr`.
|
2025-08-20 18:06:41 -04:00
|
|
|
fn exec_randr(&self, user_config: cosmic_randr_shell::List) -> Task<Message> {
|
|
|
|
|
let mut task = tokio::process::Command::new("cosmic-randr");
|
|
|
|
|
task.arg("kdl");
|
|
|
|
|
|
|
|
|
|
cosmic::task::future::<(), ()>(async move {
|
|
|
|
|
task.stdin(Stdio::piped());
|
|
|
|
|
let Ok(mut p) = task.spawn() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
2025-08-19 13:22:47 -04:00
|
|
|
|
2025-08-20 18:06:41 -04:00
|
|
|
let kdl_doc = kdl::KdlDocument::from(user_config).to_string();
|
|
|
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
|
|
|
|
|
|
if let Some(mut stdin) = p.stdin.take() {
|
|
|
|
|
if let Err(err) = stdin.write_all(kdl_doc.as_bytes()).await {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("Failed to write KDL to stdin: {err:?}");
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
2025-08-20 18:06:41 -04:00
|
|
|
if let Err(err) = stdin.flush().await {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("Failed to flush stdin: {err:?}");
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::debug!("executing {task:?}");
|
2025-08-20 18:06:41 -04:00
|
|
|
let status = p.wait().await;
|
|
|
|
|
if let Err(err) = status {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("Randr error: {err:?}");
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
2025-08-20 18:06:41 -04:00
|
|
|
})
|
|
|
|
|
.discard()
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 13:18:15 +02:00
|
|
|
fn menu(&self, id: SurfaceId) -> Element<'_, Message> {
|
2025-05-09 16:10:03 -06:00
|
|
|
let window_width = self
|
|
|
|
|
.common
|
|
|
|
|
.window_size
|
|
|
|
|
.get(&id)
|
|
|
|
|
.map(|s| s.width)
|
|
|
|
|
.unwrap_or(800.);
|
2025-04-29 18:12:56 -04:00
|
|
|
let menu_width = if window_width > 800. {
|
|
|
|
|
800.
|
|
|
|
|
} else {
|
|
|
|
|
window_width
|
|
|
|
|
};
|
2025-02-21 17:05:50 -05:00
|
|
|
let left_element = {
|
2025-04-10 16:39:32 -04:00
|
|
|
let military_time = self
|
|
|
|
|
.selected_username
|
|
|
|
|
.data_idx
|
2025-05-09 14:16:12 -06:00
|
|
|
.and_then(|i| self.flags.user_datas.get(i))
|
|
|
|
|
.map(|user_data| user_data.time_applet_config.military_time)
|
2025-04-10 16:39:32 -04:00
|
|
|
.unwrap_or_default();
|
2025-05-09 16:10:03 -06:00
|
|
|
let date_time_column = self.common.time.date_time_widget(military_time);
|
2024-02-06 15:03:07 -07:00
|
|
|
|
2025-10-27 12:43:49 +01:00
|
|
|
let mut status_row = widget::row::with_capacity(2)
|
|
|
|
|
.padding(16.0)
|
|
|
|
|
.spacing(12.0)
|
|
|
|
|
.align_y(Alignment::Center);
|
2024-02-06 15:03:07 -07:00
|
|
|
|
2025-09-09 16:20:31 +02:00
|
|
|
if let Some(network_icon) = self.common.network_icon_opt.as_ref() {
|
|
|
|
|
status_row = status_row.push(network_icon.clone());
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2024-02-06 15:03:07 -07:00
|
|
|
|
2025-05-09 16:10:03 -06:00
|
|
|
if let Some((power_icon, power_percent)) = &self.common.power_info_opt {
|
2026-03-16 14:02:28 -04:00
|
|
|
status_row = status_row.push(
|
|
|
|
|
iced::widget::row![
|
|
|
|
|
power_icon.clone(),
|
|
|
|
|
widget::text(format!("{:.0}%", power_percent)),
|
|
|
|
|
]
|
|
|
|
|
.align_y(Alignment::Center),
|
|
|
|
|
);
|
2024-02-06 15:03:07 -07:00
|
|
|
}
|
2024-02-06 15:48:57 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
//TODO: move code for custom dropdowns to libcosmic
|
2025-08-06 21:41:38 -04:00
|
|
|
fn menu_checklist<'a>(
|
|
|
|
|
label: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
|
|
|
|
value: bool,
|
|
|
|
|
message: Message,
|
|
|
|
|
) -> Element<'a, Message> {
|
2025-02-21 17:05:50 -05:00
|
|
|
Element::from(
|
|
|
|
|
widget::menu::menu_button(vec![
|
|
|
|
|
if value {
|
|
|
|
|
widget::icon::from_name("object-select-symbolic")
|
|
|
|
|
.size(16)
|
|
|
|
|
.icon()
|
|
|
|
|
.width(Length::Fixed(16.0))
|
|
|
|
|
.into()
|
|
|
|
|
} else {
|
2026-02-24 15:49:22 -05:00
|
|
|
widget::space::horizontal()
|
|
|
|
|
.width(Length::Fixed(17.0))
|
|
|
|
|
.into()
|
2025-02-21 17:05:50 -05:00
|
|
|
},
|
2026-02-24 15:49:22 -05:00
|
|
|
widget::space::horizontal().width(Length::Fixed(8.0)).into(),
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::text(label)
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Left)
|
|
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.on_press(message),
|
|
|
|
|
)
|
2025-08-06 21:41:38 -04:00
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
let dropdown_menu = |items: Vec<_>| {
|
|
|
|
|
let item_cnt = items.len();
|
|
|
|
|
|
2026-07-08 18:27:28 -04:00
|
|
|
let items = widget::menu::menu_column::MenuColumn::with_children(items);
|
2025-06-26 15:14:43 -04:00
|
|
|
let items = if item_cnt > 7 {
|
|
|
|
|
Element::from(
|
|
|
|
|
widget::scrollable(items)
|
|
|
|
|
.height(Length::Fixed(DEFAULT_MENU_ITEM_HEIGHT * 7.)),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
Element::from(items)
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-01 23:17:18 -04:00
|
|
|
let menu = widget::container(items)
|
2025-02-21 17:05:50 -05:00
|
|
|
.padding(1)
|
|
|
|
|
//TODO: move style to libcosmic
|
|
|
|
|
.class(theme::Container::custom(|theme| {
|
|
|
|
|
let cosmic = theme.cosmic();
|
2026-07-01 23:17:18 -04:00
|
|
|
let component = &cosmic.background(theme.transparent).component;
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::container::Style {
|
|
|
|
|
icon_color: Some(component.on.into()),
|
|
|
|
|
text_color: Some(component.on.into()),
|
|
|
|
|
background: Some(Background::Color(component.base.into())),
|
|
|
|
|
border: Border {
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
width: 1.0,
|
|
|
|
|
color: component.divider.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
2024-06-04 22:17:44 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}))
|
2026-07-01 23:17:18 -04:00
|
|
|
.width(Length::Fixed(240.0));
|
|
|
|
|
|
|
|
|
|
if let Some(t) = self.common.rectangle_tracker.as_ref() {
|
|
|
|
|
Element::from(t.container((id, true), menu))
|
|
|
|
|
} else {
|
|
|
|
|
menu.into()
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
};
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let mut input_button = widget::popover(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::DropdownToggle(Dropdown::Keyboard)),
|
|
|
|
|
)
|
|
|
|
|
.position(widget::popover::Position::Bottom);
|
|
|
|
|
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
|
2025-05-09 18:47:52 -06:00
|
|
|
let mut items = Vec::with_capacity(self.common.active_layouts.len());
|
|
|
|
|
for (i, layout) in self.common.active_layouts.iter().enumerate() {
|
2025-02-21 17:05:50 -05:00
|
|
|
items.push(menu_checklist(
|
|
|
|
|
&layout.description,
|
2026-07-24 12:18:43 -07:00
|
|
|
i == self.common.current_keyboard_layout,
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::KeyboardLayout(i),
|
|
|
|
|
));
|
2024-06-04 22:17:44 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
input_button = input_button.popup(dropdown_menu(items));
|
|
|
|
|
}
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let mut user_button = widget::popover(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("system-users-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::DropdownToggle(Dropdown::User)),
|
|
|
|
|
)
|
|
|
|
|
.position(widget::popover::Position::Bottom);
|
|
|
|
|
if matches!(self.dropdown_opt, Some(Dropdown::User)) {
|
|
|
|
|
let mut items = Vec::with_capacity(self.usernames.len());
|
|
|
|
|
for (name, full_name) in self.usernames.iter() {
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
full_name,
|
|
|
|
|
name == &self.selected_username.username,
|
|
|
|
|
Message::Username(name.clone()),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
let item_cnt = items.len();
|
|
|
|
|
let menu_button = widget::menu::menu_button(vec![
|
2026-02-24 15:49:22 -05:00
|
|
|
Element::from(widget::space::horizontal().width(Length::Fixed(10.0))),
|
2025-06-26 15:14:43 -04:00
|
|
|
widget::text(fl!("enter-user"))
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Left)
|
|
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.on_press(Message::EnterUser(true, String::new()))
|
|
|
|
|
.into();
|
|
|
|
|
let items = if item_cnt >= 6 {
|
|
|
|
|
dropdown_menu(vec![
|
|
|
|
|
widget::scrollable(widget::column::with_children(items))
|
|
|
|
|
.height(Length::Fixed(DEFAULT_MENU_ITEM_HEIGHT * 6.))
|
|
|
|
|
.into(),
|
|
|
|
|
widget::divider::horizontal::light().into(),
|
|
|
|
|
menu_button,
|
|
|
|
|
])
|
|
|
|
|
} else {
|
|
|
|
|
items.push(menu_button);
|
|
|
|
|
dropdown_menu(items)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
user_button = user_button.popup(items);
|
2024-06-04 22:17:44 -06:00
|
|
|
}
|
|
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let mut session_button = widget::popover(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("application-menu-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::DropdownToggle(Dropdown::Session)),
|
|
|
|
|
)
|
|
|
|
|
.position(widget::popover::Position::Bottom);
|
|
|
|
|
if matches!(self.dropdown_opt, Some(Dropdown::Session)) {
|
|
|
|
|
let mut items = Vec::with_capacity(self.session_names.len());
|
|
|
|
|
for session_name in self.session_names.iter() {
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
session_name,
|
|
|
|
|
session_name == &self.selected_session,
|
|
|
|
|
Message::Session(session_name.clone()),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
session_button = session_button.popup(dropdown_menu(items));
|
2024-02-06 15:48:57 -07:00
|
|
|
}
|
2024-08-31 01:34:31 -04:00
|
|
|
|
2025-08-06 21:41:38 -04:00
|
|
|
// Accessibility menu as a popup dialog
|
|
|
|
|
let mut accessibility_dropdown = widget::popover(
|
|
|
|
|
widget::button::custom(widget::icon::from_name(
|
|
|
|
|
"applications-accessibility-symbolic",
|
2025-02-21 17:05:50 -05:00
|
|
|
))
|
|
|
|
|
.padding(12.0)
|
2025-08-19 13:22:47 -04:00
|
|
|
.on_press(Message::DropdownToggle(Dropdown::Accessibility)),
|
2025-08-06 21:41:38 -04:00
|
|
|
)
|
|
|
|
|
.position(widget::popover::Position::Bottom);
|
|
|
|
|
|
|
|
|
|
if matches!(self.dropdown_opt, Some(Dropdown::Accessibility)) {
|
|
|
|
|
let mut items = Vec::new();
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
fl!("accessibility", "screen-reader"),
|
2025-08-07 19:05:31 -04:00
|
|
|
self.accessibility.screen_reader.is_some(),
|
2025-10-27 13:05:21 +01:00
|
|
|
Message::ScreenReader(self.accessibility.screen_reader.is_none()),
|
2025-08-06 21:41:38 -04:00
|
|
|
));
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
fl!("accessibility", "magnifier"),
|
|
|
|
|
self.accessibility.magnifier,
|
|
|
|
|
Message::Magnifier(!self.accessibility.magnifier),
|
|
|
|
|
));
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
fl!("accessibility", "high-contrast"),
|
|
|
|
|
self.accessibility.high_contrast,
|
|
|
|
|
Message::HighContrast(!self.accessibility.high_contrast),
|
|
|
|
|
));
|
|
|
|
|
items.push(menu_checklist(
|
|
|
|
|
fl!("accessibility", "invert-colors"),
|
|
|
|
|
self.accessibility.invert_colors,
|
|
|
|
|
Message::InvertColors(!self.accessibility.invert_colors),
|
|
|
|
|
));
|
|
|
|
|
accessibility_dropdown = accessibility_dropdown.popup(dropdown_menu(items));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let accessibility_button = accessibility_dropdown;
|
|
|
|
|
|
2026-07-29 17:51:37 +02:00
|
|
|
let mut button_row = iced::widget::row![
|
2025-08-06 21:41:38 -04:00
|
|
|
widget::tooltip(
|
|
|
|
|
accessibility_button,
|
|
|
|
|
text(fl!("accessibility")),
|
|
|
|
|
widget::tooltip::Position::Top
|
|
|
|
|
),
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::tooltip(
|
|
|
|
|
input_button,
|
|
|
|
|
text(fl!("keyboard-layout")),
|
|
|
|
|
widget::tooltip::Position::Top
|
|
|
|
|
),
|
|
|
|
|
widget::tooltip(
|
|
|
|
|
user_button,
|
|
|
|
|
text(fl!("user")),
|
|
|
|
|
widget::tooltip::Position::Top
|
|
|
|
|
),
|
|
|
|
|
widget::tooltip(
|
|
|
|
|
session_button,
|
|
|
|
|
text(fl!("session")),
|
|
|
|
|
widget::tooltip::Position::Top
|
|
|
|
|
),
|
2026-07-29 17:51:37 +02:00
|
|
|
];
|
|
|
|
|
if self.flags.logind_available {
|
|
|
|
|
button_row = button_row
|
|
|
|
|
.push(widget::tooltip(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("system-suspend-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::Suspend),
|
|
|
|
|
text(fl!("suspend")),
|
|
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
))
|
|
|
|
|
.push(widget::tooltip(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("system-reboot-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::Restart),
|
|
|
|
|
text(fl!("restart")),
|
|
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
))
|
|
|
|
|
.push(widget::tooltip(
|
|
|
|
|
widget::button::custom(widget::icon::from_name("system-shutdown-symbolic"))
|
|
|
|
|
.padding(12.0)
|
|
|
|
|
.on_press(Message::Shutdown),
|
|
|
|
|
text(fl!("shutdown")),
|
|
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let button_row = button_row.padding([16.0, 0.0, 0.0, 0.0]).spacing(8.0);
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::container(iced::widget::column![
|
|
|
|
|
date_time_column,
|
2025-04-29 18:12:56 -04:00
|
|
|
widget::divider::horizontal::default().width(Length::Fixed(menu_width / 2. - 16.)),
|
2025-02-21 17:05:50 -05:00
|
|
|
status_row,
|
2025-04-29 18:12:56 -04:00
|
|
|
widget::divider::horizontal::default().width(Length::Fixed(menu_width / 2. - 16.)),
|
2025-02-21 17:05:50 -05:00
|
|
|
button_row,
|
|
|
|
|
])
|
2025-10-27 12:43:49 +01:00
|
|
|
.align_x(Alignment::Start)
|
2024-03-19 21:11:21 -06:00
|
|
|
};
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let right_element = {
|
2025-10-27 12:43:49 +01:00
|
|
|
let mut column = widget::column::with_capacity(5)
|
2025-02-21 17:05:50 -05:00
|
|
|
.spacing(12.0)
|
|
|
|
|
.max_width(280.0);
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-11-12 20:35:10 +01:00
|
|
|
let military_time = self
|
|
|
|
|
.selected_username
|
|
|
|
|
.data_idx
|
|
|
|
|
.and_then(|i| self.flags.user_datas.get(i))
|
|
|
|
|
.map(|user_data| user_data.time_applet_config.military_time)
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let space_height = match military_time {
|
|
|
|
|
true => 63.0,
|
|
|
|
|
false => 10.0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add top spacing for better visual appearance
|
|
|
|
|
// Bottom of the password text input field should align with bottom of time widget
|
2026-02-24 15:49:22 -05:00
|
|
|
column = column.push(widget::space::vertical().height(Length::Fixed(space_height)));
|
2025-11-12 20:35:10 +01:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
match &self.socket_state {
|
|
|
|
|
SocketState::Pending => {
|
|
|
|
|
column = column.push(widget::text("Opening GREETD_SOCK"));
|
|
|
|
|
}
|
|
|
|
|
SocketState::Open => {
|
2025-09-09 16:20:31 +02:00
|
|
|
for (user_data, user_icon) in self
|
|
|
|
|
.flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.iter()
|
|
|
|
|
.zip(self.flags.user_icons.iter())
|
|
|
|
|
{
|
2025-06-26 15:14:43 -04:00
|
|
|
if !self.entering_name && user_data.name == self.selected_username.username
|
|
|
|
|
{
|
2025-11-12 20:35:10 +01:00
|
|
|
// Display user icon or empty transparent box
|
|
|
|
|
if let Some(icon_handle) = user_icon {
|
2025-10-27 13:05:21 +01:00
|
|
|
column = column.push(
|
|
|
|
|
widget::container(
|
2025-11-12 20:35:10 +01:00
|
|
|
widget::image(icon_handle)
|
2025-10-27 13:05:21 +01:00
|
|
|
.width(Length::Fixed(78.0))
|
2025-11-12 20:35:10 +01:00
|
|
|
.height(Length::Fixed(78.0))
|
|
|
|
|
.content_fit(iced::ContentFit::Fill),
|
2025-02-21 17:05:50 -05:00
|
|
|
)
|
2025-11-12 20:35:10 +01:00
|
|
|
.padding(0.0)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.height(Length::Fixed(78.0))
|
|
|
|
|
.align_x(Alignment::Center),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Empty transparent box for users without icons
|
|
|
|
|
column = column.push(
|
2026-02-24 15:49:22 -05:00
|
|
|
widget::container(
|
|
|
|
|
widget::space::horizontal().width(Length::Fixed(78.0)),
|
|
|
|
|
)
|
2025-11-12 20:35:10 +01:00
|
|
|
.padding(0.0)
|
2025-10-27 13:05:21 +01:00
|
|
|
.width(Length::Fill)
|
2025-11-12 20:35:10 +01:00
|
|
|
.height(Length::Fixed(78.0))
|
2025-10-27 13:05:21 +01:00
|
|
|
.align_x(Alignment::Center),
|
2025-11-12 20:35:10 +01:00
|
|
|
);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2025-05-09 11:27:14 -06:00
|
|
|
column = column.push(
|
2025-05-09 14:16:12 -06:00
|
|
|
widget::container(widget::text::title4(&user_data.full_name))
|
|
|
|
|
.width(Length::Fill)
|
2025-10-27 12:43:49 +01:00
|
|
|
.align_x(Alignment::Center),
|
2025-05-09 11:27:14 -06:00
|
|
|
);
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
if self.entering_name {
|
|
|
|
|
column = column.push(
|
|
|
|
|
widget::text_input(
|
|
|
|
|
fl!("type-username"),
|
|
|
|
|
self.selected_username.username.as_str(),
|
|
|
|
|
)
|
|
|
|
|
.id(USERNAME_ID.clone())
|
|
|
|
|
.on_input(|input| Message::EnterUser(false, input))
|
2025-10-27 13:05:21 +01:00
|
|
|
.on_submit(Message::Username),
|
2025-06-26 15:14:43 -04:00
|
|
|
)
|
|
|
|
|
}
|
2025-10-27 13:05:21 +01:00
|
|
|
if let Some((prompt, secret, value_opt)) = &self.common.prompt_opt {
|
|
|
|
|
match value_opt {
|
2025-02-21 17:05:50 -05:00
|
|
|
Some(value) => {
|
2025-11-12 20:35:10 +01:00
|
|
|
// Only show password input when not authenticating
|
|
|
|
|
if !self.authenticating {
|
|
|
|
|
let text_input_id = self
|
|
|
|
|
.common
|
|
|
|
|
.surface_names
|
|
|
|
|
.get(&id)
|
|
|
|
|
.and_then(|id| self.common.text_input_ids.get(id))
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
|
|
|
|
|
let mut text_input = widget::secure_input(
|
|
|
|
|
prompt.clone(),
|
|
|
|
|
value.as_str(),
|
|
|
|
|
Some(
|
|
|
|
|
common::Message::Prompt(
|
|
|
|
|
prompt.clone(),
|
|
|
|
|
!*secret,
|
|
|
|
|
Some(value.clone()),
|
|
|
|
|
)
|
|
|
|
|
.into(),
|
|
|
|
|
),
|
|
|
|
|
*secret,
|
|
|
|
|
)
|
|
|
|
|
.id(text_input_id)
|
|
|
|
|
.on_input(|input| {
|
2025-05-09 18:47:52 -06:00
|
|
|
common::Message::Prompt(
|
|
|
|
|
prompt.clone(),
|
2025-11-12 20:35:10 +01:00
|
|
|
*secret,
|
|
|
|
|
Some(input),
|
2025-05-09 18:47:52 -06:00
|
|
|
)
|
|
|
|
|
.into()
|
2025-11-12 20:35:10 +01:00
|
|
|
})
|
|
|
|
|
.on_submit(|v| Message::Auth(Some(v)));
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-11-12 20:35:10 +01:00
|
|
|
if let Some(text_input_id) = self
|
|
|
|
|
.common
|
|
|
|
|
.surface_names
|
|
|
|
|
.get(&id)
|
|
|
|
|
.and_then(|id| self.common.text_input_ids.get(id))
|
|
|
|
|
{
|
|
|
|
|
text_input = text_input.id(text_input_id.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if *secret {
|
|
|
|
|
text_input = text_input.password()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
column = column.push(text_input);
|
|
|
|
|
|
|
|
|
|
if self.common.caps_lock {
|
|
|
|
|
column = column.push(widget::text(fl!("caps-lock")));
|
|
|
|
|
} else if self.common.error_opt.is_none() {
|
|
|
|
|
column = column.push(widget::text(""));
|
|
|
|
|
}
|
2025-05-15 14:18:08 -06:00
|
|
|
}
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
|
|
|
|
None => {
|
2025-12-18 17:59:55 -07:00
|
|
|
// `value_opt == None` is used for non-interactive auth messages
|
|
|
|
|
// (e.g. PAM_TEXT_INFO via greetd). This is where fingerprint
|
|
|
|
|
// prompts typically come through, so show the message to the user.
|
|
|
|
|
column = column.push(widget::text(prompt));
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
2025-10-27 13:05:21 +01:00
|
|
|
}
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
SocketState::NotSet => {
|
|
|
|
|
column = column.push(widget::text("GREETD_SOCK variable not set"));
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
SocketState::Error(err) => {
|
|
|
|
|
column = column.push(widget::text(format!(
|
|
|
|
|
"Failed to open GREETD_SOCK: {:?}",
|
|
|
|
|
err
|
|
|
|
|
)))
|
2024-02-06 15:03:07 -07:00
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
|
2025-11-12 20:35:10 +01:00
|
|
|
// Show either authenticating message or error message in the same location
|
|
|
|
|
if self.authenticating {
|
|
|
|
|
column = column.push(
|
|
|
|
|
widget::container(
|
|
|
|
|
widget::row::with_capacity(2)
|
|
|
|
|
.spacing(8.0)
|
|
|
|
|
.align_y(Alignment::Center)
|
2026-06-24 19:12:46 +02:00
|
|
|
.push(widget::indeterminate_circular().size(16.0).bar_height(2.0))
|
2025-11-12 20:35:10 +01:00
|
|
|
.push(widget::text(fl!("authenticating"))),
|
|
|
|
|
)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.align_x(Alignment::Center),
|
|
|
|
|
);
|
|
|
|
|
} else if let Some(error) = &self.common.error_opt {
|
2025-10-27 12:43:49 +01:00
|
|
|
column = column.push(
|
|
|
|
|
widget::text(error)
|
|
|
|
|
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
|
|
|
|
|
);
|
|
|
|
|
if !self.common.caps_lock {
|
|
|
|
|
column = column.push(widget::text(""));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
column = column.push(widget::text(""));
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
|
2026-03-18 17:56:12 -04:00
|
|
|
id_container(
|
|
|
|
|
widget::container(column)
|
|
|
|
|
.align_x(Alignment::Center)
|
|
|
|
|
.width(Length::Fill),
|
|
|
|
|
if self.entering_name {
|
|
|
|
|
iced::id::Id::new("entering_name")
|
|
|
|
|
} else {
|
|
|
|
|
iced::id::Id::new("main_menu")
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-02-21 17:05:50 -05:00
|
|
|
};
|
2026-07-01 23:17:18 -04:00
|
|
|
|
|
|
|
|
let menu = widget::layer_container(
|
|
|
|
|
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
|
|
|
|
|
)
|
|
|
|
|
.layer(cosmic::cosmic_theme::Layer::Background)
|
|
|
|
|
.padding(16)
|
|
|
|
|
.class(cosmic::theme::Container::Custom(Box::new(
|
|
|
|
|
|theme: &cosmic::Theme| {
|
|
|
|
|
// Use background appearance as the base
|
|
|
|
|
let mut appearance =
|
|
|
|
|
widget::container::Catalog::style(theme, &cosmic::theme::Container::Background);
|
|
|
|
|
appearance.background = Some(iced::Background::Color(
|
|
|
|
|
// TODO if we can use popups instead of subsurfaces for the greeter and the lockscreen
|
|
|
|
|
// then we can allow transparency
|
|
|
|
|
theme.cosmic().background(theme.transparent).base.into(),
|
|
|
|
|
));
|
|
|
|
|
appearance.border = iced::Border::default().rounded(16);
|
|
|
|
|
appearance
|
|
|
|
|
},
|
|
|
|
|
)))
|
|
|
|
|
.width(Length::Fixed(800.0));
|
|
|
|
|
let menu = if let Some(t) = self.common.rectangle_tracker.as_ref() {
|
|
|
|
|
Element::from(t.container((id, false), menu))
|
|
|
|
|
} else {
|
|
|
|
|
menu.into()
|
|
|
|
|
};
|
2025-10-30 11:31:48 -06:00
|
|
|
let menu = widget::container(widget::column::with_children(vec![
|
2026-02-24 15:49:22 -05:00
|
|
|
widget::space::vertical()
|
|
|
|
|
.height(Length::FillPortion(1))
|
|
|
|
|
.into(),
|
2026-07-01 23:17:18 -04:00
|
|
|
menu,
|
2026-02-24 15:49:22 -05:00
|
|
|
widget::space::vertical()
|
|
|
|
|
.height(Length::FillPortion(4))
|
|
|
|
|
.into(),
|
2025-10-30 11:31:48 -06:00
|
|
|
]))
|
2025-02-21 17:05:50 -05:00
|
|
|
.width(Length::Fill)
|
2025-10-30 11:31:48 -06:00
|
|
|
.height(Length::Fill)
|
|
|
|
|
.align_x(Alignment::Center);
|
2025-02-21 17:05:50 -05:00
|
|
|
|
2026-07-01 23:17:18 -04:00
|
|
|
// TODO make these opaque?
|
2025-02-21 17:05:50 -05:00
|
|
|
let popover = widget::popover(menu).modal(true);
|
|
|
|
|
match self.dialog_page_opt {
|
|
|
|
|
Some(DialogPage::Restart(instant)) => {
|
|
|
|
|
let remaining = DialogPage::remaining(instant).unwrap_or_default();
|
|
|
|
|
popover
|
|
|
|
|
.popup(
|
|
|
|
|
widget::dialog()
|
|
|
|
|
.title(fl!("restart-now"))
|
|
|
|
|
.icon(widget::icon::from_name("system-reboot-symbolic").size(64))
|
|
|
|
|
.body(fl!("restart-timeout", seconds = remaining.as_secs()))
|
|
|
|
|
.primary_action(
|
|
|
|
|
widget::button::suggested(fl!("restart"))
|
|
|
|
|
.on_press(Message::DialogConfirm),
|
|
|
|
|
)
|
|
|
|
|
.secondary_action(
|
|
|
|
|
widget::button::standard(fl!("cancel"))
|
|
|
|
|
.on_press(Message::DialogCancel),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.into()
|
2024-02-06 10:58:34 -07:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Some(DialogPage::Shutdown(instant)) => {
|
|
|
|
|
let remaining = DialogPage::remaining(instant).unwrap_or_default();
|
|
|
|
|
popover
|
|
|
|
|
.popup(
|
|
|
|
|
widget::dialog()
|
|
|
|
|
.title(fl!("shutdown-now"))
|
|
|
|
|
.icon(widget::icon::from_name("system-shutdown-symbolic").size(64))
|
|
|
|
|
.body(fl!("shutdown-timeout", seconds = remaining.as_secs()))
|
|
|
|
|
.primary_action(
|
|
|
|
|
widget::button::suggested(fl!("shutdown"))
|
|
|
|
|
.on_press(Message::DialogConfirm),
|
|
|
|
|
)
|
|
|
|
|
.secondary_action(
|
|
|
|
|
widget::button::standard(fl!("cancel"))
|
|
|
|
|
.on_press(Message::DialogCancel),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.into()
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
None => popover.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-09 18:47:52 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Send a [`Request`] to the greetd IPC subscription.
|
2025-04-10 13:03:53 +02:00
|
|
|
fn send_request(&self, request: Request) {
|
2025-02-21 17:05:50 -05:00
|
|
|
if let Some(ref sender) = self.greetd_sender {
|
|
|
|
|
let sender = sender.clone();
|
2025-04-10 13:03:53 +02:00
|
|
|
tokio::task::spawn(async move {
|
2025-02-21 17:05:50 -05:00
|
|
|
_ = sender.send(request).await;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 18:47:52 -06:00
|
|
|
fn update_user_data(&mut self) -> Task<Message> {
|
2025-02-21 17:05:50 -05:00
|
|
|
let user_data = match self
|
|
|
|
|
.selected_username
|
|
|
|
|
.data_idx
|
|
|
|
|
.and_then(|i| self.flags.user_datas.get(i))
|
|
|
|
|
{
|
|
|
|
|
Some(some) => some,
|
|
|
|
|
None => {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-27 13:05:21 +01:00
|
|
|
self.common.update_user_data(user_data);
|
2025-02-21 17:05:50 -05:00
|
|
|
|
2025-08-07 19:05:31 -04:00
|
|
|
if let Some(builder) = &user_data.theme_builder_opt {
|
|
|
|
|
self.theme_builder = builder.clone();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 14:40:54 -04:00
|
|
|
let mut tasks = Vec::new();
|
|
|
|
|
self.accessibility.magnifier = user_data.accessibility_zoom.start_on_login;
|
2025-08-19 13:22:47 -04:00
|
|
|
self.randr_list = None;
|
|
|
|
|
tasks.push(cosmic::Task::future(async {
|
|
|
|
|
let randr_fut = cosmic_randr_shell::list().await;
|
|
|
|
|
cosmic::action::app(Message::RandrUpdate {
|
|
|
|
|
randr: Arc::new(randr_fut),
|
|
|
|
|
})
|
|
|
|
|
}));
|
2025-08-11 14:40:54 -04:00
|
|
|
if let Some(theme) = &user_data.theme_opt {
|
|
|
|
|
self.accessibility.high_contrast = theme.is_high_contrast;
|
|
|
|
|
tasks.push(cosmic::command::set_theme(cosmic::Theme::custom(Arc::new(
|
|
|
|
|
theme.clone(),
|
|
|
|
|
))));
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-08-11 14:40:54 -04:00
|
|
|
|
|
|
|
|
Task::batch(tasks)
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
|
|
|
|
impl cosmic::Application for App {
|
|
|
|
|
/// Default async executor to use with the app.
|
|
|
|
|
type Executor = executor::Default;
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Argument received [`cosmic::Application::new`].
|
|
|
|
|
type Flags = Flags;
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Message type specific to our [`App`].
|
|
|
|
|
type Message = Message;
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// The unique application ID to supply to the window manager.
|
|
|
|
|
const APP_ID: &'static str = "com.system76.CosmicGreeter";
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
fn core(&self) -> &Core {
|
2025-05-09 16:10:03 -06:00
|
|
|
&self.common.core
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
fn core_mut(&mut self) -> &mut Core {
|
2025-05-09 16:10:03 -06:00
|
|
|
&mut self.common.core
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Creates the application, and optionally emits command on initialize.
|
2026-07-01 23:17:18 -04:00
|
|
|
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
|
|
|
|
|
core.set_app_type(cosmic::core::AppType::System);
|
2025-08-19 13:22:47 -04:00
|
|
|
let mut tasks = Vec::new();
|
2025-05-09 16:10:03 -06:00
|
|
|
let (mut common, common_task) = Common::init(core);
|
|
|
|
|
common.on_output_event = Some(Box::new(|output_event, output| {
|
|
|
|
|
Message::OutputEvent(output_event, output)
|
|
|
|
|
}));
|
2025-08-19 13:22:47 -04:00
|
|
|
tasks.push(common_task);
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-05-09 14:16:12 -06:00
|
|
|
//TODO: use full_name?
|
2025-02-21 17:05:50 -05:00
|
|
|
let mut usernames: Vec<_> = flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.iter()
|
2025-05-09 14:16:12 -06:00
|
|
|
.map(|x| (x.name.clone(), x.full_name.clone()))
|
2025-02-21 17:05:50 -05:00
|
|
|
.collect();
|
|
|
|
|
usernames.sort_by(|a, b| a.1.cmp(&b.1));
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-06-30 09:36:05 -04:00
|
|
|
let last_user = flags.greeter_config.last_user.as_ref();
|
|
|
|
|
|
|
|
|
|
let (username, uid) = last_user
|
|
|
|
|
.and_then(|last_user| {
|
|
|
|
|
flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|d| d.uid == last_user.get())
|
|
|
|
|
.map(|x| (x.name.clone(), NonZeroU32::new(x.uid)))
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.first()
|
|
|
|
|
.map(|x| (x.name.clone(), NonZeroU32::new(x.uid)))
|
|
|
|
|
})
|
2025-02-21 17:05:50 -05:00
|
|
|
.unwrap_or_default();
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let mut session_names: Vec<_> = flags.sessions.keys().map(|x| x.to_string()).collect();
|
|
|
|
|
session_names.sort();
|
2024-06-04 22:17:44 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let selected_session = uid
|
|
|
|
|
.and_then(|uid| {
|
|
|
|
|
flags
|
|
|
|
|
.greeter_config
|
|
|
|
|
.users
|
|
|
|
|
.get(&uid)
|
|
|
|
|
.and_then(|user| user.last_session.clone())
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| session_names.first().cloned())
|
|
|
|
|
.unwrap_or_default();
|
2026-04-08 12:00:16 -04:00
|
|
|
let data_idx = flags.user_datas.iter().position(|d| d.name == username);
|
2025-02-21 17:05:50 -05:00
|
|
|
let selected_username = NameIndexPair { username, data_idx };
|
2025-10-27 13:05:21 +01:00
|
|
|
let accessibility = Accessibility {
|
|
|
|
|
helper: cosmic_settings_daemon_config::greeter::GreeterAccessibilityState::config()
|
|
|
|
|
.ok(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let app = App {
|
2025-05-09 16:10:03 -06:00
|
|
|
common,
|
2025-02-21 17:05:50 -05:00
|
|
|
flags,
|
|
|
|
|
greetd_sender: None,
|
|
|
|
|
socket_state: SocketState::Pending,
|
|
|
|
|
usernames,
|
|
|
|
|
selected_username,
|
|
|
|
|
session_names,
|
|
|
|
|
selected_session,
|
|
|
|
|
dialog_page_opt: None,
|
|
|
|
|
dropdown_opt: None,
|
2025-04-10 13:03:53 +02:00
|
|
|
heartbeat_handle: None,
|
2025-06-26 15:14:43 -04:00
|
|
|
entering_name: false,
|
2025-08-06 21:41:38 -04:00
|
|
|
accessibility,
|
2025-08-07 19:05:31 -04:00
|
|
|
theme_builder: Default::default(),
|
2025-08-19 13:22:47 -04:00
|
|
|
randr_list: None,
|
2025-08-25 00:25:34 -04:00
|
|
|
surface_id_pairs: Vec::new(),
|
2025-11-12 20:35:10 +01:00
|
|
|
authenticating: false,
|
2023-10-06 15:02:25 -06:00
|
|
|
};
|
2025-08-19 13:22:47 -04:00
|
|
|
(app, Task::batch(tasks))
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Handle application events here.
|
2025-03-05 23:08:56 -05:00
|
|
|
fn update(&mut self, message: Self::Message) -> Task<Message> {
|
2025-02-21 17:05:50 -05:00
|
|
|
match message {
|
2025-05-09 16:10:03 -06:00
|
|
|
Message::Common(common_message) => {
|
2026-09-01 10:04:46 -05:00
|
|
|
if matches!(&common_message, common::Message::Prompt(_, _, Some(_)))
|
|
|
|
|
&& self.authenticating
|
|
|
|
|
{
|
|
|
|
|
self.authenticating = false;
|
|
|
|
|
self.common.prompt_opt = None;
|
|
|
|
|
}
|
2025-12-18 17:59:55 -07:00
|
|
|
// In greetd's IPC protocol, the greeter must acknowledge auth messages by
|
|
|
|
|
// sending PostAuthMessageResponse. For non-interactive "info" messages
|
|
|
|
|
// (fingerprint prompts typically come through here), the correct response
|
|
|
|
|
// is `None`. If we don't ACK, greetd will wait forever and the UI will
|
|
|
|
|
// appear "stuck" on the last info message.
|
|
|
|
|
if let common::Message::Prompt(_, _secret, None) = &common_message {
|
|
|
|
|
self.send_request(Request::PostAuthMessageResponse { response: None });
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 16:10:03 -06:00
|
|
|
return self.common.update(common_message);
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::OutputEvent(output_event, output) => {
|
|
|
|
|
match output_event {
|
|
|
|
|
OutputEvent::Created(output_info_opt) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::info!("output {}: created", output.id());
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let surface_id = SurfaceId::unique();
|
|
|
|
|
let subsurface_id = SurfaceId::unique();
|
2025-10-27 13:05:21 +01:00
|
|
|
self.surface_id_pairs.push((surface_id, subsurface_id));
|
|
|
|
|
|
|
|
|
|
if let Some(old_surface_id) =
|
|
|
|
|
self.common.surface_ids.insert(output.clone(), surface_id)
|
|
|
|
|
{
|
|
|
|
|
//TODO: remove old surface?
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"output {}: already had surface ID {:?}",
|
|
|
|
|
output.id(),
|
|
|
|
|
old_surface_id
|
|
|
|
|
);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
let size = if let Some((w, h)) =
|
|
|
|
|
output_info_opt.as_ref().and_then(|info| info.logical_size)
|
|
|
|
|
{
|
|
|
|
|
Some((Some(w as u32), Some(h as u32)))
|
|
|
|
|
} else {
|
|
|
|
|
Some((None, None))
|
|
|
|
|
};
|
|
|
|
|
match output_info_opt {
|
|
|
|
|
Some(output_info) => match output_info.name {
|
|
|
|
|
Some(output_name) => {
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common
|
|
|
|
|
.surface_names
|
|
|
|
|
.insert(surface_id, output_name.clone());
|
|
|
|
|
self.common
|
|
|
|
|
.surface_names
|
2025-02-21 17:05:50 -05:00
|
|
|
.insert(subsurface_id, output_name.clone());
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.surface_images.remove(&surface_id);
|
2025-02-21 17:05:50 -05:00
|
|
|
let text_input_id =
|
|
|
|
|
widget::Id::new(format!("input-{output_name}",));
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common
|
|
|
|
|
.text_input_ids
|
2025-02-21 17:05:50 -05:00
|
|
|
.insert(output_name.clone(), text_input_id.clone());
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("output {}: no output name", output.id());
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("output {}: no output info", output.id());
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let unwrapped_size = size
|
|
|
|
|
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
|
|
|
|
|
.unwrap_or((1920, 1080));
|
|
|
|
|
let (loc, sub_size) = if unwrapped_size.0 > 800 {
|
|
|
|
|
(
|
|
|
|
|
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
|
|
|
|
|
Size::new(800., unwrapped_size.1 as f32 - 32.),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
2025-03-12 16:07:57 -04:00
|
|
|
(
|
|
|
|
|
Point::new(0., 32.),
|
|
|
|
|
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.),
|
|
|
|
|
)
|
2025-02-21 17:05:50 -05:00
|
|
|
};
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.window_size.insert(
|
2025-02-21 17:05:50 -05:00
|
|
|
surface_id,
|
|
|
|
|
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32),
|
|
|
|
|
);
|
2026-07-01 23:17:18 -04:00
|
|
|
self.common
|
|
|
|
|
.subsurface_rects
|
|
|
|
|
.insert(output.clone(), Rectangle::new(loc, sub_size));
|
|
|
|
|
self.common
|
|
|
|
|
.subsurface_outputs
|
|
|
|
|
.insert(subsurface_id, output.clone());
|
2025-03-05 23:08:56 -05:00
|
|
|
let msg = cosmic::surface::action::subsurface(
|
2025-02-21 17:05:50 -05:00
|
|
|
move |_: &mut App| SctkSubsurfaceSettings {
|
|
|
|
|
parent: surface_id,
|
|
|
|
|
id: subsurface_id,
|
|
|
|
|
loc,
|
|
|
|
|
size: Some(sub_size),
|
|
|
|
|
z: 10,
|
|
|
|
|
steal_keyboard_focus: true,
|
2025-03-05 23:08:56 -05:00
|
|
|
gravity: Gravity::BottomRight,
|
|
|
|
|
offset: (0, 0),
|
|
|
|
|
input_zone: None,
|
2025-02-21 17:05:50 -05:00
|
|
|
},
|
|
|
|
|
Some(Box::new(move |app: &App| {
|
2025-03-05 23:08:56 -05:00
|
|
|
app.menu(subsurface_id).map(cosmic::Action::App)
|
2025-02-21 17:05:50 -05:00
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
return Task::batch([
|
2025-05-09 18:47:52 -06:00
|
|
|
self.update_user_data(),
|
2025-02-21 17:05:50 -05:00
|
|
|
get_layer_surface(SctkLayerSurfaceSettings {
|
|
|
|
|
id: surface_id,
|
|
|
|
|
layer: Layer::Overlay,
|
|
|
|
|
keyboard_interactivity: KeyboardInteractivity::Exclusive,
|
2026-02-24 15:49:22 -05:00
|
|
|
input_zone: None,
|
2025-02-21 17:05:50 -05:00
|
|
|
anchor: Anchor::TOP | Anchor::LEFT | Anchor::BOTTOM | Anchor::RIGHT,
|
|
|
|
|
output: IcedOutput::Output(output),
|
|
|
|
|
namespace: "cosmic-locker".into(),
|
|
|
|
|
size: Some((None, None)),
|
|
|
|
|
margin: IcedMargin {
|
|
|
|
|
top: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
},
|
|
|
|
|
exclusive_zone: -1,
|
|
|
|
|
size_limits: iced::Limits::NONE.min_width(1.0).min_height(1.0),
|
|
|
|
|
}),
|
2025-03-05 23:08:56 -05:00
|
|
|
cosmic::task::message(cosmic::Action::Cosmic(
|
|
|
|
|
cosmic::app::Action::Surface(msg),
|
|
|
|
|
)),
|
2025-02-21 17:05:50 -05:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
OutputEvent::Removed => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::info!("output {}: removed", output.id());
|
2025-05-09 16:10:03 -06:00
|
|
|
match self.common.surface_ids.remove(&output) {
|
2025-02-21 17:05:50 -05:00
|
|
|
Some(surface_id) => {
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.surface_images.remove(&surface_id);
|
|
|
|
|
self.common.window_size.remove(&surface_id);
|
|
|
|
|
if let Some(n) = self.common.surface_names.remove(&surface_id) {
|
|
|
|
|
self.common.text_input_ids.remove(&n);
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
return destroy_layer_surface(surface_id);
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
None => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("output {}: no surface found", output.id());
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
OutputEvent::InfoUpdate(_output_info) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::info!("output {}: info update", output.id());
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Socket(socket_state) => {
|
|
|
|
|
self.socket_state = socket_state;
|
2025-10-27 13:05:21 +01:00
|
|
|
if let SocketState::Open = &self.socket_state {
|
|
|
|
|
// When socket is opened, send create session
|
|
|
|
|
self.send_request(Request::CreateSession {
|
|
|
|
|
username: self.selected_username.username.clone(),
|
|
|
|
|
});
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-07 19:05:31 -04:00
|
|
|
Message::Reload(new) => {
|
2025-08-11 14:40:54 -04:00
|
|
|
return cosmic::command::set_theme(new.clone());
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::Session(selected_session) => {
|
|
|
|
|
self.selected_session = selected_session;
|
|
|
|
|
if self.dropdown_opt == Some(Dropdown::Session) {
|
|
|
|
|
self.dropdown_opt = None;
|
2026-07-01 23:17:18 -04:00
|
|
|
return self.common.dropdown_blur_rects(false);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
Message::EnterUser(focus_input, username) => {
|
2025-06-26 16:30:38 -04:00
|
|
|
if self.dropdown_opt == Some(Dropdown::User) {
|
|
|
|
|
self.dropdown_opt = None;
|
|
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
self.entering_name = true;
|
|
|
|
|
self.selected_username = NameIndexPair {
|
2025-06-30 10:45:03 -04:00
|
|
|
data_idx: self
|
|
|
|
|
.flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|d| d.name == username),
|
2025-06-26 15:14:43 -04:00
|
|
|
username,
|
|
|
|
|
};
|
|
|
|
|
if focus_input {
|
2026-07-01 23:17:18 -04:00
|
|
|
return Task::batch([
|
|
|
|
|
self.common.dropdown_blur_rects(false),
|
|
|
|
|
widget::text_input::focus(USERNAME_ID.clone()),
|
|
|
|
|
]);
|
2025-06-26 15:14:43 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::Username(username) => {
|
|
|
|
|
if self.dropdown_opt == Some(Dropdown::User) {
|
|
|
|
|
self.dropdown_opt = None;
|
|
|
|
|
}
|
2025-06-26 15:14:43 -04:00
|
|
|
if self.entering_name || username != self.selected_username.username {
|
|
|
|
|
self.entering_name = false;
|
2025-11-12 20:35:10 +01:00
|
|
|
self.authenticating = false;
|
2025-06-30 10:45:03 -04:00
|
|
|
let data_idx = self
|
|
|
|
|
.flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|d| d.name == username);
|
2025-02-21 17:05:50 -05:00
|
|
|
self.selected_username = NameIndexPair { username, data_idx };
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.surface_images.clear();
|
2025-02-21 17:05:50 -05:00
|
|
|
if let Some(session) = data_idx.and_then(|i| {
|
|
|
|
|
self.flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.get(i)
|
|
|
|
|
.and_then(|UserData { uid, .. }| {
|
|
|
|
|
NonZeroU32::new(*uid).and_then(|uid| {
|
|
|
|
|
self.flags
|
|
|
|
|
.greeter_config
|
|
|
|
|
.users
|
|
|
|
|
.get(&uid)
|
|
|
|
|
.and_then(|conf| conf.last_session.as_deref())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}) {
|
|
|
|
|
session.clone_into(&mut self.selected_session);
|
|
|
|
|
};
|
2025-10-27 13:05:21 +01:00
|
|
|
if let SocketState::Open = &self.socket_state {
|
|
|
|
|
self.common.prompt_opt = None;
|
|
|
|
|
self.send_request(Request::CancelSession);
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-08-25 00:25:34 -04:00
|
|
|
if let Some(randr_list) = self.randr_list.as_ref() {
|
2026-07-01 23:17:18 -04:00
|
|
|
return Task::batch([
|
|
|
|
|
self.common.dropdown_blur_rects(false),
|
|
|
|
|
self.update(Message::RandrUpdate {
|
|
|
|
|
randr: Arc::new(Ok(randr_list.clone())),
|
|
|
|
|
}),
|
|
|
|
|
]);
|
2025-08-25 00:25:34 -04:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ConfigUpdateUser => {
|
|
|
|
|
let Some(user_entry) = self.selected_username.data_idx.and_then(|i| {
|
|
|
|
|
self.flags
|
|
|
|
|
.user_datas
|
|
|
|
|
.get(i)
|
|
|
|
|
.and_then(|UserData { uid, .. }| {
|
|
|
|
|
NonZeroU32::new(*uid)
|
|
|
|
|
.map(|uid| self.flags.greeter_config.users.entry(uid))
|
|
|
|
|
})
|
|
|
|
|
}) else {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!(
|
2025-06-30 10:45:03 -04:00
|
|
|
"Couldn't find user: {:?} {:?}",
|
|
|
|
|
self.selected_username.username,
|
|
|
|
|
self.selected_username.data_idx,
|
|
|
|
|
);
|
2025-02-21 17:05:50 -05:00
|
|
|
return Task::none();
|
|
|
|
|
};
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let Some(handler) = self.flags.greeter_config_handler.as_mut() else {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!(
|
2025-02-21 17:05:50 -05:00
|
|
|
"Failed to update config for {} (UID: {}): no config handler",
|
|
|
|
|
self.selected_username.username,
|
|
|
|
|
user_entry.key()
|
|
|
|
|
);
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
2024-02-06 10:58:34 -07:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
let uid = *user_entry.key();
|
2025-06-30 09:36:05 -04:00
|
|
|
self.flags.greeter_config.last_user = Some(uid);
|
2025-10-27 13:05:21 +01:00
|
|
|
if let Err(err) = handler.set("last_user", self.flags.greeter_config.last_user) {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!(
|
2025-06-30 09:36:05 -04:00
|
|
|
"Failed to set {:?} as last user: {:?}",
|
|
|
|
|
self.flags.greeter_config.last_user,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
match user_entry {
|
|
|
|
|
hash_map::Entry::Vacant(entry) => {
|
|
|
|
|
let last_session = Some(self.selected_session.clone());
|
|
|
|
|
entry.insert(cosmic_greeter_config::user::UserState { uid, last_session });
|
|
|
|
|
}
|
|
|
|
|
hash_map::Entry::Occupied(mut entry) => {
|
|
|
|
|
let last_session = entry.get_mut().last_session.as_mut();
|
|
|
|
|
if last_session
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|session| session.as_str() == self.selected_session)
|
|
|
|
|
{
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
if let Some(session) = last_session {
|
|
|
|
|
self.selected_session.clone_into(session);
|
|
|
|
|
} else {
|
|
|
|
|
let last_session = Some(self.selected_session.clone());
|
|
|
|
|
entry.insert(cosmic_greeter_config::user::UserState {
|
|
|
|
|
uid,
|
|
|
|
|
last_session,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-06 15:02:25 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
// xxx Not sure why this doesn't work unless the handler is used directly
|
|
|
|
|
// if let Err(err) = self
|
|
|
|
|
// .flags
|
|
|
|
|
// .greeter_config
|
|
|
|
|
// .set_users(&handler, self.flags.greeter_config.users.clone())
|
|
|
|
|
if let Err(err) = handler.set("users", &self.flags.greeter_config.users) {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!(
|
2025-02-21 17:05:50 -05:00
|
|
|
"Failed to set {} as last selected session for {} (UID: {}): {:?}",
|
|
|
|
|
self.selected_session,
|
|
|
|
|
self.selected_username.username,
|
|
|
|
|
uid,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Auth(response) => {
|
2026-08-17 11:53:41 +02:00
|
|
|
if response.as_deref() == Some("") {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.error_opt = None;
|
2025-11-12 20:35:10 +01:00
|
|
|
self.authenticating = true;
|
2025-04-10 13:03:53 +02:00
|
|
|
self.send_request(Request::PostAuthMessageResponse { response });
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
fix(greeter): do not cancel the session on a non-fatal PAM error message
greetd delivers PAM_ERROR_MSG as an auth message of type Error, and ipc.rs
turned that into Message::Error, which cancels the greetd session. But such a
message describes the current attempt, not the end of the conversation:
pam_fprintd reports "Failed to match fingerprint" this way and then retries
until max-tries is reached.
The consequence at the login screen is that a single failed fingerprint press
ends the conversation. The greeter reconnects and immediately creates a new
session, whose PAM worker calls fprintd's Claim 14 ms later while the previous
worker still holds it, so the claim is denied and no fingerprint prompt ever
appears again. The previous worker only exits when it next tries to talk to the
greeter, which can be arbitrarily far in the future, so delaying the new session
does not help either.
Per the greetd protocol every auth message has to be acknowledged with
PostAuthMessageResponse, and for a non-interactive one the response is None,
which is what the Info arm already does. Route the Error message to its own
Message::AuthError that shows the text and acknowledges it. Message::Error and
the Response::Error path stay as they are, so a failed authentication still
cancels the session.
Drafted with AI assistance (Claude Code); the change and the measurements behind
it were reviewed and tested by me on real hardware.
Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com>
2026-08-09 06:34:15 +02:00
|
|
|
Message::AuthError(error) => {
|
|
|
|
|
// The conversation continues, so acknowledge like any other
|
|
|
|
|
// non-interactive auth message rather than cancelling the session.
|
|
|
|
|
self.common.error_opt = Some(error);
|
|
|
|
|
self.authenticating = false;
|
|
|
|
|
self.send_request(Request::PostAuthMessageResponse { response: None });
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::Login => {
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.prompt_opt = None;
|
|
|
|
|
self.common.error_opt = None;
|
2025-11-12 20:35:10 +01:00
|
|
|
self.authenticating = false;
|
|
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
match self.flags.sessions.get(&self.selected_session).cloned() {
|
|
|
|
|
Some((cmd, env)) => {
|
2025-04-10 13:03:53 +02:00
|
|
|
self.send_request(Request::StartSession { cmd, env });
|
|
|
|
|
return self.update(Message::ConfigUpdateUser);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
None => todo!("session {:?} not found", self.selected_session),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Error(error) => {
|
2025-05-09 16:10:03 -06:00
|
|
|
self.common.error_opt = Some(error);
|
2025-11-12 20:35:10 +01:00
|
|
|
self.authenticating = false;
|
|
|
|
|
|
2025-04-10 13:03:53 +02:00
|
|
|
self.send_request(Request::CancelSession);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::Reconnect => {
|
2025-05-09 18:47:52 -06:00
|
|
|
return self.update_user_data();
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::DialogCancel => {
|
|
|
|
|
self.dialog_page_opt = None;
|
2025-04-10 13:03:53 +02:00
|
|
|
if let Some(handle) = self.heartbeat_handle.take() {
|
|
|
|
|
handle.abort();
|
|
|
|
|
}
|
2026-07-08 18:42:18 -04:00
|
|
|
if self.dropdown_opt.is_some() {
|
|
|
|
|
return self.common.dropdown_blur_rects(true);
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::DialogConfirm => match self.dialog_page_opt.take() {
|
|
|
|
|
Some(DialogPage::Restart(_)) => {
|
|
|
|
|
#[cfg(feature = "logind")]
|
2025-04-10 13:03:53 +02:00
|
|
|
return cosmic::task::future::<(), ()>(async move {
|
2025-02-21 17:05:50 -05:00
|
|
|
match crate::logind::reboot().await {
|
|
|
|
|
Ok(()) => (),
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("failed to reboot: {:?}", err);
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2025-04-10 13:03:53 +02:00
|
|
|
})
|
|
|
|
|
.discard();
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Some(DialogPage::Shutdown(_)) => {
|
|
|
|
|
#[cfg(feature = "logind")]
|
2025-04-10 13:03:53 +02:00
|
|
|
return cosmic::task::future::<(), ()>(async move {
|
2025-02-21 17:05:50 -05:00
|
|
|
match crate::logind::power_off().await {
|
|
|
|
|
Ok(()) => (),
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("failed to power off: {:?}", err);
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2025-04-10 13:03:53 +02:00
|
|
|
})
|
|
|
|
|
.discard();
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2026-07-08 18:42:18 -04:00
|
|
|
None => {
|
|
|
|
|
if self.dropdown_opt.is_some() {
|
|
|
|
|
return self.common.dropdown_blur_rects(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
},
|
|
|
|
|
Message::DropdownToggle(dropdown) => {
|
|
|
|
|
if self.dropdown_opt == Some(dropdown) {
|
|
|
|
|
self.dropdown_opt = None;
|
2026-07-01 23:17:18 -04:00
|
|
|
return self.common.dropdown_blur_rects(false);
|
2025-02-21 17:05:50 -05:00
|
|
|
} else {
|
|
|
|
|
self.dropdown_opt = Some(dropdown);
|
2026-07-01 23:17:18 -04:00
|
|
|
return self.common.dropdown_blur_rects(true);
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::KeyboardLayout(layout_i) => {
|
2026-07-24 12:18:43 -07:00
|
|
|
if let Some(keyboard_layout) = &self.common.keyboard_layout {
|
|
|
|
|
keyboard_layout.set_group(layout_i as u32);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2026-07-24 12:18:43 -07:00
|
|
|
self.common.current_keyboard_layout = layout_i;
|
2025-02-21 17:05:50 -05:00
|
|
|
if self.dropdown_opt == Some(Dropdown::Keyboard) {
|
|
|
|
|
self.dropdown_opt = None
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
Message::Suspend => {
|
|
|
|
|
#[cfg(feature = "logind")]
|
2025-04-10 13:03:53 +02:00
|
|
|
return cosmic::task::future::<(), ()>(async move {
|
2025-02-21 17:05:50 -05:00
|
|
|
match crate::logind::suspend().await {
|
|
|
|
|
Ok(()) => (),
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("failed to suspend: {:?}", err);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-10 13:03:53 +02:00
|
|
|
})
|
|
|
|
|
.discard();
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2025-04-10 13:03:53 +02:00
|
|
|
Message::Restart | Message::Shutdown => {
|
|
|
|
|
let instant = Instant::now();
|
|
|
|
|
|
|
|
|
|
self.dialog_page_opt = Some(if matches!(message, Message::Restart) {
|
|
|
|
|
DialogPage::Restart(instant)
|
|
|
|
|
} else {
|
|
|
|
|
DialogPage::Shutdown(instant)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if self.heartbeat_handle.is_none() {
|
2026-04-08 12:00:16 -04:00
|
|
|
let (heartbeat, handle) = cosmic::task::stream(cosmic::iced::stream::channel(
|
|
|
|
|
1,
|
|
|
|
|
|mut msg_tx: iced::futures::channel::mpsc::Sender<_>| async move {
|
|
|
|
|
let mut interval = time::interval(Duration::from_secs(1));
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
// Send heartbeat once a second to update time
|
|
|
|
|
msg_tx
|
|
|
|
|
.send(cosmic::Action::App(Message::Heartbeat))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
interval.tick().await;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
.abortable();
|
2025-04-10 13:03:53 +02:00
|
|
|
|
|
|
|
|
self.heartbeat_handle = Some(handle);
|
2026-07-08 18:42:18 -04:00
|
|
|
self.common.include_menu = false;
|
|
|
|
|
return Task::batch(vec![self.common.dropdown_blur_rects(false), heartbeat]);
|
2025-04-10 13:03:53 +02:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::Heartbeat => match self.dialog_page_opt {
|
|
|
|
|
Some(DialogPage::Restart(instant)) | Some(DialogPage::Shutdown(instant)) => {
|
|
|
|
|
if DialogPage::remaining(instant).is_none() {
|
|
|
|
|
return self.update(Message::DialogConfirm);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
},
|
|
|
|
|
Message::Exit => {
|
|
|
|
|
let mut commands = Vec::new();
|
2025-05-09 16:10:03 -06:00
|
|
|
for (_output, surface_id) in self.common.surface_ids.drain() {
|
|
|
|
|
self.common.surface_images.remove(&surface_id);
|
|
|
|
|
self.common.surface_names.remove(&surface_id);
|
|
|
|
|
if let Some(n) = self.common.surface_names.remove(&surface_id) {
|
|
|
|
|
self.common.text_input_ids.remove(&n);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
commands.push(destroy_layer_surface(surface_id));
|
|
|
|
|
}
|
|
|
|
|
commands.push(Task::perform(async { process::exit(0) }, |x| x));
|
|
|
|
|
return Task::batch(commands);
|
|
|
|
|
}
|
|
|
|
|
Message::GreetdChannel(sender) => {
|
|
|
|
|
self.greetd_sender = Some(sender);
|
|
|
|
|
}
|
2025-03-05 23:08:56 -05:00
|
|
|
Message::Surface(a) => {
|
|
|
|
|
return cosmic::task::message(cosmic::Action::Cosmic(
|
|
|
|
|
cosmic::app::Action::Surface(a),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-08-06 21:41:38 -04:00
|
|
|
Message::ScreenReader(enabled) => {
|
2025-08-07 19:05:31 -04:00
|
|
|
if enabled
|
|
|
|
|
&& self
|
|
|
|
|
.accessibility
|
|
|
|
|
.screen_reader
|
|
|
|
|
.as_mut()
|
|
|
|
|
.is_none_or(|c| c.try_wait().is_ok())
|
|
|
|
|
{
|
|
|
|
|
self.accessibility.screen_reader =
|
|
|
|
|
tokio::process::Command::new("/usr/bin/orca").spawn().ok();
|
2025-10-27 13:05:21 +01:00
|
|
|
} else if let Some(mut c) = self.accessibility.screen_reader.take() {
|
|
|
|
|
return cosmic::task::future::<(), ()>(async move {
|
|
|
|
|
if let Err(err) = c.kill().await {
|
|
|
|
|
tracing::error!("Failed to stop screen reader: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.discard();
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
|
|
|
|
_ = self
|
|
|
|
|
.accessibility
|
|
|
|
|
.state
|
2025-10-27 13:05:21 +01:00
|
|
|
.set_screen_reader(helper, Some(enabled));
|
2025-08-06 21:41:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Magnifier(enabled) => {
|
|
|
|
|
if let Some(tx) = &self.accessibility.wayland_sender {
|
|
|
|
|
self.accessibility.magnifier = enabled;
|
|
|
|
|
let _ = tx.send(AccessibilityRequest::Magnifier(enabled));
|
2025-08-07 19:05:31 -04:00
|
|
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
|
|
|
|
_ = self
|
|
|
|
|
.accessibility
|
|
|
|
|
.state
|
2025-10-27 13:05:21 +01:00
|
|
|
.set_magnifier(helper, Some(enabled));
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
2025-08-06 21:41:38 -04:00
|
|
|
} else {
|
|
|
|
|
self.accessibility.magnifier = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::HighContrast(enabled) => {
|
|
|
|
|
self.accessibility.high_contrast = enabled;
|
2025-08-07 19:05:31 -04:00
|
|
|
|
|
|
|
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
|
|
|
|
_ = self
|
|
|
|
|
.accessibility
|
|
|
|
|
.state
|
2025-10-27 13:05:21 +01:00
|
|
|
.set_high_contrast(helper, Some(enabled));
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
|
|
|
|
let builder = self.theme_builder.clone();
|
|
|
|
|
|
|
|
|
|
return cosmic::task::future::<_, _>(async move {
|
|
|
|
|
let builder = builder.clone();
|
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
2025-08-11 14:40:54 -04:00
|
|
|
std::thread::spawn(move || match apply_hc_theme(builder, enabled) {
|
|
|
|
|
Ok(t) => {
|
2025-08-07 19:05:31 -04:00
|
|
|
_ = tx.send(Some(t));
|
2025-08-11 14:40:54 -04:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("{err:?}");
|
2025-08-11 14:40:54 -04:00
|
|
|
_ = tx.send(None);
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if let Ok(Some(theme)) = rx.await {
|
2025-08-11 14:40:54 -04:00
|
|
|
cosmic::Action::App(Message::Reload(cosmic::Theme::custom(
|
|
|
|
|
std::sync::Arc::new(theme),
|
|
|
|
|
)))
|
2025-08-07 19:05:31 -04:00
|
|
|
} else {
|
|
|
|
|
cosmic::Action::None
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-06 21:41:38 -04:00
|
|
|
}
|
|
|
|
|
Message::InvertColors(enabled) => {
|
|
|
|
|
if let Some(tx) = &self.accessibility.wayland_sender {
|
|
|
|
|
self.accessibility.invert_colors = enabled;
|
|
|
|
|
let _ = tx.send(AccessibilityRequest::ScreenFilter {
|
|
|
|
|
inverted: enabled,
|
|
|
|
|
filter: None,
|
|
|
|
|
});
|
2025-08-07 19:05:31 -04:00
|
|
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
|
|
|
|
_ = self
|
|
|
|
|
.accessibility
|
|
|
|
|
.state
|
2025-10-27 13:05:21 +01:00
|
|
|
.set_invert_colors(helper, Some(enabled));
|
2025-08-07 19:05:31 -04:00
|
|
|
}
|
2025-08-06 21:41:38 -04:00
|
|
|
} else {
|
|
|
|
|
self.accessibility.invert_colors = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::WaylandUpdate(update) => match update {
|
|
|
|
|
WaylandUpdate::Errored => {
|
|
|
|
|
let _ = self.accessibility.wayland_sender.take();
|
|
|
|
|
self.accessibility.wayland_protocol_version = None;
|
|
|
|
|
self.accessibility.magnifier = false;
|
|
|
|
|
self.accessibility.invert_colors = false;
|
|
|
|
|
}
|
|
|
|
|
WaylandUpdate::State(AccessibilityEvent::Bound(ver)) => {
|
|
|
|
|
self.accessibility.wayland_protocol_version = Some(ver);
|
|
|
|
|
}
|
|
|
|
|
WaylandUpdate::State(AccessibilityEvent::Magnifier(enabled)) => {
|
|
|
|
|
self.accessibility.magnifier = enabled;
|
|
|
|
|
}
|
|
|
|
|
WaylandUpdate::State(AccessibilityEvent::ScreenFilter { inverted, .. }) => {
|
|
|
|
|
self.accessibility.invert_colors = inverted;
|
|
|
|
|
}
|
|
|
|
|
WaylandUpdate::State(AccessibilityEvent::Closed) => {
|
|
|
|
|
self.accessibility.wayland_sender = None;
|
|
|
|
|
self.accessibility.wayland_protocol_version = None;
|
|
|
|
|
}
|
|
|
|
|
WaylandUpdate::Started(tx) => {
|
2025-08-11 14:40:54 -04:00
|
|
|
let _ = tx.send(AccessibilityRequest::ScreenFilter {
|
|
|
|
|
inverted: self.accessibility.invert_colors,
|
|
|
|
|
filter: None,
|
|
|
|
|
});
|
|
|
|
|
let _ = tx.send(AccessibilityRequest::Magnifier(
|
|
|
|
|
self.accessibility.magnifier,
|
|
|
|
|
));
|
|
|
|
|
|
2025-08-06 21:41:38 -04:00
|
|
|
self.accessibility.wayland_sender = Some(tx);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-08-19 13:22:47 -04:00
|
|
|
Message::RandrUpdate { randr } => match randr.as_ref() {
|
|
|
|
|
Ok(outputs) => {
|
|
|
|
|
let mut tasks = Vec::new();
|
|
|
|
|
self.randr_list = Some(outputs.clone());
|
|
|
|
|
|
2025-08-20 18:06:41 -04:00
|
|
|
let mut list: Option<List> = None;
|
2025-08-19 13:22:47 -04:00
|
|
|
|
|
|
|
|
let Some(cur_user_output_state) = self
|
|
|
|
|
.selected_username
|
|
|
|
|
.data_idx
|
|
|
|
|
.and_then(|i| self.flags.user_datas.get(i))
|
2025-08-20 18:06:41 -04:00
|
|
|
.map(|user_data| &user_data.kdl_output_lists)
|
2025-08-19 13:22:47 -04:00
|
|
|
else {
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
2025-08-20 18:06:41 -04:00
|
|
|
'outer: for configured_list in cur_user_output_state
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|s| match KdlDocument::parse(s) {
|
|
|
|
|
Ok(doc) => Some(doc),
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("Invalid output KDL {err:?}");
|
2025-08-20 18:06:41 -04:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.map(|kdl| match List::try_from(kdl) {
|
|
|
|
|
Ok(list) => list,
|
|
|
|
|
Err(KdlParseWithError { list, errors }) => {
|
|
|
|
|
for err in errors {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("KDL output error: {err:?}");
|
2025-08-20 18:06:41 -04:00
|
|
|
}
|
|
|
|
|
list
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-08-19 13:22:47 -04:00
|
|
|
{
|
2025-08-20 18:06:41 -04:00
|
|
|
if configured_list.outputs.len() != outputs.outputs.len() {
|
2025-08-19 13:22:47 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for o in outputs.outputs.values() {
|
2025-08-20 18:06:41 -04:00
|
|
|
if configured_list.outputs.values().all(|configured| {
|
|
|
|
|
configured.name != o.name
|
|
|
|
|
|| configured.make != o.make
|
|
|
|
|
|| configured.model != o.model
|
2025-08-19 13:22:47 -04:00
|
|
|
}) {
|
|
|
|
|
continue 'outer;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-20 18:06:41 -04:00
|
|
|
if list
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_none_or(|old| old.outputs.len() < configured_list.outputs.len())
|
|
|
|
|
{
|
|
|
|
|
list = Some(configured_list);
|
|
|
|
|
}
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
2025-08-20 18:06:41 -04:00
|
|
|
if let Some(list) = list {
|
|
|
|
|
tasks.push(self.exec_randr(list))
|
|
|
|
|
} else {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::warn!("Failed to apply user display config");
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task::batch(tasks);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("Randr error: {err}");
|
2025-08-19 13:22:47 -04:00
|
|
|
}
|
|
|
|
|
},
|
2025-08-25 00:25:34 -04:00
|
|
|
Message::RepositionMenu(id, size) => {
|
|
|
|
|
let Some(subsurface_id) = self
|
|
|
|
|
.surface_id_pairs
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|(p, s)| (*p == id).then_some(s))
|
|
|
|
|
else {
|
2025-09-12 17:39:37 -04:00
|
|
|
tracing::error!("Failed to find subsurface menu id");
|
2025-08-25 00:25:34 -04:00
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
let loc = if size.width > 800. {
|
|
|
|
|
Point::new(size.width / 2. - 400., 32.)
|
|
|
|
|
} else {
|
|
|
|
|
Point::new(0., 32.)
|
|
|
|
|
};
|
|
|
|
|
return reposition_subsurface(*subsurface_id, loc.x as i32, loc.y as i32);
|
|
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
// Not used for layer surface window
|
2026-04-27 13:18:15 +02:00
|
|
|
fn view(&self) -> Element<'_, Self::Message> {
|
2025-02-21 17:05:50 -05:00
|
|
|
unimplemented!()
|
|
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Creates a view after each update.
|
2026-04-27 13:18:15 +02:00
|
|
|
fn view_window(&self, surface_id: SurfaceId) -> Element<'_, Self::Message> {
|
2025-02-21 17:05:50 -05:00
|
|
|
let img = self
|
2025-05-09 16:10:03 -06:00
|
|
|
.common
|
2025-02-21 17:05:50 -05:00
|
|
|
.surface_images
|
|
|
|
|
.get(&surface_id)
|
2025-05-09 18:47:52 -06:00
|
|
|
.unwrap_or(&self.common.fallback_background);
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::image(img)
|
|
|
|
|
.content_fit(iced::ContentFit::Cover)
|
2023-10-06 15:02:25 -06:00
|
|
|
.width(Length::Fill)
|
|
|
|
|
.height(Length::Fill)
|
2025-02-21 17:05:50 -05:00
|
|
|
.into()
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2023-11-21 15:48:22 -07:00
|
|
|
|
2024-02-06 10:58:34 -07:00
|
|
|
fn subscription(&self) -> Subscription<Self::Message> {
|
2025-05-09 16:10:03 -06:00
|
|
|
Subscription::batch([
|
|
|
|
|
self.common.subscription().map(Message::from),
|
2024-07-28 05:12:48 +02:00
|
|
|
ipc::subscription(),
|
2025-08-06 21:41:38 -04:00
|
|
|
wayland::a11y_subscription().map(Message::WaylandUpdate),
|
2025-08-25 00:25:34 -04:00
|
|
|
listen_with(|event, _status, id| match event {
|
|
|
|
|
iced::Event::Window(window::Event::Resized(size))
|
|
|
|
|
| iced::Event::Window(window::Event::Opened { size, .. }) => {
|
|
|
|
|
Some(Message::RepositionMenu(id, size))
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
}),
|
2025-05-09 16:10:03 -06:00
|
|
|
])
|
2023-11-21 15:48:22 -07:00
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
}
|
2025-08-07 19:05:31 -04:00
|
|
|
|
2025-08-11 14:40:54 -04:00
|
|
|
pub fn apply_hc_theme(
|
|
|
|
|
builder: cosmic_theme::ThemeBuilder,
|
|
|
|
|
enabled: bool,
|
|
|
|
|
) -> Result<cosmic_theme::Theme, cosmic_config::Error> {
|
2025-08-07 19:05:31 -04:00
|
|
|
let is_dark = builder.palette.is_dark();
|
|
|
|
|
let mut builder = builder.clone();
|
|
|
|
|
|
|
|
|
|
builder.palette = if is_dark {
|
|
|
|
|
if enabled {
|
|
|
|
|
CosmicPalette::HighContrastDark(builder.palette.inner())
|
|
|
|
|
} else {
|
|
|
|
|
CosmicPalette::Dark(builder.palette.inner())
|
|
|
|
|
}
|
|
|
|
|
} else if enabled {
|
|
|
|
|
CosmicPalette::HighContrastLight(builder.palette.inner())
|
|
|
|
|
} else {
|
|
|
|
|
CosmicPalette::Light(builder.palette.inner())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let new_theme = builder.build();
|
|
|
|
|
|
|
|
|
|
Ok(new_theme)
|
|
|
|
|
}
|