Add daemon to proxy user backgrounds

This commit is contained in:
Jeremy Soller 2024-02-06 15:03:07 -07:00
parent 3e2743a2e6
commit fd049483c3
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
15 changed files with 487 additions and 58 deletions

3
.gitignore vendored
View file

@ -2,7 +2,10 @@
/debian/*debhelper*
/debian/cosmic-greeter.substvars
/debian/cosmic-greeter/
/debian/cosmic-greeter-daemon.substvars
/debian/cosmic-greeter-daemon/
/debian/files
/debian/tmp/
/target/
/vendor.tar
/vendor/

19
Cargo.lock generated
View file

@ -811,6 +811,7 @@ dependencies = [
"cosmic-bg-config",
"cosmic-config",
"cosmic-dbus-networkmanager",
"cosmic-greeter-daemon",
"env_logger",
"freedesktop_entry_parser",
"greetd_ipc",
@ -819,6 +820,7 @@ dependencies = [
"logind-zbus",
"pam-client",
"pwd",
"ron",
"shlex",
"tokio",
"upower_dbus",
@ -826,6 +828,23 @@ dependencies = [
"zbus",
]
[[package]]
name = "cosmic-greeter-daemon"
version = "0.1.0"
dependencies = [
"cosmic-bg-config",
"cosmic-config",
"env_logger",
"libc",
"libcosmic",
"log",
"pwd",
"ron",
"serde",
"tokio",
"zbus",
]
[[package]]
name = "cosmic-protocols"
version = "0.1.0"

View file

@ -5,12 +5,19 @@ edition = "2021"
[dependencies]
chrono = "0.4.31"
env_logger = "0.10.0"
cosmic-bg-config.workspace = true
cosmic-config = { workspace = true, features = ["calloop", "macro"] }
cosmic-greeter-daemon = { path = "daemon" }
env_logger.workspace = true
freedesktop_entry_parser = "1.3.0"
log = "0.4.20"
libcosmic = { workspace = true, features = ["tokio", "wayland"] }
log.workspace = true
pam-client = "0.5.0"
pwd = "1.4.0"
pwd.workspace = true
ron.workspace = true
shlex = "1.2.0"
#TODO: reduce features
tokio = { workspace = true, features = ["full"] }
wayland-client = "0.31.1"
# For network status using networkmanager feature
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
@ -19,29 +26,38 @@ logind-zbus = { version = "3.1.2", optional = true }
# For power status with upower feature
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
# Required for some features
zbus = { version = "3.14.1", optional = true }
[dependencies.cosmic-bg-config]
git = "https://github.com/pop-os/cosmic-bg"
[dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic"
features = ["calloop", "macro"]
zbus = { workspace = true, optional = true }
[dependencies.greetd_ipc]
version = "0.9.0"
features = ["sync-codec"]
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
features = ["tokio", "wayland"]
[dependencies.tokio]
version = "1.33.0"
features = ["full"]
[features]
default = ["logind", "networkmanager", "upower"]
logind = ["logind-zbus", "zbus"]
networkmanager = ["cosmic-dbus-networkmanager", "zbus"]
upower = ["upower_dbus", "zbus"]
[workspace]
members = ["daemon"]
[workspace.dependencies]
env_logger = "0.10.0"
log = "0.4.20"
pwd = "1.4.0"
ron = "0.8"
serde = "1"
tokio = "1.33.0"
zbus = "3.14.1"
[workspace.dependencies.cosmic-bg-config]
git = "https://github.com/pop-os/cosmic-bg"
default-features = false
[workspace.dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic"
default-features = false
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false

20
daemon/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "cosmic-greeter-daemon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmic-bg-config.workspace = true
cosmic-config.workspace = true
env_logger.workspace = true
libc = "0.2"
libcosmic.workspace = true
log.workspace = true
pwd.workspace = true
ron.workspace = true
serde.workspace = true
zbus.workspace = true
#TODO: reduce features
tokio = { workspace = true, features = ["full"] }

16
daemon/src/lib.rs Normal file
View file

@ -0,0 +1,16 @@
pub use cosmic_bg_config::Color;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct UserData {
pub uid: u32,
pub name: String,
pub full_name_opt: Option<String>,
pub icon_opt: Option<Vec<u8>>,
pub wallpapers_opt: Option<Vec<(String, WallpaperData)>>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub enum WallpaperData {
Bytes(Vec<u8>),
Color(Color),
}

181
daemon/src/main.rs Normal file
View file

@ -0,0 +1,181 @@
use cosmic_bg_config::Source;
use cosmic_greeter_daemon::{UserData, WallpaperData};
use std::{error::Error, fs, future::pending, io, path::Path};
use zbus::{dbus_interface, ConnectionBuilder, DBusError};
//IMPORTANT: this function is critical to the security of this proxy. It must ensure that the
// callback is executed with the permissions of the specified user id. A good test is to see if
// the /etc/shadow file can be read with a non-root user, it should fail with EPERM.
fn run_as_user<F: FnOnce() -> T, T>(user: &pwd::Passwd, f: F) -> Result<T, io::Error> {
if unsafe { libc::seteuid(user.uid) } != 0 {
return Err(io::Error::last_os_error());
}
let t = f();
if unsafe { libc::seteuid(0) } != 0 {
panic!("failed to restore root user id")
}
Ok(t)
}
#[derive(DBusError, Debug)]
#[dbus_error(prefix = "com.system76.CosmicGreeter")]
enum GreeterError {
#[dbus_error(zbus_error)]
ZBus(zbus::Error),
Ron(String),
RunAsUser(String),
}
struct GreeterProxy;
#[dbus_interface(name = "com.system76.CosmicGreeter")]
impl GreeterProxy {
fn get_user_data(&mut self) -> Result<String, GreeterError> {
// 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
let users: Vec<_> = /* unsafe */ {
pwd::Passwd::iter()
.filter(|user| {
if user.uid < 1000 {
// Skip system accounts
return false;
}
match Path::new(&user.shell).file_name().and_then(|x| x.to_str()) {
// Skip shell ending in false
Some("false") => false,
// Skip shell ending in nologin
Some("nologin") => false,
_ => true,
}
})
.collect()
};
let mut user_datas = Vec::new();
for user in users {
if user.uid < 1000 {
// Skip system accounts
continue;
}
match Path::new(&user.shell).file_name().and_then(|x| x.to_str()) {
// Skip shell ending in false
Some("false") => continue,
// Skip shell ending in nologin
Some("nologin") => continue,
_ => (),
}
//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(&user.name);
let icon_opt = if icon_path.is_file() {
match fs::read(&icon_path) {
Ok(icon_data) => Some(icon_data),
Err(err) => {
log::error!("failed to read icon {:?}: {:?}", icon_path, err);
None
}
}
} else {
None
};
//IMPORTANT: Assume the identity of the user to ensure we don't read wallpaper file data as root
let wallpapers_opt = run_as_user(&user, || {
//TODO: use libcosmic to find this path
let wallpapers_path = Path::new(&user.dir)
.join(".local/state/cosmic/com.system76.CosmicBackground/v1/wallpapers");
if wallpapers_path.is_file() {
match fs::read_to_string(&wallpapers_path) {
Ok(wallpapers_ron) => {
match ron::from_str::<Vec<(String, Source)>>(&wallpapers_ron) {
Ok(sources) => {
let mut wallpaper_datas = Vec::new();
for (output, source) in sources {
match source {
Source::Path(path) => match fs::read(&path) {
Ok(bytes) => {
wallpaper_datas.push((
output,
WallpaperData::Bytes(bytes),
));
}
Err(err) => {
log::error!(
"failed to read wallpaper {:?}: {:?}",
path,
err
);
}
},
Source::Color(color) => {
wallpaper_datas
.push((output, WallpaperData::Color(color)));
}
}
}
Some(wallpaper_datas)
}
Err(err) => {
log::error!(
"failed to parse wallpapers {:?}: {:?}",
wallpapers_path,
err
);
None
}
}
}
Err(err) => {
log::error!(
"failed to read wallpapers {:?}: {:?}",
wallpapers_path,
err
);
None
}
}
} else {
None
}
})
.map_err(|err| GreeterError::RunAsUser(err.to_string()))?;
user_datas.push(UserData {
uid: user.uid,
name: user.name,
full_name_opt: user
.gecos
.map(|gecos| gecos.split(',').next().unwrap_or_default().to_string()),
icon_opt,
//TODO: should wallpapers come from a per-user call?
wallpapers_opt,
});
}
//TODO: is ron the best choice for passing around background data?
ron::to_string(&user_datas).map_err(|err| GreeterError::Ron(err.to_string()))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let _conn = ConnectionBuilder::system()?
.name("com.system76.CosmicGreeter")?
.serve_at("/com/system76/CosmicGreeter", GreeterProxy)?
.build()
.await?;
pending::<()>().await;
Ok(())
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy group="cosmic-greeter">
<allow send_destination="com.system76.CosmicGreeter"/>
<allow receive_sender="com.system76.CosmicGreeter"/>
</policy>
<policy user="root">
<allow own="com.system76.CosmicGreeter"/>
<allow send_destination="com.system76.CosmicGreeter"/>
<allow receive_sender="com.system76.CosmicGreeter"/>
</policy>
</busconfig>

