Compare commits

..

44 commits

Author SHA1 Message Date
57bc2dc3dc chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
7ac6ece2ba fix: cosmic-osk-config depuis le checkout local ../cosmic-osk
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
647332f1a4 merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2026-09-24 20:48:00 +02:00
Ashley Wulber
b87331f4eb fix: for exclusive keyboard interactivity layer must be >= Top
moves osk to overlay
2026-09-22 17:10:59 -06:00
Jeremy Soller
4c1e532b69 Reduce keyboard layout max width 2026-09-22 17:10:59 -06:00
Jeremy Soller
04d61e30c5 Move greeter surface to bottom to allow OSK to show on top 2026-09-22 17:10:59 -06:00
Jeremy Soller
ce726d8470 OSK integration 2026-09-22 17:10:59 -06:00
Jeremy Soller
db39ee661d Remove zed settings and use stable rustfmt 2026-09-22 17:10:59 -06:00
Nara Díaz Viñolas
bbf553e284 fix: compilation error on greeter bin 2026-09-21 22:55:38 +02:00
Nara Díaz Viñolas
b9587deadb chore: put systemd-homed extra support behind 'systemd' feature 2026-09-21 22:55:38 +02:00
Nara Díaz Viñolas
8ff96e94f7 feat: load user icon and background from systemd-homed paths as a
fallback
2026-09-21 22:55:38 +02:00
Nara Díaz Viñolas
63c2812959 fix: add systemd-homed reported uids to user filter 2026-09-21 22:55:38 +02:00
Ashley Wulber
0b0d2925ff chore: blur updates 2026-09-16 11:12:18 -06:00
Michael Murphy
bb5c7e12a4
i18n: translation update from Hosted Weblate (#529)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Greeter](https://hosted.weblate.org/projects/pop-os/cosmic-greeter/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-greeter/matrix-auto.svg)
2026-09-15 21:55:52 +02:00
Hosted Weblate
31ca2e67b3
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kurt Harren Catindig <kurtharrencatindig03@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Satakun Utama <galaxytomostore@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/th/
Translation: Pop OS/COSMIC Greeter
2026-09-12 13:51:38 +02:00
Ian Douglas Scott
06a762caca locker: Rely on cosmic-bg for showing background
Requires https://github.com/pop-os/cosmic-comp/pull/2792 and
https://github.com/pop-os/cosmic-bg/pull/144.
2026-09-11 14:38:19 -06:00
Hojjat
601e16af0e chore: adapt to the changes in libcosmic 2026-09-11 21:49:05 +02:00
672e07b4ad Merge tag 'epoch-1.8.0' into local/yoda-main
Epoch 1.8.0
2026-09-10 17:56:29 +02:00
Jeremy Soller
ff31b59b7d
Fix error when compiling without logind feature (#536)
`crate::logind::is_available()` function was used despite `logind`
module being gated behind `logind` feature, `cfg!(feature = "logind")`
was used wrongly here as it doesn't remove surrounding code from
compilation.

Additionally initialized nix development shell and a nix package with
support for flakes and legacy nix, let me know if it's too complicated
for this repo.

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-09-09 10:01:53 -06:00
r0chd
14cc125fcd
fix: compile without the logind feature 2026-09-09 10:10:52 +02:00
r0chd
499f13a634
chore: add nix 2026-09-09 10:10:52 +02:00
Frederic Laing
0a16d339a4 fix: remove unused Color import 2026-09-01 15:10:56 -06:00
demi
cde1a13140
fix: handle sequential PAM prompts in greeter and locker 2026-09-01 17:04:46 +02:00
4c995b533c chore: fige Cargo.lock tel que resolu apres le merge 1.7.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 07:58:50 +02:00
117990202a Merge tag 'epoch-1.7.0' into local/yoda-main
Epoch 1.7.0
2026-08-27 07:49:24 +02:00
chris-010
935891e36b fix(greeter): do not cancel the session on a non-fatal PAM error message
greetd delivers PAM_ERROR_MSG as an auth message of type Error, and ipc.rs
turned that into Message::Error, which cancels the greetd session. But such a
message describes the current attempt, not the end of the conversation:
pam_fprintd reports "Failed to match fingerprint" this way and then retries
until max-tries is reached.

The consequence at the login screen is that a single failed fingerprint press
ends the conversation. The greeter reconnects and immediately creates a new
session, whose PAM worker calls fprintd's Claim 14 ms later while the previous
worker still holds it, so the claim is denied and no fingerprint prompt ever
appears again. The previous worker only exits when it next tries to talk to the
greeter, which can be arbitrarily far in the future, so delaying the new session
does not help either.

Per the greetd protocol every auth message has to be acknowledged with
PostAuthMessageResponse, and for a non-interactive one the response is None,
which is what the Info arm already does. Route the Error message to its own
Message::AuthError that shows the text and acknowledges it. Message::Error and
the Response::Error path stay as they are, so a failed authentication still
cancels the session.

Drafted with AI assistance (Claude Code); the change and the measurements behind
it were reviewed and tested by me on real hardware.

Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com>
2026-08-25 02:33:15 +02:00
Michael Murphy
a133abe686
i18n: translation update from Hosted Weblate (#518)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Greeter](https://hosted.weblate.org/projects/pop-os/cosmic-greeter/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-greeter/matrix-auto.svg)
2026-08-24 20:51:07 +02:00
Hosted Weblate
c35a64e704
i18n: translation updates from weblate
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-greeter/da/
Translation: Pop OS/COSMIC Greeter
2026-08-22 19:35:58 +02:00
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
36 changed files with 2636 additions and 1282 deletions

View file

@ -1,15 +0,0 @@
{
"format_on_save": "on",
"lsp": {
"rust-analyzer": {
"initialization_options": {
"check": {
"command": "clippy",
},
"rustfmt": {
"extraArgs": ["+nightly"],
},
},
},
},
}

2356
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ cosmic-comp-config.workspace = true
cosmic-config = { workspace = true, features = ["calloop", "macro"] }
cosmic-greeter-config.workspace = true
cosmic-greeter-daemon = { path = "daemon" }
cosmic-osk-config = { git = "https://github.com/pop-os/cosmic-osk" }
dirs = "6"
cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [
"advanced-shaping",
@ -80,12 +81,16 @@ 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"] }
enumflags2 = "0.7.12"
[dependencies.greetd_ipc]
version = "0.10.3"
@ -168,8 +173,8 @@ iced_widget = { path = "../libcosmic/iced/widget" }
iced_winit = { path = "../libcosmic/iced/winit" }
[patch."https://github.com/pop-os/cosmic-protocols"]
cosmic-protocols = { path = "../cosmic-protocols" }
cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" }
cosmic-protocols = { path = "../cosmic-protocols" }
[patch."https://github.com/pop-os/cosmic-settings-daemon"]
cosmic-settings-config = { path = "../cosmic-settings-daemon/config" }
@ -184,3 +189,7 @@ dnd = { path = "../window_clipboard/dnd" }
mime = { path = "../window_clipboard/mime" }
clipboard_wayland = { path = "../window_clipboard/wayland" }
clipboard_macos = { path = "../window_clipboard/macos" }
# Yoda: cosmic-osk-config depuis le checkout local (meme source que le paquet cosmic-osk).
[patch."https://github.com/pop-os/cosmic-osk"]
cosmic-osk-config = { path = "../cosmic-osk/config" }

View file

@ -28,7 +28,8 @@ whitespace-conf = "1"
#TODO: reduce features
tokio = { workspace = true, features = ["full"] }
xdg = "3.0"
zbus_systemd = { version = "0.26000.0", features = ["home1"], optional = true }
[features]
default = ["systemd"]
systemd = ["tracing-journald"]
systemd = ["tracing-journald", "zbus_systemd"]

View file

@ -1,11 +1,12 @@
use cosmic_comp_config::output::randr;
use cosmic_config::CosmicConfigEntry;
use kdl::KdlDocument;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use zbus::Connection;
pub use cosmic_applets_config::time::TimeAppletConfig;
pub use cosmic_bg_config::state::State as BgState;
@ -16,12 +17,41 @@ pub use cosmic_theme::{Theme, ThemeBuilder};
pub struct UserFilter {
uid_min: u32,
uid_max: u32,
homed_uids: BTreeSet<u32>,
}
impl Default for UserFilter {
fn default() -> Self {
impl UserFilter {
#[cfg(feature = "systemd")]
async fn get_homed_uids() -> Result<BTreeSet<u32>, zbus::Error> {
use zbus_systemd::home1;
let connection = Connection::system().await?;
let homed = home1::ManagerProxy::new(&connection).await?;
let homed_uids = homed
.list_homes()
.await?
.iter()
.map(|(_, uid, ..)| *uid)
.collect();
Ok(homed_uids)
}
pub async fn new() -> Self {
let login_defs_data = fs::read_to_string("/etc/login.defs").unwrap_or_default();
let login_defs = whitespace_conf::parse(&login_defs_data);
#[cfg(feature = "systemd")]
let homed_uids = Self::get_homed_uids()
.await
.inspect_err(|e| {
tracing::warn!("failed to list dynamic UIDs from systemd-homed: {e:?}")
})
.unwrap_or_default();
#[cfg(not(feature = "systemd"))]
let homed_uids = BTreeSet::new();
Self {
uid_min: login_defs
.get("UID_MIN")
@ -31,17 +61,14 @@ impl Default for UserFilter {
.get("UID_MAX")
.and_then(|x| x.parse::<u32>().ok())
.unwrap_or(65000),
homed_uids,
}
}
}
impl UserFilter {
pub fn new() -> Self {
Self::default()
}
pub fn filter(&self, user: &pwd::Passwd) -> bool {
if user.uid < self.uid_min || user.uid > self.uid_max {
if (user.uid < self.uid_min || user.uid > self.uid_max)
&& !self.homed_uids.contains(&user.uid)
{
// Skip system accounts
return false;
}
@ -101,6 +128,48 @@ impl UserData {
}
}
fn load_icon_as_user(&mut self) {
//TODO: use accountsservice?
let icon_paths = [
//IMPORTANT: This file is owned by root and safe to read (it won't be a link to /etc/shadow for example)
// It may not exist if the user uses one of the system icons. In that case, we should read the
// information in /var/lib/AccountsService/users, and then read the icon path as the user
Path::new("/var/lib/AccountsService/icons").join(&self.name),
// systemd-homed cache
Path::new("/var/cache/systemd/home")
.join(&self.name)
.join("avatar"),
];
for icon_path in icon_paths {
match fs::OpenOptions::new()
.read(true)
// Do not follow symlinks
.custom_flags(libc::O_NOFOLLOW)
.open(&icon_path)
{
Ok(mut icon_file) => {
let mut icon_data = Vec::new();
match icon_file.read_to_end(&mut icon_data) {
Ok(count) => {
icon_data.truncate(count);
self.icon_opt = Some(icon_data);
return;
}
Err(err) => {
tracing::error!("failed to read icon data {:?}: {:?}", icon_path, err);
}
}
}
Err(err) => {
tracing::warn!("failed to open icon {:?}: {:?}", icon_path, err);
}
}
}
tracing::error!("failed to load icon for user {:?}", self.name)
}
pub fn load_config_as_user(&mut self) {
self.icon_opt = None;
self.theme_opt = None;
@ -109,33 +178,7 @@ impl UserData {
self.xkb_config_opt = None;
self.time_applet_config = Default::default();
//TODO: use accountsservice?
//IMPORTANT: This file is owned by root and safe to read (it won't be a link to /etc/shadow for example)
// It may not exist if the user uses one of the system icons. In that case, we should read the
// information in /var/lib/AccountsService/users, and then read the icon path as the user
let icon_path = Path::new("/var/lib/AccountsService/icons").join(&self.name);
match fs::OpenOptions::new()
.read(true)
// Do not follow symlinks
.custom_flags(libc::O_NOFOLLOW)
.open(&icon_path)
{
Ok(mut icon_file) => {
let mut icon_data = Vec::new();
match icon_file.read_to_end(&mut icon_data) {
Ok(count) => {
icon_data.truncate(count);
self.icon_opt = Some(icon_data);
}
Err(err) => {
tracing::error!("failed to read icon data {:?}: {:?}", icon_path, err);
}
}
}
Err(err) => {
tracing::error!("failed to open icon {:?}: {:?}", icon_path, err);
}
}
self.load_icon_as_user();
let mut is_dark = true;
match cosmic_theme::ThemeMode::config() {
@ -209,6 +252,16 @@ impl UserData {
tracing::error!("failed to create cosmic-bg state helper: {:?}", err);
}
}
if self.bg_state.wallpapers.is_empty() {
self.bg_state.wallpapers.push((
String::new(),
BgSource::Path(
Path::new("/var/cache/systemd/home")
.join(&self.name)
.join("login-background"),
),
));
}
self.load_wallpapers_as_user();
match cosmic_config::Config::new("com.system76.CosmicComp", CosmicCompConfig::VERSION) {

View file

@ -70,8 +70,8 @@ struct GreeterProxy;
#[zbus::interface(name = "com.system76.CosmicGreeter")]
impl GreeterProxy {
fn get_user_data(&mut self) -> Result<String, GreeterError> {
let user_filter = UserFilter::new();
async fn get_user_data(&mut self) -> Result<String, GreeterError> {
let user_filter = UserFilter::new().await;
// The pwd::Passwd method is unsafe (but not labelled as such) due to using global state (libc pwent functions).
// To prevent issues, this should only be called once in the entire process space at a time

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,

37
default.nix Normal file
View file

@ -0,0 +1,37 @@
{
rust-overlay ? import (import ./nix/flake-parse.nix "rust-overlay"),
pkgs ? import (import ./nix/flake-parse.nix "nixpkgs") {
system = builtins.currentSystem;
overlays = [ rust-overlay ];
},
}:
let
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-analyzer"
"rust-src"
"llvm-tools"
];
};
in
{
shell = import ./nix/shell.nix {
inherit pkgs rustToolchain;
};
formatter = import ./nix/formatter.nix { inherit pkgs rustToolchain; };
packages =
let
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
in
rec {
default = pkgs.callPackage ./nix/package.nix {
inherit rustPlatform;
};
sessiond-seat = default;
};
}

45
flake.lock generated Normal file
View file

@ -0,0 +1,45 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1788881743,
"narHash": "sha256-151taSq/21cxagiIu7hyMVl68+FbHfwBP4lh6TB6KOM=",
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1069856.d6524aaca2ff/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1788851328,
"narHash": "sha256-YOwlUD0Lt/3SulKhZ7z0Jmgbq/DWh8SjnGwUlFSw+YM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "6ae57a71bcb0bebc7a66cc2bd76942c4cf649167",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

84
flake.nix Normal file
View file

@ -0,0 +1,84 @@
{
inputs = {
nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
rust-overlay,
...
}:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
pkgsFor =
system:
import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (pkgsFor system));
in
{
devShells = forAllSystems (pkgs: {
default =
let
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-analyzer"
"rust-src"
"llvm-tools"
];
};
in
import ./nix/shell.nix { inherit pkgs rustToolchain; };
});
packages = forAllSystems (
pkgs:
let
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-analyzer"
"rust-src"
"llvm-tools"
];
};
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
in
{
cosmic-greeter = pkgs.callPackage ./nix/package.nix {
inherit rustPlatform;
};
default = self.packages.${pkgs.stdenv.hostPlatform.system}.cosmic-greeter;
}
);
formatter = forAllSystems (
pkgs:
let
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-analyzer"
"rust-src"
"llvm-tools"
];
};
in
import ./nix/formatter.nix { inherit pkgs rustToolchain; }
);
};
}

View file

@ -2,7 +2,7 @@ accessibility = Přístupnost
.screen-reader = Čtečka obrazovky
.magnifier = Lupa
.high-contrast = Vysoký kontrast
.invert-colors = Převrátit barvy
.invert-colors = Inverze barev
cancel = Zrušit
caps-lock = Caps Lock je aktivní.
enter-user = Zadat jméno ručně...

View file

@ -0,0 +1,37 @@
cancel = Afbryd
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

@ -9,6 +9,7 @@ caps-lock = Caps Lock is active.
enter-user = Enter name manually...
type-username = Username:
keyboard-layout = Keyboard layout
on-screen-keyboard = On-screen keyboard
restart = Restart
restart-now = Restart now?
restart-timeout = The system will restart automatically

View file

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

View file

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

@ -1,4 +1,4 @@
cancel = Sefsex
cancel = Semmet
restart-timeout =
Anagraw ad yales tanekra s wudem awurman
{ $seconds ->

View file

@ -0,0 +1,17 @@
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.
}
cancel = Anullar
user = Utilizaire

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

@ -2,10 +2,10 @@ cancel = ยกเลิก
keyboard-layout = ผังคีย์บอร์ด
restart = รีสตาร์ท
restart-now = รีสตาร์ทตอนนี้เลยหรือไม่
restart-timeout = ระบบจะรีสตาร์ทโดยอัตโนมัติภายใน {$seconds} วินาที
restart-timeout = ระบบจะรีสตาร์ทโดยอัตโนมัติภายใน { $seconds } วินาที
session = เซสชั่น
shutdown = ปิดระบบ
shutdown-now = ปิดระบบตอนนี้เลยหรือไม่
shutdown-timeout = ระบบจะปิดลงโดยอัตโนมัติภายใน {$seconds} วินาที
shutdown-timeout = ระบบจะปิดลงโดยอัตโนมัติภายใน { $seconds } วินาที
suspend = ระงับ
user = ผู้ใช้

12
nix/flake-parse.nix Normal file
View file

@ -0,0 +1,12 @@
inputName:
let
lock = builtins.fromJSON (builtins.readFile ../flake.lock);
namedNode = lock.nodes.${lock.nodes.root.inputs.${inputName}}.locked;
githubTarball = fetchTarball {
url =
namedNode.url
or "https://github.com/${namedNode.owner}/${namedNode.repo}/archive/${namedNode.rev}.tar.gz";
sha256 = namedNode.narHash;
};
in
githubTarball

17
nix/formatter.nix Normal file
View file

@ -0,0 +1,17 @@
{ pkgs, rustToolchain }:
pkgs.writeShellApplication {
name = "nix3-fmt-wrapper";
runtimeInputs = builtins.attrValues {
inherit (pkgs)
nixfmt
taplo
fd
;
inherit rustToolchain;
};
text = ''
fd "$@" -t f -e nix -x nixfmt -q '{}'
fd "$@" -t f -e toml -x taplo format '{}'
cargo fmt
'';
}

108
nix/package.nix Normal file
View file

@ -0,0 +1,108 @@
{
lib,
stdenv,
fetchFromGitHub,
rustPlatform,
libcosmicAppHook,
cmake,
just,
cosmic-randr,
dav1d,
libinput,
linux-pam,
udev,
coreutils,
xkeyboard_config,
nixosTests,
orca,
withLogind ? true,
withUpower ? true,
withNetworkManager ? true,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "cosmic-greeter";
version = "1.6.0";
src = fetchFromGitHub {
owner = "pop-os";
repo = "cosmic-greeter";
tag = "epoch-${finalAttrs.version}";
hash = "sha256-hDVdl2+7NVLA+YxO2HToni57IEr0i4OGTsYDc3YGTuw=";
};
postPatch = ''
substituteInPlace src/greeter.rs --replace-fail '/usr/bin/env' '${lib.getExe' coreutils "env"}'
substituteInPlace src/greeter.rs --replace-fail '/usr/bin/orca' '${lib.getExe orca}'
'';
cargoHash = "sha256-vHR9go8/iVUT7oBV8h+mmBvhi2oSKNBKtV0uoDOr6go=";
buildNoDefaultFeatures = true;
buildFeatures =
lib.optionals withLogind [ "logind" ]
++ lib.optionals withUpower [ "upower" ]
++ lib.optionals withNetworkManager [ "networkmanager" ];
separateDebugInfo = true;
__structuredAttrs = true;
env.VERGEN_GIT_SHA = finalAttrs.src.tag;
nativeBuildInputs = [
rustPlatform.bindgenHook
cmake
just
libcosmicAppHook
];
buildInputs = [
cosmic-randr
dav1d
libinput
linux-pam
udev
orca
];
dontUseJustBuild = true;
dontUseJustCheck = true;
justFlags = [
"--set"
"prefix"
(placeholder "out")
"--set"
"cargo-target-dir"
"target/${stdenv.hostPlatform.rust.cargoShortTarget}"
];
preFixup = ''
libcosmicAppWrapperArgs+=(
--prefix PATH : ${lib.makeBinPath [ cosmic-randr ]}
--set-default X11_BASE_RULES_XML ${xkeyboard_config}/share/X11/xkb/rules/base.xml
--set-default X11_BASE_EXTRA_RULES_XML ${xkeyboard_config}/share/X11/xkb/rules/extra.xml
)
'';
passthru = {
tests = {
inherit (nixosTests)
cosmic
cosmic-autologin
cosmic-noxwayland
cosmic-autologin-noxwayland
;
};
};
meta = {
homepage = "https://github.com/pop-os/cosmic-greeter";
description = "Greeter for the COSMIC Desktop Environment";
mainProgram = "cosmic-greeter";
license = lib.licenses.gpl3Only;
teams = [ lib.teams.cosmic ];
platforms = lib.platforms.linux;
};
})

31
nix/shell.nix Normal file
View file

@ -0,0 +1,31 @@
{ pkgs, rustToolchain }:
pkgs.mkShell {
name = "cosmic-greeter-devshell";
packages = builtins.attrValues {
inherit
rustToolchain
;
inherit (pkgs)
rust-analyzer-unwrapped
nixd
libxkbcommon
dav1d
libinput
linux-pam
gcc
glibc
cmake
;
inherit (pkgs.llvmPackages)
libclang
llvm
;
};
nativeBuildInputs = [ pkgs.pkg-config ];
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
LLVM_CONFIG_PATH = "${pkgs.llvmPackages.llvm}/bin/llvm-config";
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linux-pam}/include -I${pkgs.glibc.dev}/include";
}

View file

@ -1 +0,0 @@
imports_granularity = "Module"

View file

@ -3,15 +3,24 @@ 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_config::CosmicConfigEntry;
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 MAX_WIDTH: f32 = 866.;
pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.;
#[derive(Debug, Clone, Eq, PartialEq)]
@ -21,14 +30,28 @@ pub struct ActiveLayout {
pub variant: String,
}
impl ActiveLayout {
pub fn name(&self) -> String {
let mut name = self.layout.clone();
if !self.variant.is_empty() {
name.push_str(" (");
name.push_str(&self.variant);
name.push_str(")");
}
name
}
}
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 +62,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 +81,16 @@ pub enum Message {
Focus(SurfaceId),
Key(Modifiers, Key, Option<SmolStr>),
NetworkIcon(Option<&'static str>),
OnScreenKeyboard,
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 +103,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 +112,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 +140,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 +154,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) {
@ -286,7 +291,32 @@ impl<M: From<Message> + Send + 'static> Common<M> {
self.network_icon_opt =
network_icon_opt.map(|name| widget::icon::from_name(name).into());
}
Message::OnScreenKeyboard => {
use cosmic_osk_config::Config;
match Config::handler() {
Ok(handler) => {
let mut config = match Config::get_entry(&handler) {
Ok(config) => config,
Err((errs, config)) => {
tracing::warn!("failed to parse OSK config: {:?}", errs);
config
}
};
if let Err(err) = config.set_always_shown(&handler, !config.always_shown) {
tracing::error!("failed to set OSK always_shown config: {}", err);
}
}
Err(err) => {
tracing::error!("failed to create OSK config handler: {}", err);
}
}
}
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 +352,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 +456,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

@ -11,6 +11,7 @@ use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity
use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::cosmic_theme::{self, CosmicPalette};
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::event::listen_with;
use cosmic::iced::event::wayland::OutputEvent;
use cosmic::iced::futures::SinkExt;
@ -18,14 +19,15 @@ use cosmic::iced::platform_specific::runtime::wayland::layer_surface::{
IcedMargin, IcedOutput, SctkLayerSurfaceSettings,
};
use cosmic::iced::platform_specific::shell::wayland::commands::layer_surface::{
Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface,
Anchor, KeyboardInteractivity, Layer, destroy_layer_surface,
};
use cosmic::iced::platform_specific::shell::wayland::commands::subsurface::reposition_subsurface;
use cosmic::iced::runtime::core::window::Id as SurfaceId;
use cosmic::iced::runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
use cosmic::iced::{
self, Alignment, Background, Border, Length, Point, Size, Subscription, window,
self, Alignment, Background, Border, Length, Point, Rectangle, Size, Subscription, window,
};
use cosmic::surface::action::{LiveSettings, app_layer_shell};
use cosmic::widget::{id_container, text};
use cosmic::{Element, executor, surface, theme, widget};
use cosmic_greeter_config::Config as CosmicGreeterConfig;
@ -40,7 +42,7 @@ use std::num::NonZeroU32;
use std::process::Stdio;
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use std::{fs, io, process};
use std::{fs, io, process, thread};
use tokio::process::Child;
use tokio::time;
use tracing::metadata::LevelFilter;
@ -51,7 +53,7 @@ use wayland_client::Proxy;
use wayland_client::protocol::wl_output::WlOutput;
use zbus::{Connection, proxy};
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT};
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT, MAX_WIDTH};
use crate::fl;
static USERNAME_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("username-id"));
@ -76,8 +78,8 @@ async fn user_data_dbus() -> Result<Vec<UserData>, Box<dyn Error>> {
Ok(user_datas)
}
fn user_data_fallback() -> Vec<UserData> {
let user_filter = UserFilter::new();
async fn user_data_fallback() -> Vec<UserData> {
let user_filter = UserFilter::new().await;
// The pwd::Passwd method is unsafe (but not labelled as such) due to using global state (libc pwent functions).
/* unsafe */
@ -129,7 +131,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
Ok(ok) => ok,
Err(err) => {
tracing::error!("failed to load user data from daemon: {}", err);
user_data_fallback()
runtime.block_on(user_data_fallback())
}
};
@ -279,6 +281,22 @@ pub fn main() -> Result<(), Box<dyn Error>> {
sessions
};
match process::Command::new("cosmic-osk")
.arg("overlay")
.env("RUST_LOG", "cosmic_osk=info")
.spawn()
{
Ok(mut child) => {
thread::spawn(move || match child.wait() {
Ok(status) => tracing::warn!("cosmic-osk exited: {}", status),
Err(err) => tracing::error!("failed to wait on cosmic-osk: {}", err),
});
}
Err(err) => {
tracing::error!("failed to spawn cosmic-osk: {}", err);
}
}
let flags = Flags {
user_icons: user_datas
.iter_mut()
@ -288,6 +306,10 @@ pub fn main() -> Result<(), Box<dyn Error>> {
sessions,
greeter_config,
greeter_config_handler,
#[cfg(feature = "logind")]
logind_available: crate::logind::is_available(),
#[cfg(not(feature = "logind"))]
logind_available: false,
};
let settings = Settings::default().no_main_window(true);
@ -304,6 +326,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)]
@ -358,6 +381,9 @@ pub enum Message {
Common(common::Message),
OutputEvent(OutputEvent, WlOutput),
Auth(Option<String>),
/// Non-fatal error message from PAM (`PAM_ERROR_MSG`), reported by greetd as an auth
/// message of type `Error`. Unlike [`Message::Error`] the conversation stays alive.
AuthError(String),
ConfigUpdateUser,
DialogCancel,
DialogConfirm,
@ -381,7 +407,7 @@ pub enum Message {
Session(String),
Shutdown,
Socket(SocketState),
Surface(surface::Action),
Surface(surface::Action<Message>),
Suspend,
Username(String),
EnterUser(bool, String),
@ -473,9 +499,9 @@ impl App {
.window_size
.get(&id)
.map(|s| s.width)
.unwrap_or(800.);
let menu_width = if window_width > 800. {
800.
.unwrap_or(MAX_WIDTH);
let menu_width = if window_width > MAX_WIDTH {
MAX_WIDTH
} else {
window_width
};
@ -537,7 +563,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 +573,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,13 +591,29 @@ 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(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
widget::container(
widget::button::custom(
widget::text(
self.common.active_layouts[self.common.current_keyboard_layout].name(),
)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.height(16)
.center(),
)
.padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Keyboard)),
)
.max_width(80),
)
.position(widget::popover::Position::Bottom);
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
@ -579,7 +621,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,12 +723,19 @@ 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")),
widget::tooltip::Position::Top
),
widget::tooltip(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::Common(common::Message::OnScreenKeyboard)),
text(fl!("on-screen-keyboard")),
widget::tooltip::Position::Top,
),
widget::tooltip(
input_button,
text(fl!("keyboard-layout")),
@ -702,30 +751,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 +979,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(MAX_WIDTH));
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 +1018,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 +1073,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 +1087,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 +1134,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| {
@ -1171,6 +1216,12 @@ impl cosmic::Application for App {
fn update(&mut self, message: Self::Message) -> Task<Message> {
match message {
Message::Common(common_message) => {
if matches!(&common_message, common::Message::Prompt(_, _, Some(_)))
&& self.authenticating
{
self.authenticating = false;
self.common.prompt_opt = None;
}
// In greetd's IPC protocol, the greeter must acknowledge auth messages by
// sending PostAuthMessageResponse. For non-interactive "info" messages
// (fingerprint prompts typically come through here), the correct response
@ -1236,10 +1287,10 @@ impl cosmic::Application for App {
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 {
let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
(
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.),
Point::new((unwrapped_size.0 as f32 - MAX_WIDTH) / 2., 32.),
Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
)
} else {
(
@ -1251,7 +1302,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,
@ -1270,27 +1326,41 @@ impl cosmic::Application for App {
);
return Task::batch([
self.update_user_data(),
get_layer_surface(SctkLayerSurfaceSettings {
id: surface_id,
layer: Layer::Overlay,
keyboard_interactivity: KeyboardInteractivity::Exclusive,
input_zone: None,
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,
cosmic::surface::surface_task(app_layer_shell(
|_: &App| LiveSettings {
padding: None,
corners: None,
blur: Some(false),
},
exclusive_zone: -1,
size_limits: iced::Limits::NONE.min_width(1.0).min_height(1.0),
}),
cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
move |_: &mut App| {
let output = output.clone();
SctkLayerSurfaceSettings {
id: surface_id,
layer: Layer::Top,
keyboard_interactivity: KeyboardInteractivity::Exclusive,
input_zone: None,
anchor: Anchor::TOP
| Anchor::LEFT
| Anchor::BOTTOM
| Anchor::RIGHT,
output: IcedOutput::Output(output),
namespace: "cosmic-greeter".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),
}
},
None,
)),
cosmic::task::message(cosmic::Action::Surface(msg)),
]);
}
OutputEvent::Removed => {
@ -1330,6 +1400,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 +1417,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 +1458,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,10 +1544,20 @@ 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 });
}
Message::AuthError(error) => {
// The conversation continues, so acknowledge like any other
// non-interactive auth message rather than cancelling the session.
self.common.error_opt = Some(error);
self.authenticating = false;
self.send_request(Request::PostAuthMessageResponse { response: None });
}
Message::Login => {
self.common.prompt_opt = None;
self.common.error_opt = None;
@ -1498,6 +1585,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 +1614,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 +1679,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 {
@ -1611,9 +1708,7 @@ impl cosmic::Application for App {
self.greetd_sender = Some(sender);
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
return cosmic::task::message(cosmic::Action::Surface(a));
}
Message::ScreenReader(enabled) => {
if enabled
@ -1811,8 +1906,8 @@ impl cosmic::Application for App {
tracing::error!("Failed to find subsurface menu id");
return Task::none();
};
let loc = if size.width > 800. {
Point::new(size.width / 2. - 400., 32.)
let loc = if size.width > MAX_WIDTH {
Point::new((size.width - MAX_WIDTH) / 2., 32.)
} else {
Point::new(0., 32.)
};

View file

@ -115,7 +115,8 @@ pub fn subscription() -> Subscription<Message> {
.await;
}
greetd_ipc::AuthMessageType::Error => {
_ = sender.send(Message::Error(auth_message)).await;
// PAM_ERROR_MSG: a failed attempt, not a dead session.
_ = sender.send(Message::AuthError(auth_message)).await;
}
},
greetd_ipc::Response::Error {

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

@ -4,16 +4,18 @@
use color_eyre::eyre::WrapErr;
use cosmic::app::{Core, Settings, Task};
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::event::wayland::{OutputEvent, SessionLockEvent};
use cosmic::iced::futures::{self, SinkExt};
use cosmic::iced::platform_specific::shell::wayland::commands::session_lock::{
destroy_lock_surface, get_lock_surface, lock, unlock,
destroy_lock_surface, lock, unlock,
};
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, Rectangle, Size, Subscription,
};
use cosmic::surface::action::LiveSettings;
use cosmic::{Element, executor, surface, theme, widget};
use cosmic_config::CosmicConfigEntry;
use cosmic_greeter_daemon::{TimeAppletConfig, UserData};
@ -33,7 +35,7 @@ use tracing_subscriber::{EnvFilter, fmt};
use wayland_client::Proxy;
use wayland_client::protocol::wl_output::WlOutput;
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT};
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT, MAX_WIDTH};
use crate::fl;
fn lockfile_opt() -> Option<PathBuf> {
@ -86,6 +88,10 @@ pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
.map(widget::image::Handle::from_bytes),
user_data,
lockfile_opt: lockfile_opt(),
#[cfg(feature = "logind")]
logind_available: crate::logind::is_available(),
#[cfg(not(feature = "logind"))]
logind_available: false,
};
let settings = Settings::default().no_main_window(true);
@ -247,6 +253,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
@ -268,7 +275,7 @@ pub enum Message {
KeyboardLayout(usize),
Inhibit(Arc<OwnedFd>),
Submit(String),
Surface(surface::Action),
Surface(surface::Action<Message>),
Suspend,
TimeAppletConfig(TimeAppletConfig),
Error(String),
@ -320,9 +327,9 @@ impl App {
.window_size
.get(&surface_id)
.map(|s| s.width)
.unwrap_or(800.);
let menu_width = if window_width > 800. {
800.
.unwrap_or(MAX_WIDTH);
let menu_width = if window_width > MAX_WIDTH {
MAX_WIDTH
} else {
window_width
};
@ -372,7 +379,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,31 +389,48 @@ 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(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
widget::container(
widget::button::custom(
widget::text(
self.common.active_layouts[self.common.current_keyboard_layout].name(),
)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.height(16)
.center(),
)
.padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Keyboard)),
)
.max_width(80),
)
.position(widget::popover::Position::Bottom);
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
@ -414,7 +438,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 +446,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"
@ -430,21 +454,29 @@ impl App {
.padding(12.0)
.on_press(Message::None),
*/
widget::tooltip(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::Common(common::Message::OnScreenKeyboard)),
widget::text(fl!("on-screen-keyboard")),
widget::tooltip::Position::Top,
),
widget::tooltip(
input_button,
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 +617,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 +680,9 @@ 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 +705,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()
@ -692,6 +730,12 @@ impl cosmic::Application for App {
match message {
Message::None => {}
Message::Common(common_message) => {
if matches!(&common_message, common::Message::Prompt(_, _, Some(_)))
&& self.authenticating
{
self.authenticating = false;
self.common.prompt_opt = None;
}
return self.common.update(common_message);
}
Message::OutputEvent(output_event, output) => {
@ -714,6 +758,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)
{
@ -753,10 +801,10 @@ impl cosmic::Application for App {
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 {
let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
(
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.),
Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
)
} else {
(
@ -791,11 +839,19 @@ impl cosmic::Application for App {
);
if matches!(self.state, State::Locked { .. }) {
return Task::batch([get_lock_surface(surface_id, output).chain({
cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
))
})]);
return cosmic::task::message(cosmic::Action::Surface(
cosmic::surface::action::lock(
|| LiveSettings {
padding: None,
corners: None,
blur: Some(false),
},
surface_id,
output,
None::<fn() -> cosmic::Element<'static, cosmic::Action<Message>>>,
),
))
.chain(cosmic::task::message(cosmic::Action::Surface(msg)));
}
}
OutputEvent::Removed => {
@ -826,10 +882,10 @@ impl cosmic::Application for App {
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 {
let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
(
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.),
Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
)
} else {
(Point::ORIGIN, Size::new(1920., 1080.))
@ -935,7 +991,18 @@ impl cosmic::Application for App {
// Create lock surfaces
for (output, surface_id) in self.common.surface_ids.iter() {
commands.push(get_lock_surface(*surface_id, output.clone()));
commands.push(cosmic::task::message(cosmic::Action::Surface(
cosmic::surface::action::lock(
|| LiveSettings {
padding: None,
corners: None,
blur: Some(false),
},
*surface_id,
output.clone(),
None::<fn() -> cosmic::Element<'static, cosmic::Action<Message>>>,
),
)));
if let Some((rect, name)) = self
.common
@ -945,11 +1012,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,
@ -966,11 +1037,9 @@ impl cosmic::Application for App {
app.menu(subsurface_id).map(cosmic::Action::App)
})),
);
commands.push(cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
)));
commands.push(cosmic::task::message(cosmic::Action::Surface(msg)));
} else {
tracing::error!("no rectangle for subsurface...");
tracing::error!("no rectangle for subsurface creation...");
}
}
return Task::batch(commands);
@ -985,11 +1054,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 +1078,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 +1093,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() {
@ -1127,9 +1202,7 @@ impl cosmic::Application for App {
}
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
return cosmic::task::message(cosmic::Action::Surface(a));
}
}
Task::none()
@ -1141,17 +1214,8 @@ impl cosmic::Application for App {
}
/// Creates a view after each update.
fn view_window(&self, surface_id: SurfaceId) -> Element<'_, Self::Message> {
let img = self
.common
.surface_images
.get(&surface_id)
.unwrap_or(&self.common.fallback_background);
widget::image(img)
.content_fit(iced::ContentFit::Cover)
.width(Length::Fill)
.height(Length::Fill)
.into()
fn view_window(&self, _surface_id: SurfaceId) -> Element<'_, Self::Message> {
widget::space::horizontal().into()
}
fn subscription(&self) -> Subscription<Self::Message> {
@ -1190,7 +1254,7 @@ impl cosmic::Application for App {
);
#[cfg(feature = "logind")]
{
if 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(),