Compare commits

...

16 commits

Author SHA1 Message Date
bfe657faa0 Merge remote-tracking branch 'upstream/master' into local/yoda-main
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/greeter.rs
#	src/locker.rs
2026-08-19 22:01:39 +02:00
Michael Aaron Murphy
e9ab9e74a5 feat: enable JPEG-XL and AVIF image support 2026-08-18 18:58:39 +02:00
Michael Murphy
7e7b0261c3
i18n: translation update from Hosted Weblate (#506) 2026-08-18 14:03:26 +02:00
Hosted Weblate
aee6352fdc
i18n: translation updates from weblate
Co-authored-by: CodeBeats <nicholai67@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Michael Millet <mikevlet@protonmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Steen Rabol <steen@rabol.dev>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/pt_BR/
Translation: Pop OS/COSMIC Greeter
2026-08-18 09:08:33 +00:00
chris-010
088f26cbb8 fix: ignore empty responses on the login screen
Message::Auth is constructed in exactly one place, the .on_submit of the
password input, and it forwards whatever the field holds to greetd. An
empty field therefore sends PostAuthMessageResponse with response
Some(""), PAM rejects it, and greetd tears the session down; the greeter
then reconnects with a fresh CreateSession. Every stray Enter costs that
round trip, and while it runs the user is looking at the authenticating
spinner.

Non-interactive auth messages (AuthMessageType::Info) are acknowledged
separately with response None and never reach Message::Auth, so an empty
Some can only come from the user submitting an empty field.

This is the login-screen counterpart of the lock-screen fix in #509. The
two do not overlap; that one guards Message::Submit in locker.rs.

Drafted with AI assistance (Claude Code); the change was reviewed, built
and tested by me as described.

Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com>
2026-08-18 04:21:19 +02:00
Ralph S
ae627ab0d0
fix: ignore empty password 2026-08-17 13:45:03 -06:00
Hugo
f64e2d8fab
fix: detect and enable logind functionality at runtime
Downstream distributions have optional logind, but the logind feature
must be defined at compile time. If cosmic-greeter is compiled with
logind support it fails to run on a host that's not running logind.

Detect whether logind is present at runtime, enabling the use of the
same binaries regardless of whether logind is installed or not.
2026-07-29 17:51:37 +02:00
Ian Douglas Scott
d39915ae23 Use cosmic-keymap-unstable-v1 2026-07-28 16:17:56 -04:00
Michael Murphy
15476e3861
i18n: translation update from Hosted Weblate (#479)
i18n: translation updates from weblate






Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/sat/
Translation: Pop OS/COSMIC Greeter

Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
2026-07-28 16:40:34 +02:00
Hosted Weblate
766a77a2a7
i18n: translation updates from weblate
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/sat/
Translation: Pop OS/COSMIC Greeter
2026-07-28 08:01:59 +00:00
Ashley Wulber
2290aae5e2 chore: update libcosmic 2026-07-09 17:51:17 -06:00
Ashley Wulber
95067445be improv: better handling of popover dialog after dropdown 2026-07-09 12:10:46 -06:00
Ashley Wulber
05f09fe33c improv: use the new menu column widget 2026-07-09 12:10:46 -06:00
Ashley Wulber
6a19629f15 fix: update libcosmic 2026-07-09 12:10:46 -06:00
Ashley Wulber
119d98fe7c chore: update deps 2026-07-09 12:10:46 -06:00
Ashley Wulber
0b7803077a feat: blur 2026-07-09 12:10:46 -06:00
18 changed files with 1705 additions and 753 deletions

1672
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -80,12 +80,15 @@ cosmic-randr-shell = { workspace = true }
kdl.workspace = true
color-eyre.workspace = true
image = { version = "0.25", default-features = false, features = [
"avif-native",
"jpeg",
"png",
"rayon",
"webp",
"hdr",
] }
# JPEG-XL support
jxl-oxide = { version = "0.12.6", features = ["image"] }
[dependencies.greetd_ipc]
version = "0.10.3"

1
debian/control vendored
View file

@ -7,6 +7,7 @@ Build-Depends:
git,
just (>= 1.13.0),
libclang-dev,
libdav1d-dev,
libinput-dev,
libpam-dev,
libwayland-dev,

View file

@ -0,0 +1,37 @@
cancel = Annuller
restart = Genstart
caps-lock = Skiftelås er aktiv.
enter-user = Indtast navn manuelt...
type-username = Brugernavn:
keyboard-layout = Tastaturopsætning
restart-now = Genstart nu?
session = Session
shutdown = Luk ned
shutdown-now = Luk ned nu?
user = Bruger
suspend = Slumre
auth-error-default = Godkendelse fejlede. Prøv venligst igen.
auth-error-credentials = Ukorrekt adgangskode. Kontrollér venligst din tastaturopsætning og prøv igen.
auth-error-denied = Adgang nægtet.
auth-error-maxtries = For mange mislykkede godkendelsesforsøg.
auth-error-account = Konto er utilgængelig eller deaktiveret.
accessibility = Accessibility
.screen-reader = Skærmlæser
.magnifier = Forstørrelsesglas
.high-contrast = Høj kontrast
.invert-colors = Invertér Farver
authenticating = Godkender...
restart-timeout =
The system will restart automatically
{ $seconds ->
[0] now.
[1] in 1 second.
*[other] in { $seconds } seconds.
}
shutdown-timeout =
Systemet lukker automatisk ned
{ $seconds ->
[0] nu.
[1] om 1 sekund.
*[other] om { $seconds } sekunder.
}

View file

@ -27,11 +27,11 @@ accessibility = Barrierefreiheit
.magnifier = Vergrößerungsglas
.high-contrast = Hoher Kontrast
.invert-colors = Farben invertieren
enter-user = Name manuell eingeben…
enter-user = Name manuell eingeben ...
type-username = Benutzername:
auth-error-default = Authentifizierung fehlgeschlagen. Bitte versuche es erneut.
auth-error-credentials = Falsches Passwort. Bitte überprüfe deine Tastaturbelegung und versuche es erneut.
auth-error-denied = Zugriff verweigert.
auth-error-maxtries = Zu viele fehlgeschlagene Authentifizierungsversuche.
auth-error-account = Konto ist nicht verfügbar oder deaktiviert.
authenticating = Authentifizierung...
authenticating = Authentifizierung ...

View file

@ -0,0 +1 @@
cancel = Cancel

View file

@ -0,0 +1 @@
cancel = لغو

View file

@ -0,0 +1,37 @@
cancel = Cancelar
accessibility = Accesibilidade
.screen-reader = Lector de pantalla
.magnifier = Lupa
.high-contrast = Alto contraste
.invert-colors = Inverter cores
authenticating = Autentificando...
caps-lock = Bloqueo de Maiúsculas activo.
enter-user = Introducir nome manualmente...
type-username = Nome de usuario:
keyboard-layout = Distribución do teclado
restart = Reiniciar
restart-now = Reiniciar agora?
restart-timeout =
O sistema reiniciarase automaticamente
{ $seconds ->
[0] agora.
[1] dentro de 1 segundo.
*[other] dentro de { $seconds } segundos.
}
session = Sesión
shutdown = Apagar
shutdown-now = Apagar agora?
shutdown-timeout =
O sistema apagarase automaticamente
{ $seconds ->
[0] agora.
[1] dentro de 1 segundo.
*[other] dentro de { $seconds } segundos.
}
suspend = Suspender
user = Usuario
auth-error-default = A autenticación fallou. Téntao de novo.
auth-error-credentials = Contrasinal incorrecto. Comproba a distribución do teclado e téntao de novo.
auth-error-denied = Acceso denegado.
auth-error-maxtries = Demasiados intentos de autenticación.
auth-error-account = A conta non estña dispoñible ou está deshabilitada.

View file

@ -0,0 +1,15 @@
shutdown = Atudar
restart-timeout =
Lo sistèma reaviarà automaticament
{ $seconds ->
[0] ara.
[1] d’aquí 1 segondas.
*[other] d’aquí { $seconds } segondas.
}
shutdown-timeout =
Lo sistèma s'atudarà automaticament
{ $seconds ->
[0] ara.
[1] d’aquí 1 segondas.
*[other] d’aquí { $seconds } segondas.
}

View file

@ -29,7 +29,7 @@ shutdown-timeout =
}
suspend = Suspender
user = Usuário
authenticating = Autenticando…
authenticating = Autenticando...
auth-error-default = A autenticação falhou. Tente novamente.
auth-error-credentials = Senha incorreta. Verifique o layout do seu teclado e tente novamente.
auth-error-denied = Acesso negado.

View file

@ -0,0 +1 @@
cancel = ᱵᱟᱹᱛᱤᱞ

View file

@ -3,14 +3,21 @@ use cosmic::iced::core::SmolStr;
use cosmic::iced::event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent};
use cosmic::iced::event::{self};
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
use cosmic::iced::platform_specific::shell::commands::blur::blur;
use cosmic::iced::runtime::core::window::Id as SurfaceId;
use cosmic::iced::runtime::platform_specific::wayland::CornerRadius;
use cosmic::iced::{self, Rectangle, Size, Subscription};
use cosmic::widget;
use cosmic_config::{ConfigSet, CosmicConfigEntry};
use cosmic_greeter_daemon::{BgSource, CosmicCompConfig, UserData};
use std::collections::HashMap;
use cosmic::surface::corner_radius::rounded_rect_strips;
use cosmic::widget::rectangle_tracker::{RectangleUpdate, rectangle_tracker_subscription};
use cosmic::widget::{self, RectangleTracker};
use cosmic_greeter_daemon::{BgSource, UserData};
use cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::{Connection, Proxy};
use crate::keyboard_layout_wayland;
pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.;
@ -22,13 +29,15 @@ pub struct ActiveLayout {
}
pub struct Common<M> {
pub wayland_connection: Option<Connection>,
pub keyboard_layout: Option<ZcosmicKeyboardLayoutV1>,
pub current_keyboard_layout: usize,
pub active_layouts: Vec<ActiveLayout>,
pub active_surface_id_opt: Option<SurfaceId>,
pub on_battery: bool,
pub battery_percent: f64,
pub charging_limit: Option<bool>,
pub caps_lock: bool,
pub comp_config_handler: Option<cosmic_config::Config>,
pub core: Core,
pub error_opt: Option<String>,
pub fallback_background: widget::image::Handle,
@ -39,8 +48,12 @@ pub struct Common<M> {
pub output_names: HashMap<WlOutput, String>,
pub power_info_opt: Option<(widget::Icon, f64)>,
pub prompt_opt: Option<(String, bool, Option<String>)>,
pub rectangle_tracker: Option<RectangleTracker<(SurfaceId, bool)>>,
pub rectangles: HashMap<(SurfaceId, bool), iced::Rectangle>,
pub include_menu: bool,
pub subsurface_rects: HashMap<WlOutput, Rectangle>,
pub surface_ids: HashMap<WlOutput, SurfaceId>,
pub subsurface_outputs: HashMap<SurfaceId, WlOutput>,
pub surface_images: HashMap<SurfaceId, widget::image::Handle>,
pub surface_names: HashMap<SurfaceId, String>,
pub text_input_ids: HashMap<String, widget::Id>,
@ -54,12 +67,15 @@ pub enum Message {
Focus(SurfaceId),
Key(Modifiers, Key, Option<SmolStr>),
NetworkIcon(Option<&'static str>),
SubsurfaceOpened(SurfaceId),
OutputEvent(OutputEvent, WlOutput),
PowerInfo(Option<(f64, bool, bool)>),
Prompt(String, bool, Option<String>),
SessionLockEvent(SessionLockEvent),
Tick,
Tz(jiff::tz::TimeZone),
Rectangle(RectangleUpdate<(SurfaceId, bool)>),
KeyboardLayoutWayland(keyboard_layout_wayland::Event),
}
impl<M: From<Message> + Send + 'static> Common<M> {
@ -72,17 +88,6 @@ impl<M: From<Message> + Send + 'static> Common<M> {
core.window.show_minimize = false;
core.window.use_template = false;
let comp_config_handler = match cosmic_config::Config::new(
"com.system76.CosmicComp",
CosmicCompConfig::VERSION,
) {
Ok(config_handler) => Some(config_handler),
Err(err) => {
tracing::error!("failed to create cosmic-comp config handler: {}", err);
None
}
};
let layouts_opt = match xkb_data::all_keyboard_layouts() {
Ok(ok) => Some(Arc::new(ok)),
Err(err) => {
@ -92,10 +97,12 @@ impl<M: From<Message> + Send + 'static> Common<M> {
};
let app = Self {
wayland_connection: None,
keyboard_layout: None,
current_keyboard_layout: 0,
active_layouts: Vec::new(),
active_surface_id_opt: None,
caps_lock: false,
comp_config_handler,
core,
error_opt: None,
fallback_background: widget::image::Handle::from_bytes(
@ -118,6 +125,10 @@ impl<M: From<Message> + Send + 'static> Common<M> {
battery_percent: 0.0,
on_battery: false,
charging_limit: None,
subsurface_outputs: HashMap::new(),
rectangle_tracker: None,
rectangles: HashMap::new(),
include_menu: false,
};
(
app,
@ -128,27 +139,6 @@ impl<M: From<Message> + Send + 'static> Common<M> {
)
}
pub fn set_xkb_config(&self, user_data: &UserData) {
if let Some(mut xkb_config) = user_data.xkb_config_opt.clone() {
xkb_config.layout = String::new();
xkb_config.variant = String::new();
for (i, layout) in self.active_layouts.iter().enumerate() {
if i > 0 {
xkb_config.layout.push(',');
xkb_config.variant.push(',');
}
xkb_config.layout.push_str(&layout.layout);
xkb_config.variant.push_str(&layout.variant);
}
if let Some(comp_config_handler) = &self.comp_config_handler {
match comp_config_handler.set("xkb_config", xkb_config) {
Ok(()) => tracing::info!("updated cosmic-comp xkb_config"),
Err(err) => tracing::error!("failed to update cosmic-comp xkb_config: {}", err),
}
}
}
}
pub fn update_wallpapers(&mut self, user_data: &UserData) {
for (_output, surface_id) in self.surface_ids.iter() {
if self.surface_images.contains_key(surface_id) {
@ -287,6 +277,11 @@ impl<M: From<Message> + Send + 'static> Common<M> {
network_icon_opt.map(|name| widget::icon::from_name(name).into());
}
Message::OutputEvent(output_event, output) => {
if self.wayland_connection.is_none() {
if let Some(backend) = output.backend().upgrade() {
self.wayland_connection = Some(Connection::from_backend(backend));
}
}
if let Some(on_output_event) = &self.on_output_event {
return Task::done(cosmic::Action::App(on_output_event(output_event, output)));
}
@ -322,13 +317,88 @@ impl<M: From<Message> + Send + 'static> Common<M> {
Message::Tz(tz) => {
self.time.set_tz(tz);
}
Message::SubsurfaceOpened(id) => {
return self.blur_rects(id);
}
Message::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => {
self.rectangles.insert(r.0, r.1);
return self.blur_rects(r.0.0);
}
RectangleUpdate::Init(tracker) => {
self.rectangle_tracker.replace(tracker);
}
},
Message::KeyboardLayoutWayland(w) => match w {
keyboard_layout_wayland::Event::KeyboardLayout(keyboard_layout) => {
self.keyboard_layout = Some(keyboard_layout);
}
keyboard_layout_wayland::Event::Group(group) => {
self.current_keyboard_layout = group;
}
},
}
Task::none()
}
pub(crate) fn dropdown_blur_rects(&mut self, enable: bool) -> Task<M> {
let mut ids = HashSet::new();
for r in self.rectangles.keys() {
ids.insert(r.0);
}
self.include_menu = enable;
Task::batch(
ids.into_iter()
.map(|i| self.blur_rects(i))
.collect::<Vec<_>>(),
)
}
pub fn blur_rects(&mut self, id: SurfaceId) -> Task<M> {
if let Some(output) = self.subsurface_outputs.get(&id) {
if let Some(rect) = self.subsurface_rects.get(output) {
let x = rect.x;
let y = rect.y;
if let Some(rect) = self.rectangles.get(&(id, false)) {
let mut offset_rect = *rect;
offset_rect.x += x;
offset_rect.y += y;
let rad = CornerRadius {
top_left: 16,
top_right: 16,
bottom_left: 16,
bottom_right: 16,
};
let mut rects = rounded_rect_strips(offset_rect, rad);
if let Some(other_rect) = self.rectangles.get(&(id, true))
&& self.include_menu
{
let mut other = *other_rect;
other.x += x;
other.y += y;
let rad = CornerRadius {
top_left: 8,
top_right: 8,
bottom_left: 8,
bottom_right: 8,
};
rects.append(&mut rounded_rect_strips(other, rad));
}
return blur(id, Some(rects)).discard();
} else {
tracing::error!("no rectangle for surface {id:?}");
}
}
}
Task::none()
}
pub fn subscription(&self) -> Subscription<Message> {
let mut subscriptions = Vec::with_capacity(3);
subscriptions
.push(rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)));
subscriptions.push(event::listen_with(|event, status, id| match event {
iced::Event::Keyboard(KeyEvent::KeyPressed {
key,
@ -351,12 +421,21 @@ impl<M: From<Message> + Send + 'static> Common<M> {
WaylandEvent::SessionLock(lock_event) => {
Some(Message::SessionLockEvent(lock_event))
}
iced::event::wayland::Event::Subsurface(
iced::event::wayland::SubsurfaceEvent::Created,
) => Some(Message::SubsurfaceOpened(id)),
_ => None,
},
iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)),
_ => None,
}));
if let Some(conn) = self.wayland_connection.clone() {
subscriptions.push(
keyboard_layout_wayland::subscription(conn).map(Message::KeyboardLayoutWayland),
);
}
#[cfg(feature = "networkmanager")]
{
subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon));

View file

@ -24,7 +24,8 @@ use cosmic::iced::platform_specific::shell::wayland::commands::subsurface::repos
use cosmic::iced::runtime::core::window::Id as SurfaceId;
use cosmic::iced::runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
use cosmic::iced::{
self, Alignment, Background, Border, Length, Point, Size, Subscription, window,
self, Alignment, Background, Border, Color, Length, Point, Rectangle, Size, Subscription,
window,
};
use cosmic::widget::{id_container, text};
use cosmic::{Element, executor, surface, theme, widget};
@ -279,6 +280,8 @@ pub fn main() -> Result<(), Box<dyn Error>> {
sessions
};
let logind_available = cfg!(feature = "logind") && crate::logind::is_available();
let flags = Flags {
user_icons: user_datas
.iter_mut()
@ -288,6 +291,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
sessions,
greeter_config,
greeter_config_handler,
logind_available,
};
let settings = Settings::default().no_main_window(true);
@ -304,6 +308,7 @@ pub struct Flags {
sessions: HashMap<String, (Vec<String>, Vec<String>)>,
greeter_config: CosmicGreeterConfig,
greeter_config_handler: Option<cosmic_config::Config>,
logind_available: bool,
}
#[derive(Clone, Debug)]
@ -537,7 +542,7 @@ impl App {
let dropdown_menu = |items: Vec<_>| {
let item_cnt = items.len();
let items = widget::column::with_children(items);
let items = widget::menu::menu_column::MenuColumn::with_children(items);
let items = if item_cnt > 7 {
Element::from(
widget::scrollable(items)
@ -547,12 +552,12 @@ impl App {
Element::from(items)
};
widget::container(items)
let menu = widget::container(items)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
let component = &cosmic.background(theme.transparent).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
@ -565,7 +570,13 @@ impl App {
..Default::default()
}
}))
.width(Length::Fixed(240.0))
.width(Length::Fixed(240.0));
if let Some(t) = self.common.rectangle_tracker.as_ref() {
Element::from(t.container((id, true), menu))
} else {
menu.into()
}
};
let mut input_button = widget::popover(
@ -579,7 +590,7 @@ impl App {
for (i, layout) in self.common.active_layouts.iter().enumerate() {
items.push(menu_checklist(
&layout.description,
i == 0,
i == self.common.current_keyboard_layout,
Message::KeyboardLayout(i),
));
}
@ -681,7 +692,7 @@ impl App {
let accessibility_button = accessibility_dropdown;
let button_row = iced::widget::row![
let mut button_row = iced::widget::row![
widget::tooltip(
accessibility_button,
text(fl!("accessibility")),
@ -702,30 +713,32 @@ impl App {
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);
];
if self.flags.logind_available {
button_row = button_row
.push(widget::tooltip(
widget::button::custom(widget::icon::from_name("system-suspend-symbolic"))
.padding(12.0)
.on_press(Message::Suspend),
text(fl!("suspend")),
widget::tooltip::Position::Top,
))
.push(widget::tooltip(
widget::button::custom(widget::icon::from_name("system-reboot-symbolic"))
.padding(12.0)
.on_press(Message::Restart),
text(fl!("restart")),
widget::tooltip::Position::Top,
))
.push(widget::tooltip(
widget::button::custom(widget::icon::from_name("system-shutdown-symbolic"))
.padding(12.0)
.on_press(Message::Shutdown),
text(fl!("shutdown")),
widget::tooltip::Position::Top,
));
}
let button_row = button_row.padding([16.0, 0.0, 0.0, 0.0]).spacing(8.0);
widget::container(iced::widget::column![
date_time_column,
@ -928,29 +941,37 @@ impl App {
},
)
};
let menu = widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
)
.layer(cosmic::cosmic_theme::Layer::Background)
.padding(16)
.class(cosmic::theme::Container::Custom(Box::new(
|theme: &cosmic::Theme| {
// Use background appearance as the base
let mut appearance =
widget::container::Catalog::style(theme, &cosmic::theme::Container::Background);
appearance.background = Some(iced::Background::Color(
// TODO if we can use popups instead of subsurfaces for the greeter and the lockscreen
// then we can allow transparency
theme.cosmic().background(theme.transparent).base.into(),
));
appearance.border = iced::Border::default().rounded(16);
appearance
},
)))
.width(Length::Fixed(800.0));
let menu = if let Some(t) = self.common.rectangle_tracker.as_ref() {
Element::from(t.container((id, false), menu))
} else {
menu.into()
};
let menu = widget::container(widget::column::with_children(vec![
widget::space::vertical()
.height(Length::FillPortion(1))
.into(),
widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
)
.layer(cosmic::cosmic_theme::Layer::Background)
.padding(16)
.class(cosmic::theme::Container::Custom(Box::new(
|theme: &cosmic::Theme| {
// Use background appearance as the base
let mut appearance = widget::container::Catalog::style(
theme,
&cosmic::theme::Container::Background,
);
appearance.border = iced::Border::default().rounded(16);
appearance
},
)))
.class(cosmic::theme::Container::Background)
.width(Length::Fixed(800.0))
.into(),
menu,
widget::space::vertical()
.height(Length::FillPortion(4))
.into(),
@ -959,6 +980,7 @@ impl App {
.height(Length::Fill)
.align_x(Alignment::Center);
// TODO make these opaque?
let popover = widget::popover(menu).modal(true);
match self.dialog_page_opt {
Some(DialogPage::Restart(instant)) => {
@ -1013,19 +1035,6 @@ impl App {
}
}
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,
};
self.common.set_xkb_config(user_data);
}
fn update_user_data(&mut self) -> Task<Message> {
let user_data = match self
.selected_username
@ -1040,9 +1049,6 @@ impl App {
self.common.update_user_data(user_data);
// Ensure that user's xkb config is used
self.common.set_xkb_config(user_data);
if let Some(builder) = &user_data.theme_builder_opt {
self.theme_builder = builder.clone();
}
@ -1090,7 +1096,8 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
core.set_app_type(cosmic::core::AppType::System);
let mut tasks = Vec::new();
let (mut common, common_task) = Common::init(core);
common.on_output_event = Some(Box::new(|output_event, output| {
@ -1251,7 +1258,12 @@ impl cosmic::Application for App {
surface_id,
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32),
);
self.common
.subsurface_rects
.insert(output.clone(), Rectangle::new(loc, sub_size));
self.common
.subsurface_outputs
.insert(subsurface_id, output.clone());
let msg = cosmic::surface::action::subsurface(
move |_: &mut App| SctkSubsurfaceSettings {
parent: surface_id,
@ -1330,6 +1342,7 @@ impl cosmic::Application for App {
self.selected_session = selected_session;
if self.dropdown_opt == Some(Dropdown::Session) {
self.dropdown_opt = None;
return self.common.dropdown_blur_rects(false);
}
}
Message::EnterUser(focus_input, username) => {
@ -1346,7 +1359,10 @@ impl cosmic::Application for App {
username,
};
if focus_input {
return widget::text_input::focus(USERNAME_ID.clone());
return Task::batch([
self.common.dropdown_blur_rects(false),
widget::text_input::focus(USERNAME_ID.clone()),
]);
}
}
Message::Username(username) => {
@ -1384,9 +1400,12 @@ impl cosmic::Application for App {
self.send_request(Request::CancelSession);
}
if let Some(randr_list) = self.randr_list.as_ref() {
return self.update(Message::RandrUpdate {
randr: Arc::new(Ok(randr_list.clone())),
});
return Task::batch([
self.common.dropdown_blur_rects(false),
self.update(Message::RandrUpdate {
randr: Arc::new(Ok(randr_list.clone())),
}),
]);
}
}
}
@ -1467,6 +1486,9 @@ impl cosmic::Application for App {
}
}
Message::Auth(response) => {
if response.as_deref() == Some("") {
return Task::none();
}
self.common.error_opt = None;
self.authenticating = true;
self.send_request(Request::PostAuthMessageResponse { response });
@ -1498,6 +1520,9 @@ impl cosmic::Application for App {
if let Some(handle) = self.heartbeat_handle.take() {
handle.abort();
}
if self.dropdown_opt.is_some() {
return self.common.dropdown_blur_rects(true);
}
}
Message::DialogConfirm => match self.dialog_page_opt.take() {
Some(DialogPage::Restart(_)) => {
@ -1524,20 +1549,26 @@ impl cosmic::Application for App {
})
.discard();
}
None => {}
None => {
if self.dropdown_opt.is_some() {
return self.common.dropdown_blur_rects(true);
}
}
},
Message::DropdownToggle(dropdown) => {
if self.dropdown_opt == Some(dropdown) {
self.dropdown_opt = None;
return self.common.dropdown_blur_rects(false);
} else {
self.dropdown_opt = Some(dropdown);
return self.common.dropdown_blur_rects(true);
}
}
Message::KeyboardLayout(layout_i) => {
if layout_i < self.common.active_layouts.len() {
self.common.active_layouts.swap(0, layout_i);
self.set_xkb_config();
if let Some(keyboard_layout) = &self.common.keyboard_layout {
keyboard_layout.set_group(layout_i as u32);
}
self.common.current_keyboard_layout = layout_i;
if self.dropdown_opt == Some(Dropdown::Keyboard) {
self.dropdown_opt = None
}
@ -1583,7 +1614,8 @@ impl cosmic::Application for App {
.abortable();
self.heartbeat_handle = Some(handle);
return heartbeat;
self.common.include_menu = false;
return Task::batch(vec![self.common.dropdown_blur_rects(false), heartbeat]);
}
}
Message::Heartbeat => match self.dialog_page_opt {

View file

@ -0,0 +1,157 @@
use cctk::{
cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1,
keyboard_layout::{KeyboardLayoutHandler, KeyboardLayoutState},
sctk::{
self,
registry::{ProvidesRegistryState, RegistryState},
seat::{Capability, SeatHandler, SeatState},
},
wayland_client::{
Connection, QueueHandle, delegate_noop,
globals::registry_queue_init,
protocol::{wl_keyboard, wl_seat},
},
};
use cosmic::iced;
use cosmic::iced::futures::{SinkExt, channel::mpsc, executor::block_on};
use std::{hash::Hash, thread};
#[derive(Clone, Debug)]
pub enum Event {
KeyboardLayout(ZcosmicKeyboardLayoutV1),
Group(usize),
}
pub fn subscription(connection: Connection) -> iced::Subscription<Event> {
#[derive(Clone)]
struct WaylandSubscription(Connection);
impl Hash for WaylandSubscription {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.backend().display_id().hash(state);
}
}
iced::Subscription::run_with(
WaylandSubscription(connection),
|WaylandSubscription(connection)| {
let connection = connection.clone();
iced::stream::channel(8, move |sender| async move {
thread::spawn(move || thread(connection, sender));
})
},
)
}
struct Keyboard {
seat: wl_seat::WlSeat,
keyboard_layout: ZcosmicKeyboardLayoutV1,
}
impl Drop for Keyboard {
fn drop(&mut self) {
self.keyboard_layout.destroy();
}
}
struct AppData {
seat_state: SeatState,
registry_state: RegistryState,
keyboard_layout_state: KeyboardLayoutState,
keyboard: Option<Keyboard>,
sender: mpsc::Sender<Event>,
}
impl KeyboardLayoutHandler for AppData {
fn group(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &wl_keyboard::WlKeyboard,
_keyboard_layout: &ZcosmicKeyboardLayoutV1,
group: u32,
) {
let _ = block_on(self.sender.send(Event::Group(group as usize)));
}
}
impl ProvidesRegistryState for AppData {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
sctk::registry_handlers![SeatState,];
}
impl SeatHandler for AppData {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard && self.keyboard.is_none() {
let keyboard = seat.get_keyboard(qh, ());
let keyboard_layout = self
.keyboard_layout_state
.get_keyboard_layout(&keyboard, qh);
keyboard.release();
if let Some(keyboard_layout) = keyboard_layout {
let _ = block_on(
self.sender
.send(Event::KeyboardLayout(keyboard_layout.clone())),
);
self.keyboard = Some(Keyboard {
seat,
keyboard_layout,
});
} else {
keyboard.release();
}
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
_capability: Capability,
) {
self.keyboard.take_if(|x| x.seat == seat);
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
self.keyboard.take_if(|x| x.seat == seat);
}
}
fn thread(conn: Connection, sender: mpsc::Sender<Event>) {
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
let qh = event_queue.handle();
let registry_state = RegistryState::new(&globals);
let seat_state = SeatState::new(&globals, &qh);
let keyboard_layout_state = KeyboardLayoutState::new(&registry_state, &qh);
let mut app_data = AppData {
seat_state,
registry_state,
keyboard_layout_state,
keyboard: None,
sender,
};
loop {
event_queue.blocking_dispatch(&mut app_data).unwrap();
}
}
sctk::delegate_registry!(AppData);
sctk::delegate_seat!(AppData);
cctk::delegate_keyboard_layout!(AppData);
delegate_noop!(AppData: ignore wl_keyboard::WlKeyboard);

View file

@ -8,6 +8,8 @@ mod wayland;
mod common;
mod keyboard_layout_wayland;
mod localize;
#[cfg(feature = "logind")]

View file

@ -79,6 +79,8 @@ pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
// We are already the user at this point
user_data.load_config_as_user();
let logind_available = cfg!(feature = "logind") && crate::logind::is_available();
let flags = Flags {
user_icon: user_data
.icon_opt
@ -86,6 +88,7 @@ pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
.map(widget::image::Handle::from_bytes),
user_data,
lockfile_opt: lockfile_opt(),
logind_available,
};
let settings = Settings::default().no_main_window(true);
@ -247,6 +250,7 @@ pub struct Flags {
user_data: UserData,
user_icon: Option<widget::image::Handle>,
lockfile_opt: Option<PathBuf>,
logind_available: bool,
}
///TODO: this is custom code that should be better handled by libcosmic
@ -372,7 +376,7 @@ impl App {
let dropdown_menu = |items: Vec<_>| {
let item_cnt = items.len();
let items = widget::column::with_children(items);
let items = widget::menu::menu_column::MenuColumn::with_children(items);
let items = if item_cnt > 7 {
Element::from(
widget::scrollable(items)
@ -382,25 +386,32 @@ impl App {
Element::from(items)
};
widget::container(items)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).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()
}
}))
.width(Length::Fixed(240.0))
let menu: widget::Container<'_, Message, cosmic::prelude::Theme> =
widget::container(items)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(theme.transparent).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()
}
}))
.width(Length::Fixed(240.0));
if let Some(t) = self.common.rectangle_tracker.as_ref() {
Element::from(t.container((surface_id, true), menu))
} else {
menu.into()
}
};
let mut input_button = widget::popover(
@ -414,7 +425,7 @@ impl App {
for (i, layout) in self.common.active_layouts.iter().enumerate() {
items.push(menu_checklist(
&layout.description,
i == 0,
i == self.common.current_keyboard_layout,
Message::KeyboardLayout(i),
));
}
@ -422,7 +433,7 @@ impl App {
}
//TODO: implement these buttons
let button_row = iced::widget::row![
let mut button_row = iced::widget::row![
/*TODO: greeter accessibility options
widget::button::custom(widget::icon::from_name(
"applications-accessibility-symbolic"
@ -435,16 +446,17 @@ impl App {
widget::text(fl!("keyboard-layout")),
widget::tooltip::Position::Top
),
widget::tooltip(
];
if cfg!(feature = "logind") && self.flags.logind_available {
button_row = button_row.push(widget::tooltip(
widget::button::custom(widget::icon::from_name("system-suspend-symbolic"))
.padding(12.0)
.on_press(Message::Suspend),
widget::text(fl!("suspend")),
widget::tooltip::Position::Top
),
]
.padding([16.0, 0.0, 0.0, 0.0])
.spacing(8.0);
widget::tooltip::Position::Top,
));
}
let button_row = button_row.padding([16.0, 0.0, 0.0, 0.0]).spacing(8.0);
widget::container(iced::widget::column![
date_time_column,
@ -585,30 +597,34 @@ impl App {
.align_x(Alignment::Center)
.width(Length::Fill)
};
let menu = widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
)
.layer(cosmic::cosmic_theme::Layer::Background)
.padding(16)
.class(cosmic::theme::Container::Custom(Box::new(
|theme: &cosmic::Theme| {
// Use background appearance as the base
let mut appearance =
widget::container::Catalog::style(theme, &cosmic::theme::Container::Background);
let c: iced::Color = theme.cosmic().background(theme.transparent).base.into();
appearance.background = Some(iced::Background::Color(c));
appearance.border = iced::Border::default().rounded(16.0);
appearance
},
)))
.width(Length::Fill)
.height(Length::Shrink);
let menu = if let Some(t) = self.common.rectangle_tracker.as_ref() {
Element::from(t.container((surface_id, false), menu))
} else {
menu.into()
};
widget::container(widget::column::with_children(vec![
widget::space::vertical()
.height(Length::FillPortion(1))
.into(),
widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
)
.layer(cosmic::cosmic_theme::Layer::Background)
.padding(16)
.class(cosmic::theme::Container::Custom(Box::new(
|theme: &cosmic::Theme| {
// Use background appearance as the base
let mut appearance = widget::container::Catalog::style(
theme,
&cosmic::theme::Container::Background,
);
appearance.border = iced::Border::default().rounded(16.0);
appearance
},
)))
.width(Length::Fill)
.height(Length::Shrink)
.into(),
menu,
widget::space::vertical()
.height(Length::FillPortion(4))
.into(),
@ -644,7 +660,8 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
core.set_app_type(cosmic::core::AppType::System);
let (mut common, common_task) = Common::init(core);
common.on_output_event = Some(Box::new(|output_event, output| {
Message::OutputEvent(output_event, output)
@ -667,18 +684,18 @@ impl cosmic::Application for App {
authenticating: false,
};
let task = if cfg!(feature = "logind") {
let task = if cfg!(feature = "logind") && app.flags.logind_available {
if already_locked {
// Recover previously locked state
tracing::info!("recovering previous locked state");
app.state = State::Locking;
lock()
} else {
// When logind feature is used, wait for lock signal
// When logind is available, wait for lock signal
Task::none()
}
} else {
// When logind feature not used, lock immediately
// When logind is not available, lock immediately
tracing::info!("locking immediately");
app.state = State::Locking;
lock()
@ -714,6 +731,10 @@ impl cosmic::Application for App {
return Task::none();
}
self.common
.subsurface_outputs
.insert(subsurface_id, output.clone());
let size = if let Some((w, h)) =
output_info_opt.as_ref().and_then(|info| info.logical_size)
{
@ -791,11 +812,11 @@ impl cosmic::Application for App {
);
if matches!(self.state, State::Locked { .. }) {
return Task::batch([get_lock_surface(surface_id, output).chain({
return get_lock_surface(surface_id, output).chain({
cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
))
})]);
});
}
}
OutputEvent::Removed => {
@ -945,11 +966,15 @@ impl cosmic::Application for App {
.zip(self.common.output_names.get(output))
{
let subsurface_id = SurfaceId::unique();
self.common
.subsurface_outputs
.insert(subsurface_id, output.clone());
let surface_id = *surface_id;
self.common.surface_names.insert(surface_id, name.clone());
self.common
.surface_names
.insert(subsurface_id, name.clone());
let msg = cosmic::surface::action::subsurface(
move |_: &mut App| SctkSubsurfaceSettings {
parent: surface_id,
@ -970,7 +995,7 @@ impl cosmic::Application for App {
cosmic::app::Action::Surface(msg),
)));
} else {
tracing::error!("no rectangle for subsurface...");
tracing::error!("no rectangle for subsurface creation...");
}
}
return Task::batch(commands);
@ -985,11 +1010,11 @@ impl cosmic::Application for App {
self.common.window_size.remove(surface_id);
commands.push(destroy_lock_surface(*surface_id));
}
if cfg!(feature = "logind") {
if cfg!(feature = "logind") && self.flags.logind_available {
return Task::batch(commands);
// When using logind feature, stick around for more lock signals
// When logind is available, stick around for more lock signals
} else {
// When not using logind feature, exit immediately after unlocking
// When logind is not available, exit immediately after unlocking
//TODO: cleaner method to exit?
process::exit(0);
}
@ -1009,8 +1034,10 @@ impl cosmic::Application for App {
Message::DropdownToggle(dropdown) => {
if self.dropdown_opt == Some(dropdown) {
self.dropdown_opt = None;
return self.common.dropdown_blur_rects(false);
} else {
self.dropdown_opt = Some(dropdown);
return self.common.dropdown_blur_rects(true);
}
}
Message::Inhibit(inhibit) => match self.state {
@ -1022,15 +1049,19 @@ impl cosmic::Application for App {
}
},
Message::KeyboardLayout(layout_i) => {
if layout_i < self.common.active_layouts.len() {
self.common.active_layouts.swap(0, layout_i);
self.common.set_xkb_config(&self.flags.user_data);
if let Some(keyboard_layout) = &self.common.keyboard_layout {
keyboard_layout.set_group(layout_i as u32);
}
self.common.current_keyboard_layout = layout_i;
if self.dropdown_opt == Some(Dropdown::Keyboard) {
self.dropdown_opt = None
self.dropdown_opt = None;
return self.common.dropdown_blur_rects(false);
}
}
Message::Submit(value) => {
if value.is_empty() {
return Task::none();
}
self.common.error_opt = None;
self.authenticating = true;
match self.value_tx_opt.take() {
@ -1189,8 +1220,7 @@ impl cosmic::Application for App {
}),
);
#[cfg(feature = "logind")]
{
if cfg!(feature = "logind") && self.flags.logind_available {
subscriptions.push(crate::logind::subscription());
}

View file

@ -13,6 +13,27 @@ use zbus::Connection;
use crate::common;
use crate::locker::Message;
/// Checks whether logind (either elogind or systemd-logind) is reachable on the system bus.
pub fn is_available() -> bool {
fn ping() -> zbus::Result<()> {
let connection = zbus::blocking::Connection::system()?;
connection.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.DBus.Peer"),
"Ping",
&(),
)?;
Ok(())
}
if let Err(err) = ping() {
tracing::info!("logind not available: {}", err);
false
} else {
true
}
}
pub async fn power_off() -> zbus::Result<()> {
let connection = Connection::system().await?;
let manager = ManagerProxy::new(&connection).await?;

View file

@ -28,6 +28,9 @@ fn main() -> Result<(), Box<dyn Error>> {
}
}
// Enable jpeg-xl support for image-rs.
let _ = jxl_oxide::integration::register_image_decoding_hook();
match pwd::Passwd::current_user() {
Some(current_user) => match current_user.name.as_str() {
"cosmic-greeter" => greeter::main(),