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;
|
2025-08-25 00:25:34 -04:00
|
|
|
use cosmic::iced::event::listen_with;
|
2026-04-08 12:00:16 -04:00
|
|
|
use cosmic::iced::runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
|
2025-08-25 00:25:34 -04:00
|
|
|
use cosmic::iced::{Point, Size, window};
|
2026-03-18 17:56:12 -04:00
|
|
|
use cosmic::widget::{id_container, text};
|
2023-10-06 15:02:25 -06:00
|
|
|
use cosmic::{
|
2025-04-10 13:03:53 +02:00
|
|
|
Element,
|
2025-05-09 18:47:52 -06:00
|
|
|
cosmic_config::{self, ConfigSet},
|
2023-10-06 15:02:25 -06:00
|
|
|
executor,
|
2026-04-08 12:00:16 -04:00
|
|
|
iced::runtime::core::window::Id as SurfaceId,
|
2024-02-06 10:58:34 -07:00
|
|
|
iced::{
|
2025-10-27 12:43:49 +01:00
|
|
|
self, Alignment, Background, Border, Length, Subscription,
|
2025-05-09 16:10:03 -06:00
|
|
|
event::wayland::OutputEvent,
|
2025-08-20 18:06:41 -04:00
|
|
|
futures::SinkExt,
|
2025-02-21 17:05:50 -05:00
|
|
|
platform_specific::{
|
|
|
|
|
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
|
|
|
|
shell::wayland::commands::layer_surface::{
|
2025-04-10 13:03:53 +02:00
|
|
|
Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface,
|
2024-02-06 10:58:34 -07:00
|
|
|
},
|
2025-08-25 00:25:34 -04:00
|
|
|
shell::wayland::commands::subsurface::reposition_subsurface,
|
2024-02-06 10:58:34 -07:00
|
|
|
},
|
|
|
|
|
},
|
2025-04-10 16:39:32 -04:00
|
|
|
theme, widget,
|
2023-10-06 15:02:25 -06:00
|
|
|
};
|
2025-08-11 14:40:54 -04:00
|
|
|
use cosmic::{
|
|
|
|
|
cosmic_theme::{self, CosmicPalette},
|
2026-01-21 11:16:55 -07:00
|
|
|
desktop::fde::{DesktopEntry, get_languages_from_env},
|
2025-08-11 14:40:54 -04:00
|
|
|
surface,
|
|
|
|
|
};
|
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;
|
|
|
|
|
use std::process::Stdio;
|
2025-06-26 15:14:43 -04:00
|
|
|
use std::sync::LazyLock;
|
2024-02-19 22:08:02 -05:00
|
|
|
use std::{
|
2025-04-10 13:03:53 +02:00
|
|
|
collections::{HashMap, hash_map},
|
2024-02-19 22:08:02 -05:00
|
|
|
error::Error,
|
|
|
|
|
fs, io,
|
2024-08-22 08:17:06 -04:00
|
|
|
num::NonZeroU32,
|
2024-02-19 22:08:02 -05:00
|
|
|
process,
|
|
|
|
|
sync::Arc,
|
2024-05-07 10:08:03 -06:00
|
|
|
time::{Duration, Instant},
|
2024-02-19 22:08:02 -05:00
|
|
|
};
|
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;
|
|
|
|
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
2025-04-10 13:03:53 +02:00
|
|
|
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
|
|
|
|
|
use zbus::{Connection, proxy};
|
2024-02-06 15:03:07 -07:00
|
|
|
|
2025-05-09 16:10:03 -06:00
|
|
|
use crate::{
|
2025-06-26 15:31:10 -04:00
|
|
|
common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT},
|
2025-05-09 16:10:03 -06:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
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,
|
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>,
|
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>),
|
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),
|
2025-11-12 20:35:10 +01:00
|
|
|
SpinnerTick,
|
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,
|
|
|
|
|
spinner_rotation: f32,
|
|
|
|
|
spinner_handle: Option<cosmic::iced::task::Handle>,
|
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
|
|
|
}
|
|
|
|
|
|
2025-04-10 13:03:53 +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();
|
|
|
|
|
|
|
|
|
|
let items = widget::column::with_children(items);
|
|
|
|
|
let items = if item_cnt > 7 {
|
|
|
|
|
Element::from(
|
|
|
|
|
widget::scrollable(items)
|
|
|
|
|
.height(Length::Fixed(DEFAULT_MENU_ITEM_HEIGHT * 7.)),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
Element::from(items)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
let component = &cosmic.background.component;
|
|
|
|
|
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
|
|
|
}))
|
|
|
|
|
.width(Length::Fixed(240.0))
|
|
|
|
|
};
|
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,
|
|
|
|
|
i == 0,
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
let button_row = iced::widget::row![
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
.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)
|
|
|
|
|
.push(
|
|
|
|
|
widget::icon::from_name("process-working-symbolic")
|
|
|
|
|
.size(16)
|
|
|
|
|
.icon()
|
|
|
|
|
.rotation(iced::Rotation::Floating(iced::Radians(
|
|
|
|
|
self.spinner_rotation.to_radians(),
|
|
|
|
|
))),
|
|
|
|
|
)
|
|
|
|
|
.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
|
|
|
};
|
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(),
|
2025-02-21 17:05:50 -05:00
|
|
|
widget::layer_container(
|
2025-11-12 20:35:10 +01:00
|
|
|
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
|
2025-02-21 17:05:50 -05:00
|
|
|
)
|
|
|
|
|
.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.border = iced::Border::default().rounded(16);
|
|
|
|
|
appearance
|
|
|
|
|
},
|
|
|
|
|
)))
|
|
|
|
|
.class(cosmic::theme::Container::Background)
|
2025-10-30 11:31:48 -06:00
|
|
|
.width(Length::Fixed(800.0))
|
|
|
|
|
.into(),
|
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
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_xkb_config(&self) {
|
|
|
|
|
let user_data = match self
|
|
|
|
|
.selected_username
|
|
|
|
|
.data_idx
|
|
|
|
|
.and_then(|i| self.flags.user_datas.get(i))
|
|
|
|
|
{
|
|
|
|
|
Some(some) => some,
|
|
|
|
|
None => return,
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-27 13:05:21 +01:00
|
|
|
self.common.set_xkb_config(user_data);
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
2024-08-22 08:17:06 -04:00
|
|
|
|
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-05-09 18:47:52 -06:00
|
|
|
// Ensure that user's xkb config is used
|
2025-10-27 13:05:21 +01:00
|
|
|
self.common.set_xkb_config(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.
|
2025-05-09 16:10:03 -06:00
|
|
|
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
|
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,
|
|
|
|
|
spinner_rotation: 0.0,
|
|
|
|
|
spinner_handle: None,
|
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) => {
|
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),
|
|
|
|
|
);
|
2025-03-12 16:07:57 -04:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
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 {
|
|
|
|
|
return widget::text_input::focus(USERNAME_ID.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
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() {
|
|
|
|
|
return self.update(Message::RandrUpdate {
|
|
|
|
|
randr: Arc::new(Ok(randr_list.clone())),
|
|
|
|
|
});
|
|
|
|
|
}
|
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) => {
|
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-11-12 20:35:10 +01:00
|
|
|
|
|
|
|
|
// Start spinner animation if not already running
|
|
|
|
|
if self.spinner_handle.is_none() {
|
2026-02-24 15:49:22 -05:00
|
|
|
let (spinner_task, handle) =
|
2026-04-08 12:00:16 -04:00
|
|
|
cosmic::task::stream(cosmic::iced::stream::channel(
|
2026-02-24 15:49:22 -05:00
|
|
|
1,
|
|
|
|
|
|mut msg_tx: iced::futures::channel::mpsc::Sender<_>| async move {
|
|
|
|
|
let mut interval = time::interval(Duration::from_millis(16)); // ~60fps
|
|
|
|
|
loop {
|
|
|
|
|
msg_tx
|
|
|
|
|
.send(cosmic::Action::App(Message::SpinnerTick))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
interval.tick().await;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
.abortable();
|
2025-11-12 20:35:10 +01:00
|
|
|
self.spinner_handle = Some(handle);
|
|
|
|
|
return spinner_task;
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
|
|
|
|
// Stop spinner animation
|
|
|
|
|
if let Some(handle) = self.spinner_handle.take() {
|
|
|
|
|
handle.abort();
|
|
|
|
|
}
|
|
|
|
|
self.spinner_rotation = 0.0;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Stop spinner animation
|
|
|
|
|
if let Some(handle) = self.spinner_handle.take() {
|
|
|
|
|
handle.abort();
|
|
|
|
|
}
|
|
|
|
|
self.spinner_rotation = 0.0;
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
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
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
None => {}
|
|
|
|
|
},
|
|
|
|
|
Message::DropdownToggle(dropdown) => {
|
|
|
|
|
if self.dropdown_opt == Some(dropdown) {
|
|
|
|
|
self.dropdown_opt = None;
|
|
|
|
|
} else {
|
|
|
|
|
self.dropdown_opt = Some(dropdown);
|
2023-10-06 15:02:25 -06:00
|
|
|
}
|
2025-02-21 17:05:50 -05:00
|
|
|
}
|
|
|
|
|
Message::KeyboardLayout(layout_i) => {
|
2025-05-09 18:47:52 -06:00
|
|
|
if layout_i < self.common.active_layouts.len() {
|
|
|
|
|
self.common.active_layouts.swap(0, layout_i);
|
2025-02-21 17:05:50 -05:00
|
|
|
self.set_xkb_config();
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
return heartbeat;
|
|
|
|
|
}
|
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-11-12 20:35:10 +01:00
|
|
|
Message::SpinnerTick => {
|
|
|
|
|
// Update spinner rotation angle (360 degrees per second = 6 degrees per frame at 60fps)
|
|
|
|
|
self.spinner_rotation = (self.spinner_rotation + 6.0) % 360.0;
|
|
|
|
|
}
|
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
|
|
|
|
|
fn view(&self) -> Element<Self::Message> {
|
|
|
|
|
unimplemented!()
|
|
|
|
|
}
|
2023-10-05 17:47:23 -06:00
|
|
|
|
2025-02-21 17:05:50 -05:00
|
|
|
/// Creates a view after each update.
|
|
|
|
|
fn view_window(&self, surface_id: SurfaceId) -> Element<Self::Message> {
|
|
|
|
|
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)
|
|
|
|
|
}
|