8
debian/control vendored
View file

@ -20,8 +20,16 @@ Depends:
adduser,
bash,
cosmic-comp,
cosmic-greeter-daemon,
greetd,
${misc:Depends},
${shlibs:Depends}
Provides: x-display-manager
Description: Cosmic Greeter
Package: cosmic-greeter-daemon
Architecture: amd64 arm64
Depends:
${misc:Depends},
${shlibs:Depends}
Description: Cosmic Greeter daemon

2
debian/cosmic-greeter-daemon.install vendored Normal file
View file

@ -0,0 +1,2 @@
/usr/bin/cosmic-greeter-daemon
/usr/share/dbus-1/system.d/com.system76.CosmicGreeter.conf

9
debian/cosmic-greeter-daemon.service vendored Normal file
View file

@ -0,0 +1,9 @@
[Unit]
Description=COSMIC Greeter Daemon
[Service]
ExecStart=/usr/bin/cosmic-greeter-daemon
Restart=on-failure
[Install]
WantedBy=multi-user.target

View file

@ -1 +1,2 @@
cosmic-greeter.toml /etc/greetd/
/usr/bin/cosmic-greeter

View file

@ -1,6 +1,6 @@
[Unit]
Description=COSMIC Greeter daemon
After=systemd-user-sessions.service plymouth-quit-wait.service
Description=COSMIC Greeter
After=systemd-user-sessions.service plymouth-quit-wait.service cosmic-greeter-daemon.service
After=getty@tty1.service
Conflicts=getty@tty1.service

