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-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-03-12 16:07:57 -04:00
use cosmic ::iced ::{ Point , Size } ;
2025-02-21 17:05:50 -05:00
use cosmic ::iced_runtime ::platform_specific ::wayland ::subsurface ::SctkSubsurfaceSettings ;
use cosmic ::widget ::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 ,
2024-02-06 10:58:34 -07:00
iced ::{
2025-04-10 13:03:53 +02:00
self , Background , Border , Length , Subscription , alignment ,
2025-05-09 16:10:03 -06:00
event ::wayland ::OutputEvent ,
2025-02-21 17:05:50 -05:00
futures ::SinkExt ,
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
} ,
} ,
} ,
iced_runtime ::core ::window ::Id as SurfaceId ,
2025-04-10 16:39:32 -04:00
theme , widget ,
2023-10-06 15:02:25 -06:00
} ;
2025-08-07 19:05:31 -04:00
use cosmic ::{ cosmic_theme ::{ self , CosmicPalette } , surface } ;
use cosmic_config ::CosmicConfigEntry ;
2024-08-22 08:17:06 -04:00
use cosmic_greeter_config ::Config as CosmicGreeterConfig ;
2025-05-09 16:10:03 -06:00
use cosmic_greeter_daemon ::UserData ;
2025-08-07 19:05:31 -04:00
use cosmic_settings_daemon_config ::greeter ::GreeterAccessibilityState ;
use cosmic_settings_subscriptions ::cosmic_a11y_manager ::{
AccessibilityEvent , AccessibilityRequest ,
2025-08-06 21:41:38 -04:00
} ;
2024-07-28 05:12:48 +02:00
use greetd_ipc ::Request ;
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
path ::{ Path , PathBuf } ,
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-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 > {
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 ( )
. filter ( | user | {
if user . uid < 1000 {
// Skip system accounts
return false ;
}
match Path ::new ( & user . shell ) . file_name ( ) . and_then ( | x | x . to_str ( ) ) {
// Skip shell ending in false
Some ( " false " ) = > false ,
// Skip shell ending in nologin
Some ( " nologin " ) = > false ,
_ = > true ,
}
} )
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 > > {
2024-06-04 22:17:44 -06:00
env_logger ::Builder ::from_env ( env_logger ::Env ::default ( ) . default_filter_or ( " warn " ) ) . init ( ) ;
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 ) = > {
log ::error! ( " failed to load user data from daemon: {} " , err ) ;
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 " )
. map_or (
vec! [ PathBuf ::from ( " /usr/share/wayland-sessions " ) ] ,
| xdg_dirs | xdg_dirs . get_data_dirs ( ) ,
)
. into_iter ( )
. map ( | dir | ( dir , SessionType ::Wayland ) )
. chain (
xdg ::BaseDirectories ::with_prefix ( " xsessions " )
2024-03-19 21:11:21 -06:00
. map_or ( vec! [ PathBuf ::from ( " /usr/share/xsessions " ) ] , | xdg_dirs | {
xdg_dirs . 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 ( ) ;
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 ) = > {
log ::warn! (
" 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 ) = > {
log ::warn! (
" failed to read session directory {:?} entry: {:?} " ,
session_dir ,
err
) ;
continue ;
}
} ;
let entry = match freedesktop_entry_parser ::parse_entry ( dir_entry . path ( ) ) {
Ok ( ok ) = > ok ,
Err ( err ) = > {
log ::warn! (
" failed to read session file {:?}: {:?} " ,
dir_entry . path ( ) ,
err
) ;
continue ;
}
} ;
let name = match entry . section ( " Desktop Entry " ) . attr ( " Name " ) {
Some ( some ) = > some ,
None = > {
log ::warn! (
" failed to read session file {:?}: no Desktop Entry/Name attribute " ,
dir_entry . path ( )
) ;
continue ;
}
} ;
let exec = match entry . section ( " Desktop Entry " ) . attr ( " Exec " ) {
Some ( some ) = > some ,
None = > {
log ::warn! (
" 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
} ;
2024-03-19 21:11:21 -06:00
if let Some ( desktop_names ) = entry . section ( " Desktop Entry " ) . attr ( " 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 = > {
log ::warn! (
" failed to parse session file {:?} Exec field {:?} " ,
dir_entry . path ( ) ,
exec
) ;
continue ;
}
} ;
2024-06-05 10:43:05 -06:00
log ::info! ( " session {} using command {:?} env {:?} " , name , command , env ) ;
match sessions . insert ( name . to_string ( ) , ( command , env ) ) {
2023-10-05 17:47:23 -06:00
Some ( some ) = > {
2024-02-06 10:26:53 -07:00
log ::warn! ( " session {} overwrote old command {:?} " , name , some ) ;
2023-10-05 17:47:23 -06:00
}
None = > { }
}
}
}
sessions
} ;
2023-10-06 15:02:25 -06:00
let flags = Flags {
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 > ,
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 > ) ,
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 ) ,
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-06 21:41:38 -04:00
accessibility : Accessibility ,
}
#[ 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-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-02-21 17:05:50 -05:00
let mut status_row = widget ::row ::with_capacity ( 2 ) . padding ( 16.0 ) . spacing ( 12.0 ) ;
2024-02-06 15:03:07 -07:00
2025-05-09 16:10:03 -06:00
if let Some ( network_icon ) = self . common . network_icon_opt {
2025-02-21 17:05:50 -05:00
status_row = status_row . push ( widget ::icon ::from_name ( network_icon ) ) ;
}
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 {
2025-02-21 17:05:50 -05:00
status_row = status_row . push ( iced ::widget ::row! [
widget ::icon ::from_name ( power_icon . clone ( ) ) ,
widget ::text ( format! ( " {:.0} % " , power_percent ) ) ,
] ) ;
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 {
widget ::Space ::with_width ( Length ::Fixed ( 17.0 ) ) . into ( )
} ,
widget ::Space ::with_width ( Length ::Fixed ( 8.0 ) ) . into ( ) ,
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! [
Element ::from ( widget ::Space ::with_width ( Length ::Fixed ( 25.0 ) ) ) ,
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-06 21:41:38 -04:00
. on_press ( Message ::DropdownToggle ( Dropdown ::Accessibility ) ) , // We'll use Dropdown::Keyboard as a dummy, since we don't have a dedicated Dropdown for accessibility
)
. 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 ( ) ,
Message ::ScreenReader ( ! self . accessibility . screen_reader . is_some ( ) ) ,
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 ,
] )
. align_x ( alignment ::Horizontal ::Left )
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 = {
let mut column = widget ::column ::with_capacity ( 2 )
. spacing ( 12.0 )
. max_width ( 280.0 ) ;
2024-02-06 10:58:34 -07: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 = > {
for user_data in & self . flags . user_datas {
2025-06-26 15:14:43 -04:00
if ! self . entering_name & & user_data . name = = self . selected_username . username
{
2025-02-21 17:05:50 -05:00
match & user_data . icon_opt {
Some ( icon ) = > {
column = column . push (
widget ::container (
widget ::Image ::new (
//TODO: cache handle
widget ::image ::Handle ::from_bytes ( icon . clone ( ) ) ,
)
. width ( Length ::Fixed ( 78.0 ) )
. height ( Length ::Fixed ( 78.0 ) ) ,
)
. width ( Length ::Fill )
. align_x ( alignment ::Horizontal ::Center ) ,
)
2024-02-06 10:58:34 -07:00
}
2025-02-21 17:05:50 -05:00
None = > { }
}
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 )
. align_x ( alignment ::Horizontal ::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 ) )
. on_submit ( | v | Message ::Username ( v ) ) ,
)
}
2025-05-09 16:10:03 -06:00
match & self . common . prompt_opt {
2025-02-21 17:05:50 -05:00
Some ( ( prompt , secret , value_opt ) ) = > match value_opt {
Some ( value ) = > {
let text_input_id = self
2025-05-09 16:10:03 -06:00
. common
2025-02-21 17:05:50 -05:00
. surface_names
. get ( & id )
2025-05-09 16:10:03 -06:00
. and_then ( | id | self . common . text_input_ids . get ( id ) )
2025-02-21 17:05:50 -05:00
. cloned ( )
. unwrap_or_else ( | | cosmic ::widget ::Id ::new ( " text_input " ) ) ;
let mut text_input = widget ::secure_input (
prompt . clone ( ) ,
2025-05-09 18:47:52 -06:00
value . as_str ( ) ,
Some (
common ::Message ::Prompt (
prompt . clone ( ) ,
! * secret ,
Some ( value . clone ( ) ) ,
)
. into ( ) ,
) ,
2025-02-21 17:05:50 -05:00
* secret ,
)
. id ( text_input_id )
2025-05-09 18:47:52 -06:00
. on_input ( | input | {
common ::Message ::Prompt ( prompt . clone ( ) , * secret , Some ( input ) )
. into ( )
} )
2025-02-21 17:05:50 -05:00
. on_submit ( | v | Message ::Auth ( Some ( v ) ) ) ;
if let Some ( text_input_id ) = self
2025-05-09 16:10:03 -06:00
. common
2025-02-21 17:05:50 -05:00
. surface_names
. get ( & id )
2025-05-09 16:10:03 -06:00
. and_then ( | id | self . common . text_input_ids . get ( id ) )
2025-02-21 17:05:50 -05:00
{
text_input = text_input . id ( text_input_id . clone ( ) ) ;
}
2024-02-06 10:58:34 -07:00
2025-02-21 17:05:50 -05:00
if * secret {
text_input = text_input . password ( )
}
2024-02-06 10:58:34 -07:00
2025-02-21 17:05:50 -05:00
column = column . push ( text_input ) ;
2025-05-15 14:18:08 -06:00
if self . common . caps_lock {
column = column . push ( widget ::text ( fl! ( " caps-lock " ) ) ) ;
}
2024-02-06 10:58:34 -07:00
}
None = > {
2025-02-21 17:05:50 -05:00
column = column . push (
widget ::button ::custom ( " Confirm " ) . on_press ( Message ::Auth ( None ) ) ,
) ;
2024-02-06 10:58:34 -07:00
}
2025-02-21 17:05:50 -05:00
} ,
None = > { }
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-05-09 16:10:03 -06:00
if let Some ( error ) = & self . common . error_opt {
2025-02-21 17:05:50 -05:00
column = column . push ( widget ::text ( error ) ) ;
2024-02-06 10:58:34 -07:00
}
2025-02-21 17:05:50 -05:00
widget ::container ( column )
. align_x ( alignment ::Horizontal ::Center )
. width ( Length ::Fill )
} ;
let menu = widget ::container (
widget ::layer_container (
iced ::widget ::row! [ left_element , right_element ]
. align_y ( alignment ::Alignment ::Center ) ,
)
. 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 )
. width ( Length ::Fixed ( 800.0 ) ) ,
)
. padding ( [ 32.0 , 0.0 , 0.0 , 0.0 ] )
. width ( Length ::Fill )
. height ( Length ::Shrink )
. align_x ( alignment ::Horizontal ::Center )
. align_y ( alignment ::Vertical ::Top ) ;
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-05-09 18:47:52 -06: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-05-09 18:47:52 -06: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
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-02-21 17:05:50 -05:00
match & user_data . theme_opt {
Some ( theme ) = > {
2025-08-07 19:05:31 -04:00
self . accessibility . high_contrast = theme . is_high_contrast ;
2025-03-05 23:08:56 -05:00
cosmic ::command ::set_theme ( cosmic ::Theme ::custom ( Arc ::new ( theme . clone ( ) ) ) )
2024-07-28 05:12:48 +02:00
}
2025-02-21 17:05:50 -05:00
None = > Task ::none ( ) ,
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 > ) {
let ( mut common , common_task ) = Common ::init ( core ) ;
common . on_output_event = Some ( Box ::new ( | output_event , output | {
Message ::OutputEvent ( output_event , output )
} ) ) ;
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 ( ) ;
let data_idx = Some ( 0 ) ;
let selected_username = NameIndexPair { username , data_idx } ;
2025-08-06 21:41:38 -04:00
let mut accessibility = Accessibility ::default ( ) ;
accessibility . helper =
cosmic_settings_daemon_config ::greeter ::GreeterAccessibilityState ::config ( ) . ok ( ) ;
2025-08-07 19:05:31 -04:00
// Reset the state so that only new changes are applied.
if let Some ( helper ) = accessibility . helper . as_ref ( ) {
_ = GreeterAccessibilityState ::write_entry ( & Default ::default ( ) , helper ) ;
}
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 ( ) ,
2023-10-06 15:02:25 -06:00
} ;
2025-05-09 16:10:03 -06:00
( app , common_task )
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 ) = > {
return self . common . update ( common_message ) ;
}
2025-08-07 19:05:31 -04:00
2025-02-21 17:05:50 -05:00
Message ::OutputEvent ( output_event , output ) = > {
match output_event {
OutputEvent ::Created ( output_info_opt ) = > {
log ::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-05-09 16:10:03 -06:00
match self . common . surface_ids . insert ( output . clone ( ) , surface_id ) {
2025-02-21 17:05:50 -05:00
Some ( old_surface_id ) = > {
//TODO: remove old surface?
log ::warn! (
" output {}: already had surface ID {:?} " ,
output . id ( ) ,
old_surface_id
) ;
}
None = > { }
}
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 = > {
log ::warn! ( " output {}: no output name " , output . id ( ) ) ;
}
} ,
None = > {
log ::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 ,
pointer_interactivity : true ,
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 = > {
log ::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 = > {
log ::warn! ( " output {}: no surface found " , output . id ( ) ) ;
}
}
}
OutputEvent ::InfoUpdate ( _output_info ) = > {
log ::info! ( " output {}: info update " , output . id ( ) ) ;
}
}
}
Message ::Socket ( socket_state ) = > {
self . socket_state = socket_state ;
match & self . socket_state {
SocketState ::Open = > {
// When socket is opened, send create session
2025-04-10 13:03:53 +02:00
self . send_request ( Request ::CreateSession {
2025-02-21 17:05:50 -05:00
username : self . selected_username . username . clone ( ) ,
} ) ;
}
_ = > { }
}
}
2025-08-07 19:05:31 -04:00
Message ::Reload ( new ) = > {
return cosmic ::command ::set_theme (
new . clone ( ) ,
) ;
}
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-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 ) ;
} ;
match & self . socket_state {
SocketState ::Open = > {
2025-05-09 16:10:03 -06:00
self . common . prompt_opt = None ;
2025-04-10 13:03:53 +02:00
self . send_request ( Request ::CancelSession ) ;
2023-10-05 17:47:23 -06:00
}
2025-02-21 17:05:50 -05:00
_ = > { }
2023-10-05 17:47:23 -06: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-06-30 10:45:03 -04:00
log ::error! (
" 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 {
log ::error! (
" 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 ) ;
if let Err ( err ) = handler . set ( " last_user " , & self . flags . greeter_config . last_user ) {
log ::error! (
" 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 ) {
log ::error! (
" 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 . prompt_opt = None ;
self . common . error_opt = None ;
2025-04-10 13:03:53 +02:00
self . send_request ( Request ::PostAuthMessageResponse { response } ) ;
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-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-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 ) = > {
log ::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 ) = > {
log ::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 ) = > {
log ::error! ( " failed to suspend: {:?} " , err ) ;
}
}
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 ( ) {
let ( heartbeat , handle ) = cosmic ::task ::stream (
cosmic ::iced_futures ::stream ::channel ( 1 , | mut msg_tx | 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 ( ) ;
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-08-06 21:41:38 -04:00
} else {
2025-08-07 19:05:31 -04:00
if let Some ( mut c ) = self . accessibility . screen_reader . take ( ) {
return cosmic ::task ::future ::< ( ) , ( ) > ( async move {
if let Err ( err ) = c . kill ( ) . await {
log ::error! ( " Failed to stop screen reader: {err:?} " ) ;
}
} )
. discard ( ) ;
}
}
if let Some ( helper ) = self . accessibility . helper . as_ref ( ) {
_ = self
. accessibility
. state
. 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
. set_magnifier ( & helper , Some ( enabled ) ) ;
}
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
. set_high_contrast ( & helper , Some ( enabled ) ) ;
}
let builder = self . theme_builder . clone ( ) ;
return cosmic ::task ::future ::< _ , _ > ( async move {
let builder = builder . clone ( ) ;
let ( tx , rx ) = tokio ::sync ::oneshot ::channel ( ) ;
std ::thread ::spawn ( move | | {
match apply_hc_theme ( builder , enabled ) {
Ok ( t ) = > {
_ = tx . send ( Some ( t ) ) ;
}
Err ( err ) = > {
log ::error! ( " {err:?} " ) ;
_ = tx . send ( None ) ;
}
}
} ) ;
if let Ok ( Some ( theme ) ) = rx . await {
cosmic ::Action ::App ( Message ::Reload ( cosmic ::Theme ::custom ( std ::sync ::Arc ::new ( theme ) ) ) )
} 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
. set_invert_colors ( & helper , Some ( enabled ) ) ;
}
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 ) = > {
self . accessibility . wayland_sender = Some ( tx ) ;
}
} ,
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-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
pub fn apply_hc_theme ( builder : cosmic_theme ::ThemeBuilder , enabled : bool ) -> Result < cosmic_theme ::Theme , cosmic_config ::Error > {
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 )
}