3
debian/rules vendored
View file

@ -1,6 +1,6 @@
#!/usr/bin/make -f
export DESTDIR = debian/cosmic-greeter
export DESTDIR = debian/tmp
export VENDOR ?= 1
%:
@ -23,3 +23,4 @@ override_dh_auto_install:
override_dh_installsystemd:
dh_installsystemd -pcosmic-greeter --no-start -r cosmic-greeter.service
dh_installsystemd -pcosmic-greeter-daemon --no-start -r cosmic-greeter-daemon.service

View file

@ -11,6 +11,12 @@ export INSTALL_DIR := base-dir / 'share'
bin-src := 'target' / 'release' / name
bin-dst := base-dir / 'bin' / name
daemon-src := 'target' / 'release' / name + '-daemon'
daemon-dst := base-dir / 'bin' / name + '-daemon'
dbus-src := 'dbus' / APPID + '.conf'
dbus-dst := base-dir / 'share' / 'dbus-1' / 'system.d' / APPID + '.conf'
# Default recipe which runs `just build-release`
default: build-release
@ -24,7 +30,7 @@ clean-dist: clean
# Compiles with debug profile
build-debug *args:
cargo build {{args}}
cargo build --all {{args}}
# Compiles with release profile
build-release *args: (build-debug '--release' args)
@ -46,10 +52,12 @@ run *args:
# Installs files
install:
install -Dm0755 {{bin-src}} {{bin-dst}}
install -Dm0755 {{daemon-src}} {{daemon-dst}}
install -Dm0755 {{dbus-src}} {{dbus-dst}}
# Uninstalls installed files
uninstall:
rm {{bin-dst}}
rm {{bin-dst}} {{daemon-dst}} {{dbus-dst}}
# Vendor dependencies locally
vendor:

View file

@ -10,7 +10,7 @@ use cosmic::{
self,
wayland::{Event as WaylandEvent, LayerEvent, OutputEvent},
},
futures::SinkExt,
futures::{self, SinkExt},
subscription,
wayland::{
actions::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
@ -23,14 +23,37 @@ use cosmic::{
iced_runtime::core::window::Id as SurfaceId,
style, widget, Element,
};
use cosmic_greeter_daemon::{UserData, WallpaperData};
use greetd_ipc::{codec::SyncCodec, AuthMessageType, Request, Response};
use std::{collections::HashMap, env, fs, io, path::Path, process, sync::Arc};
use std::{collections::HashMap, env, error::Error, fs, io, path::Path, process, sync::Arc};
use tokio::{net::UnixStream, time};
use wayland_client::{protocol::wl_output::WlOutput, Proxy};
use zbus::{dbus_proxy, Connection};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[dbus_proxy(
interface = "com.system76.CosmicGreeter",
default_service = "com.system76.CosmicGreeter",
default_path = "/com/system76/CosmicGreeter"
)]
trait Greeter {
async fn get_user_data(&self) -> Result<String, zbus::Error>;
}
async fn user_data_dbus() -> Result<Vec<UserData>, Box<dyn Error>> {
let connection = Connection::system().await?;
// `dbus_proxy` macro creates `MyGreaterProxy` based on `Notifications` trait.
let proxy = GreeterProxy::new(&connection).await?;
let reply = proxy.get_user_data().await?;
let user_datas: Vec<UserData> = ron::from_str(&reply)?;
Ok(user_datas)
}
fn user_data_fallback() -> Vec<UserData> {
// The pwd::Passwd method is unsafe (but not labelled as such) due to using global state (libc pwent functions).
let users: Vec<_> = /* unsafe */ {
/* unsafe */
{
pwd::Passwd::iter()
.filter(|user| {
if user.uid < 1000 {
@ -51,7 +74,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let icon_path = Path::new("/var/lib/AccountsService/icons").join(&user.name);
let icon_opt = if icon_path.is_file() {
match fs::read(&icon_path) {
Ok(icon_data) => Some(widget::image::Handle::from_memory(icon_data)),
Ok(icon_data) => Some(icon_data),
Err(err) => {
log::error!("failed to read {:?}: {:?}", icon_path, err);
None
@ -60,11 +83,33 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
} else {
None
};
(user, icon_opt)
UserData {
uid: user.uid,
name: user.name,
full_name_opt: user
.gecos
.map(|gecos| gecos.split(',').next().unwrap_or_default().to_string()),
icon_opt,
wallpapers_opt: None,
}
})
.collect()
}
}
pub fn main() -> Result<(), Box<dyn Error>> {
let mut user_datas = match futures::executor::block_on(async { user_data_dbus().await }) {
Ok(ok) => ok,
Err(err) => {
log::error!("failed to load user data from daemon: {}", err);
user_data_fallback()
}
};
// Sort user data by uid
user_datas.sort_by(|a, b| a.uid.cmp(&b.uid));
enum SessionType {
X11,
Wayland,
@ -186,13 +231,13 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
sessions
};
//TODO: use background config
let background = widget::image::Handle::from_memory(include_bytes!("../res/background.png"));
let fallback_background =
widget::image::Handle::from_memory(include_bytes!("../res/background.png"));
let flags = Flags {
users,
user_datas,
sessions,
background,
fallback_background,
};
let settings = Settings::default().no_main_window(true);
@ -287,9 +332,9 @@ fn request_command(socket: Arc<UnixStream>, request: Request) -> Command<Message
#[derive(Clone)]
pub struct Flags {
users: Vec<(pwd::Passwd, Option<widget::image::Handle>)>,
user_datas: Vec<UserData>,
sessions: HashMap<String, Vec<String>>,
background: widget::image::Handle,
fallback_background: widget::image::Handle,
}
#[derive(Clone, Debug)]
@ -329,6 +374,7 @@ pub struct App {
flags: Flags,
surface_ids: HashMap<WlOutput, SurfaceId>,
active_surface_id_opt: Option<SurfaceId>,
surface_images: HashMap<SurfaceId, widget::image::Handle>,
surface_names: HashMap<SurfaceId, String>,
text_input_ids: HashMap<SurfaceId, widget::Id>,
network_icon_opt: Option<&'static str>,
@ -341,6 +387,55 @@ pub struct App {
error_opt: Option<String>,
}
impl App {
fn update_wallpapers(&mut self) {
let username = match &self.username_opt {
Some(some) => some,
None => return,
};
let user_data = match self.flags.user_datas.iter().find(|x| &x.name == username) {
Some(some) => some,
None => return,
};
let wallpapers = match &user_data.wallpapers_opt {
Some(some) => some,
None => return,
};
for (output, surface_id) in self.surface_ids.iter() {
if self.surface_images.contains_key(surface_id) {
continue;
}
let output_name = match self.surface_names.get(surface_id) {
Some(some) => some,
None => continue,
};
log::info!("updating wallpaper for {:?}", output_name);
for (wallpaper_output_name, wallpaper_data) in wallpapers.iter() {
if wallpaper_output_name == output_name {
match wallpaper_data {
WallpaperData::Bytes(bytes) => {
let image = widget::image::Handle::from_memory(bytes.clone());
self.surface_images.insert(*surface_id, image);
//TODO: what to do about duplicates?
break;
}
WallpaperData::Color(color) => {
//TODO: support color sources
log::warn!("output {}: unsupported source {:?}", output.id(), color);
}
}
}
}
}
}
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
@ -384,12 +479,12 @@ impl cosmic::Application for App {
flags,
surface_ids: HashMap::new(),
active_surface_id_opt: None,
surface_images: HashMap::new(),
surface_names: HashMap::new(),
text_input_ids: HashMap::new(),
network_icon_opt: None,
power_info_opt: None,
socket_state: SocketState::Pending,
//TODO: choose last used user
username_opt: None,
prompt_opt: None,
session_names,
@ -437,6 +532,8 @@ impl cosmic::Application for App {
Some(output_info) => match output_info.name {
Some(output_name) => {
self.surface_names.insert(surface_id, output_name.clone());
self.surface_images.remove(&surface_id);
self.update_wallpapers();
}
None => {
log::warn!("output {}: no output name", output.id());
@ -477,6 +574,7 @@ impl cosmic::Application for App {
log::info!("output {}: removed", output.id());
match self.surface_ids.remove(&output) {
Some(surface_id) => {
self.surface_images.remove(&surface_id);
self.surface_names.remove(&surface_id);
self.text_input_ids.remove(&surface_id);
return destroy_layer_surface(surface_id);
@ -503,6 +601,23 @@ impl cosmic::Application for App {
},
Message::Socket(socket_state) => {
self.socket_state = socket_state;
match &self.socket_state {
SocketState::Open(socket) => {
// When socket is opened, request default user
//TODO: choose last used user
match self.flags.user_datas.first().map(|x| x.name.clone()) {
Some(username) => {
let socket = socket.clone();
return Command::perform(
async { message::app(Message::Username(socket, username)) },
|x| x,
);
}
None => {}
}
}
_ => {}
}
}
Message::NetworkIcon(network_icon_opt) => {
self.network_icon_opt = network_icon_opt;
@ -526,6 +641,8 @@ impl cosmic::Application for App {
}
Message::Username(socket, username) => {
self.username_opt = Some(username.clone());
self.surface_images.clear();
self.update_wallpapers();
return request_command(socket, Request::CreateSession { username });
}
Message::Auth(socket, response) => {
@ -566,6 +683,7 @@ impl cosmic::Application for App {
Message::Exit => {
let mut commands = Vec::new();
for (_output, surface_id) in self.surface_ids.drain() {
self.surface_images.remove(&surface_id);
self.surface_names.remove(&surface_id);
self.text_input_ids.remove(&surface_id);
commands.push(destroy_layer_surface(surface_id));
@ -671,15 +789,20 @@ impl cosmic::Application for App {
SocketState::Open(socket) => {
match &self.username_opt {
Some(username) => {
for (user, icon_opt) in &self.flags.users {
if &user.name == username {
match icon_opt {
for user_data in &self.flags.user_datas {
if &user_data.name == username {
match &user_data.icon_opt {
Some(icon) => {
column = column.push(
widget::container(
widget::Image::new(icon.clone())
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
widget::Image::new(
//TODO: cache handle
widget::image::Handle::from_memory(
icon.clone(),
),
)
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
@ -687,9 +810,8 @@ impl cosmic::Application for App {
}
None => {}
}
match &user.gecos {
Some(gecos) => {
let full_name = gecos.split(",").next().unwrap_or("");
match &user_data.full_name_opt {
Some(full_name) => {
column = column.push(
widget::container(widget::text::title4(full_name))
.width(Length::Fill)
@ -702,18 +824,23 @@ impl cosmic::Application for App {
}
}
None => {
let mut row =
widget::row::with_capacity(self.flags.users.len()).spacing(12.0);
for (user, icon_opt) in &self.flags.users {
let mut row = widget::row::with_capacity(self.flags.user_datas.len())
.spacing(12.0);
for user_data in &self.flags.user_datas {
let mut column = widget::column::with_capacity(2).spacing(12.0);
match icon_opt {
match &user_data.icon_opt {
Some(icon) => {
column = column.push(
widget::container(
widget::Image::new(icon.clone())
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
widget::Image::new(
//TODO: cache handle
widget::image::Handle::from_memory(
icon.clone(),
),
)
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
@ -721,9 +848,8 @@ impl cosmic::Application for App {
}
None => {}
}
match &user.gecos {
Some(gecos) => {
let full_name = gecos.split(",").next().unwrap_or("");
match &user_data.full_name_opt {
Some(full_name) => {
column = column.push(
widget::container(widget::text::title4(full_name))
.width(Length::Fill)
@ -740,7 +866,9 @@ impl cosmic::Application for App {
.padding(16)
.style(cosmic::theme::Container::Card),
)
.on_press(Message::Username(socket.clone(), user.name.clone())),
.on_press(
Message::Username(socket.clone(), user_data.name.clone()),
),
);
}
column = column.push(row);
@ -844,7 +972,10 @@ impl cosmic::Application for App {
.align_y(alignment::Vertical::Top)
.style(cosmic::theme::Container::Transparent),
)
.image(self.flags.background.clone())
.image(match self.surface_images.get(&surface_id) {
Some(some) => some.clone(),
None => self.flags.fallback_background.clone(),
})
.content_fit(iced::ContentFit::Cover)
.into()
}