// Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only #![allow(clippy::single_match)] use cctk::cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1; use cctk::cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2; use cctk::sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}; use cctk::wayland_client::protocol::wl_output; use cctk::wayland_client::{Connection, Proxy}; use cctk::wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1; use clap::Parser; use cosmic::app::{Application, CosmicFlags}; use cosmic::iced::clipboard::dnd::{DndEvent, SourceEvent}; use cosmic::iced::event::wayland::{Event as WaylandEvent, LayerEvent, OutputEvent}; use cosmic::iced::keyboard::key::{Key, Named}; use cosmic::iced::mouse::ScrollDelta; use cosmic::iced::platform_specific::shell::commands::layer_surface::{ destroy_layer_surface, get_layer_surface, }; use cosmic::iced::runtime::platform_specific::wayland::layer_surface::{ IcedOutput, SctkLayerSurfaceSettings, }; use cosmic::iced::window::Id as SurfaceId; use cosmic::iced::{self, Size, Subscription, Task}; use cosmic::scroll::DiscreteScrollState; use cosmic::{cctk, dbus_activation}; use cosmic_comp_config::CosmicCompConfig; use cosmic_config::CosmicConfigEntry; use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfigEntry, PanelAnchor}; use i18n_embed::DesktopLanguageRequester; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::process::Command; use std::time::Duration; use std::{mem, str}; mod dbus; mod desktop_info; #[macro_use] mod localize; mod backend; mod view; use backend::{ExtForeignToplevelHandleV1, ExtWorkspaceHandleV1, ToplevelInfo}; mod dnd; mod utils; mod widgets; use dnd::{DragSurface, DragToplevel, DragWorkspace, DropTarget}; const SCROLL_RATE_LIMIT: Duration = Duration::from_millis(200); #[derive(Clone, Debug, Default, PartialEq, CosmicConfigEntry)] struct CosmicWorkspacesConfig { show_workspace_number: bool, show_workspace_name: bool, } #[derive(Parser, Debug, Clone)] #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] pub struct Args {} #[derive(Default, Debug, Clone)] pub struct WorkspaceCommands; #[allow(clippy::to_string_trait_impl)] impl ToString for WorkspaceCommands { fn to_string(&self) -> String { String::new() } } impl CosmicFlags for Args { type SubCommand = WorkspaceCommands; type Args = Vec; fn action(&self) -> Option<&WorkspaceCommands> { None } } #[derive(Clone, Debug)] enum Msg { WaylandEvent(WaylandEvent), Wayland(backend::Event), Close, ActivateWorkspace(ExtWorkspaceHandleV1), #[allow(dead_code)] CloseWorkspace(ExtWorkspaceHandleV1), ActivateToplevel(ExtForeignToplevelHandleV1), CloseToplevel(ExtForeignToplevelHandleV1), StartDrag(DragSurface), DndEnter(DropTarget, f64, f64, Vec), DndLeave(DropTarget), DndToplevelDrop(DragToplevel), #[allow(dead_code)] DndWorkspaceDrag, #[allow(dead_code)] DndWorkspaceDrop(DragWorkspace), SourceFinished, #[allow(dead_code)] NewWorkspace, CompConfig(Box), Config(CosmicWorkspacesConfig), BgConfig(cosmic_bg_config::state::State), UpdateToplevelIcon(String, Option), OnScroll(wl_output::WlOutput, ScrollDelta), TogglePinned(ExtWorkspaceHandleV1), EnteredWorkspaceSidebarEntry(ExtWorkspaceHandleV1, bool), DbusInterface(zbus::Result), DBus(dbus::Event), PanelContainerEntries(Vec), PanelConfig(CosmicPanelConfig), ActionOnTyping(String), Ignore, } #[derive(Clone, Debug)] struct Workspace { info: backend::Workspace, // img_for_output: HashMap, img: Option, outputs: HashSet, has_cursor: bool, dnd_source_id: iced::id::Id, } impl Workspace { fn handle(&self) -> &ExtWorkspaceHandleV1 { &self.info.handle } fn is_active(&self) -> bool { self.info .state .contains(ext_workspace_handle_v1::State::Active) } fn is_pinned(&self) -> bool { self.info .cosmic_state .contains(zcosmic_workspace_handle_v2::State::Pinned) } } #[derive(Clone, Debug)] struct Toplevel { handle: ExtForeignToplevelHandleV1, info: ToplevelInfo, img: Option, icon: Option, } #[derive(Clone)] struct Output { handle: wl_output::WlOutput, name: String, width: i32, height: i32, } #[derive(Debug)] struct LayerSurface { output: wl_output::WlOutput, // for transitions, would need windows in more than one workspace? But don't capture all of // them all the time every frame. } #[derive(Default)] struct Conf { workspace_config: cosmic_comp_config::workspace::WorkspaceConfig, config: CosmicWorkspacesConfig, bg: cosmic_bg_config::state::State, } #[derive(Default)] struct App { capture_filter: backend::CaptureFilter, layer_surfaces: HashMap, outputs: Vec, workspaces: Workspaces, toplevels: Toplevels, toplevel_capabilities: Vec, conn: Option, visible: bool, wayland_cmd_sender: Option>, drag_surface: Option<(DragSurface, Size)>, conf: Conf, core: cosmic::app::Core, drop_target: Option, scroll: DiscreteScrollState, dbus_interface: Option, panel_configs: HashMap>, action_on_typing_activated: bool, } #[derive(Debug, Default)] struct Workspaces(Vec); impl Workspaces { fn for_handle(&self, handle: &ExtWorkspaceHandleV1) -> Option<&Workspace> { self.0.iter().find(|i| i.handle() == handle) } fn for_handle_mut(&mut self, handle: &ExtWorkspaceHandleV1) -> Option<&mut Workspace> { self.0.iter_mut().find(|i| i.handle() == handle) } fn for_output<'a>( &'a self, output: &'a wl_output::WlOutput, ) -> impl Iterator + 'a { self.0.iter().filter(|w| w.outputs.contains(output)) } } #[derive(Debug, Default)] struct Toplevels(Vec); impl Toplevels { fn for_handle_mut(&mut self, handle: &ExtForeignToplevelHandleV1) -> Option<&mut Toplevel> { self.0.iter_mut().find(|i| &i.handle == handle) } } impl App { fn create_surface(&mut self, output: wl_output::WlOutput) -> Task> { let id = SurfaceId::unique(); self.layer_surfaces.insert( id, LayerSurface { output: output.clone(), }, ); get_layer_surface(SctkLayerSurfaceSettings { id, keyboard_interactivity: KeyboardInteractivity::Exclusive, namespace: "cosmic-workspace-overview".into(), layer: Layer::Top, size: Some((None, None)), output: IcedOutput::Output(output), anchor: Anchor::all(), ..Default::default() }) } fn destroy_surface(&mut self, output: &wl_output::WlOutput) -> Task> { if let Some((id, _)) = self .layer_surfaces .iter() .find(|(_id, surface)| &surface.output == output) { let id = *id; destroy_layer_surface(id) } else { Task::none() } } fn toggle(&mut self) -> Task> { if self.visible { self.hide() } else { self.show() } } fn show(&mut self) -> Task> { if !self.visible { self.visible = true; let outputs = self.outputs.clone(); let cmd = Task::batch( outputs .into_iter() .map(|output| self.create_surface(output.handle)) .collect::>(), ); self.update_capture_filter(); if let Some(interface) = self.dbus_interface.clone() { tokio::spawn(async move { let _ = interface.shown().await; }); } cmd } else { Task::none() } } // Close all shell surfaces fn hide(&mut self) -> Task> { if let Some(interface) = self.dbus_interface.clone() { tokio::spawn(async move { let _ = interface.hidden().await; }); } if self.action_on_typing_activated { let cmd = match self.conf.workspace_config.action_on_typing { cosmic_comp_config::workspace::Action::None => return Task::none(), cosmic_comp_config::workspace::Action::OpenLauncher => { // self.common.config.system_actions.get(&Launcher) Some("cosmic-launcher \"$@\"".to_string()) } cosmic_comp_config::workspace::Action::OpenApplications => { // self.common.config.system_actions.get(&Applications) Some("cosmic-app-library \"$@\"".to_string()) } }; if let Some(cmd) = cmd { tokio::spawn(async { let mut child = Command::new("/bin/sh") .arg("-c") .arg(cmd) .arg("_") .arg("close") .spawn() .unwrap(); let _ = child.wait().unwrap(); }); } } self.action_on_typing_activated = false; self.visible = false; self.update_capture_filter(); self.drag_surface = None; Task::batch( self.layer_surfaces .keys() .copied() .map(destroy_layer_surface), ) } fn send_wayland_cmd(&self, cmd: backend::Cmd) { if let Some(sender) = self.wayland_cmd_sender.as_ref() { sender.send(cmd).unwrap(); } } fn update_capture_filter(&mut self) { let mut capture_filter = backend::CaptureFilter::default(); if self.visible { capture_filter.workspaces_on_outputs = self.outputs.iter().map(|x| x.handle.clone()).collect(); capture_filter.toplevels_on_workspaces = self .workspaces .0 .iter() .filter(|x| x.is_active()) .map(|x| x.handle().clone()) .collect(); } // Drop `CaptureImage` for workspaces and toplevels not matching new // filter. for workspace in &mut self.workspaces.0 { if !capture_filter.workspace_outputs_matches(&workspace.outputs) { workspace.img = None; } } for toplevel in &mut self.toplevels.0 { if !capture_filter.toplevel_matches(&toplevel.info) { toplevel.img = None; } } self.capture_filter = capture_filter.clone(); self.send_wayland_cmd(backend::Cmd::CaptureFilter(capture_filter)); } fn panel_regions(&self, output_handle: &wl_output::WlOutput) -> iced::Padding { let Some(output) = self.outputs.iter().find(|o| o.handle == *output_handle) else { return iced::Padding::ZERO; }; let mut regions = iced::Padding::ZERO; // TODO: If compositor supports overlap notify, also use that? // Or otherwise verify the panel is actually running. for config in self.panel_configs.values().flatten() { if config.autohide.is_some() && !config.exclusive_zone { let dimention_constraints = config.get_dimensions( Some((output.width as u32, output.height as u32)), None, Some(config.get_effective_anchor_gap()), ); let size = config.size.get_applet_icon_size_with_padding(true) + u32::from(config.margin); match config.anchor { PanelAnchor::Left => { let size = dimention_constraints.0.map_or(size, |constraints| { size.clamp(constraints.start, constraints.end) }); regions.left += size as f32; } PanelAnchor::Right => { let size = dimention_constraints.0.map_or(size, |constraints| { size.clamp(constraints.start, constraints.end) }); regions.right += size as f32; } PanelAnchor::Top => { let size = dimention_constraints.1.map_or(size, |constraints| { size.clamp(constraints.start, constraints.end) }); regions.top += size as f32; } PanelAnchor::Bottom => { let size = dimention_constraints.1.map_or(size, |constraints| { size.clamp(constraints.start, constraints.end) }); regions.bottom += size as f32; } } } } regions } } impl Application for App { type Message = Msg; type Executor = cosmic::SingleThreadExecutor; type Flags = Args; const APP_ID: &'static str = "com.system76.CosmicWorkspaces"; fn init(core: cosmic::app::Core, _flags: Self::Flags) -> (Self, Task>) { ( Self { core, scroll: DiscreteScrollState::default().rate_limit(Some(SCROLL_RATE_LIMIT)), ..Default::default() }, Task::none(), ) } // TODO: show panel and dock? Drag? fn update(&mut self, message: Msg) -> Task> { match message { Msg::SourceFinished => { self.drag_surface = None; } Msg::WaylandEvent(evt) => match evt { WaylandEvent::Output(evt, output) => { // TODO: Less hacky way to get connection from iced-sctk if self.conn.is_none() && let Some(backend) = output.backend().upgrade() { self.conn = Some(Connection::from_backend(backend)); } match evt { OutputEvent::Created(Some(info)) => { if let (Some((width, height)), Some(name)) = (info.logical_size, info.name) { self.outputs.push(Output { handle: output.clone(), name: name.clone(), width, height, }); if self.visible { return self.create_surface(output.clone()); } } } OutputEvent::Created(None) => {} // XXX? OutputEvent::InfoUpdate(info) => { if let Some(output) = self.outputs.iter_mut().find(|x| x.handle == output) { if let Some((width, height)) = info.logical_size { output.width = width; output.height = height; } if let Some(name) = info.name { output.name = name; } // XXX re-create surface? } } OutputEvent::Removed => { if let Some(idx) = self.outputs.iter().position(|x| x.handle == output) { self.outputs.remove(idx); } if self.visible { return self.destroy_surface(&output); } } } } WaylandEvent::Layer(LayerEvent::Done, _surface, id) => { if self.layer_surfaces.remove(&id).is_none() { log::error!("removing non-existant layer shell id {}?", id); } } _ => {} }, Msg::Wayland(evt) => { match evt { backend::Event::CmdSender(sender) => { self.wayland_cmd_sender = Some(sender); } backend::Event::Workspaces(mut workspaces) => { workspaces.sort_by(|(_, w1), (_, w2)| w1.coordinates.cmp(&w2.coordinates)); let old_workspaces = mem::take(&mut self.workspaces); for (outputs, workspace) in workspaces { // XXX efficiency let old_workspace = old_workspaces.for_handle(&workspace.handle); let img = old_workspace.map(|i| i.img.clone()).unwrap_or_default(); let has_cursor = old_workspace.is_some_and(|w| w.has_cursor); let dnd_source_id = old_workspace .map_or_else(iced::id::Id::unique, |w| w.dnd_source_id.clone()); self.workspaces.0.push(Workspace { info: workspace, outputs, img, has_cursor, dnd_source_id, }); } self.update_capture_filter(); } backend::Event::NewToplevel(handle, info) => { log::debug!("New toplevel: {info:?}"); let app_id = info.app_id.clone(); let icon_task = iced::Task::perform( desktop_info::icon_for_app_id(app_id.clone()), move |path| Msg::UpdateToplevelIcon(app_id.clone(), path), ) .map(cosmic::Action::App); self.toplevels.0.push(Toplevel { icon: None, handle, info, img: None, }); // Close workspaces view if a window spawns while open #[cfg(not(feature = "mock-backend"))] if self.visible { return Task::batch([icon_task, self.hide()]); } return icon_task; } backend::Event::UpdateToplevel(handle, info) => { if let Some(toplevel) = self.toplevels.for_handle_mut(&handle) { let mut task = Task::none(); if toplevel.info.app_id != info.app_id { let app_id = info.app_id.clone(); task = iced::Task::perform( desktop_info::icon_for_app_id(app_id.clone()), move |path| Msg::UpdateToplevelIcon(app_id.clone(), path), ) .map(cosmic::Action::App); } toplevel.info = info; return task; } } backend::Event::CloseToplevel(handle) => { if let Some(idx) = self.toplevels.0.iter().position(|x| x.handle == handle) { self.toplevels.0.remove(idx); } } backend::Event::WorkspaceCapture(handle, image) => { //println!("Workspace capture"); if let Some(workspace) = self.workspaces.for_handle_mut(&handle) && self .capture_filter .workspace_outputs_matches(&workspace.outputs) { workspace.img = Some(image); } } backend::Event::ToplevelCapture(handle, image) => { if let Some(toplevel) = self.toplevels.for_handle_mut(&handle) { // println!("Got toplevel image!"); if self.capture_filter.toplevel_matches(&toplevel.info) { toplevel.img = Some(image); } } } backend::Event::ToplevelCapabilities(capabilities) => { self.toplevel_capabilities = capabilities; } } } Msg::Close => { return self.hide(); } Msg::ActivateWorkspace(workspace_handle) => { if let Some(workspace) = self.workspaces.for_handle(&workspace_handle) && workspace.is_active() { return self.hide(); } self.send_wayland_cmd(backend::Cmd::ActivateWorkspace(workspace_handle)); } Msg::ActivateToplevel(toplevel_handle) => { self.send_wayland_cmd(backend::Cmd::ActivateToplevel(toplevel_handle)); return self.hide(); } Msg::CloseWorkspace(_workspace_handle) => { // XXX close specific workspace /* if let WorkspaceAmount::Static(n) = &mut self.conf.workspace_config.workspace_amount { if *n != 1 { *n -= 1; self.conf .cosmic_comp_config .set("workspaces", &self.conf.workspace_config); } } */ } Msg::CloseToplevel(toplevel_handle) => { // TODO confirmation? self.send_wayland_cmd(backend::Cmd::CloseToplevel(toplevel_handle)); } Msg::StartDrag(drag_surface) => { self.drag_surface = Some((drag_surface, Default::default())); } Msg::DndEnter(drop_target, _x, _y, _mimes) => { self.drop_target = Some(drop_target); } Msg::DndLeave(drop_target) => { // Currently in iced-sctk, a `DndOfferEvent::Motion` may cause a leave event after // an enter event, based on which widget handles it first. So we need a test here. if self.drop_target == Some(drop_target) { self.drop_target = None; } } Msg::DndToplevelDrop(_toplevel) => { if let Some((DragSurface::Toplevel(handle), _)) = &self.drag_surface { match self.drop_target.take() { Some( DropTarget::WorkspaceSidebarEntry(workspace, output) | DropTarget::OutputToplevels(workspace, output), ) => { self.send_wayland_cmd(backend::Cmd::MoveToplevelToWorkspace( handle.clone(), workspace, output, )); } Some( DropTarget::WorkspacesBar(_) | DropTarget::WorkspaceSidebarDragPlaceholder(_, _), ) | None => {} } } } Msg::NewWorkspace => { /* if let WorkspaceAmount::Static(n) = &mut self.conf.workspace_config.workspace_amount { *n += 1; self.conf .cosmic_comp_config .set("workspaces", &self.conf.workspace_config); } */ } Msg::Config(c) => { self.conf.config = c; } Msg::CompConfig(c) => { self.conf.workspace_config = c.workspaces; } Msg::BgConfig(c) => { self.conf.bg = c; } Msg::UpdateToplevelIcon(app_id, path) => { for toplevel in self.toplevels.0.iter_mut() { if toplevel.info.app_id == app_id { toplevel.icon = path.clone(); } } } Msg::OnScroll(output, delta) => { let discrete_delta = self.scroll.update(delta); if discrete_delta.y != 0 { // TODO assumes only one active workspace per output let workspaces = self.workspaces.for_output(&output).collect::>(); if let Some(workspace_idx) = workspaces.iter().position(|i| i.is_active()) { // Add delta_num, to index wrapping around let new_workspace_idx = (workspace_idx as isize - discrete_delta.y) .rem_euclid(workspaces.len() as isize) as usize; let workspace = workspaces[new_workspace_idx]; self.send_wayland_cmd(backend::Cmd::ActivateWorkspace( workspace.handle().clone(), )); } } } Msg::DndWorkspaceDrag => {} Msg::DndWorkspaceDrop(_workspace) => { if let Some((DragSurface::Workspace(handle), _)) = &self.drag_surface { match self.drop_target.take() { Some( DropTarget::WorkspaceSidebarEntry(other_handle, _output) | DropTarget::WorkspaceSidebarDragPlaceholder(other_handle, _output), ) => { let workspace = self.workspaces.for_handle(handle); let other_workspace = self.workspaces.for_handle(&other_handle); if let (Some(workspace), Some(other_workspace)) = (workspace, other_workspace) { if workspace.outputs == other_workspace.outputs && workspace.info.coordinates[0] + 1 == other_workspace.info.coordinates[0] { // Workspace is already in requested position } else { self.send_wayland_cmd(backend::Cmd::MoveWorkspaceBefore( handle.clone(), other_handle, )); } } } Some(DropTarget::OutputToplevels(_, _) | DropTarget::WorkspacesBar(_)) | None => {} } } } Msg::TogglePinned(workspace_handle) => { if let Some(workspace) = self.workspaces.for_handle(&workspace_handle) { self.send_wayland_cmd(backend::Cmd::SetWorkspacePinned( workspace_handle, !workspace.is_pinned(), )); } } Msg::EnteredWorkspaceSidebarEntry(workspace_handle, entered) => { if let Some(workspace) = self.workspaces.for_handle_mut(&workspace_handle) { workspace.has_cursor = entered; } } Msg::DbusInterface(interface) => { if let Ok(interface) = interface { self.dbus_interface = Some(interface); } } Msg::DBus(evt) => { return match evt { dbus::Event::Show => self.show(), dbus::Event::Hide => self.hide(), }; } Msg::PanelContainerEntries(entries) => { self.panel_configs.retain(|k, _| entries.contains(k)); for entry in entries { self.panel_configs.entry(entry).or_insert(None); } } Msg::PanelConfig(config) => { self.panel_configs.insert(config.name.clone(), Some(config)); } Msg::ActionOnTyping(input) => { let cmd = match self.conf.workspace_config.action_on_typing { cosmic_comp_config::workspace::Action::None => return Task::none(), cosmic_comp_config::workspace::Action::OpenLauncher => { Some("cosmic-launcher \"$@\"".to_string()) } cosmic_comp_config::workspace::Action::OpenApplications => { Some("cosmic-app-library \"$@\"".to_string()) } }; if let Some(cmd) = cmd { tokio::spawn(async { let mut child = Command::new("/bin/sh") .arg("-c") .arg(cmd) .arg("_") .arg("input") .arg(input) .spawn() .unwrap(); let _ = child.wait().unwrap(); }); } self.action_on_typing_activated = true; } Msg::Ignore => {} } Task::none() } fn dbus_activation(&mut self, msg: dbus_activation::Message) -> Task> { if let dbus_activation::Details::Activate = msg.msg { self.toggle() } else { Task::none() } } fn dbus_connection(&mut self, conn: zbus::Connection) -> Task> { Task::perform(dbus::Interface::new(conn), Msg::DbusInterface).map(cosmic::Action::App) } fn subscription(&self) -> Subscription { let events = iced::event::listen_with(|evt, _, _| match evt { iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(evt)) => { if !matches!(evt, WaylandEvent::RequestResize) { Some(Msg::WaylandEvent(evt)) } else { None } } iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(Msg::Close), iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { key: Key::Character(key), modifiers, .. }) if !modifiers.control() && !modifiers.alt() && !modifiers.logo() => { Some(Msg::ActionOnTyping(key.to_string())) } // XXX Workaround for `on_finish`/`on_cancel` not being called, seemingly // due to state diffing behavior. iced::Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { Some(Msg::SourceFinished) } _ => None, }); let config_subscription = cosmic_config::config_subscription::<_, CosmicWorkspacesConfig>( "config-sub", "com.system76.CosmicWorkspaces".into(), 1, ) .map(|update| { if !update.errors.is_empty() { log::error!("Failed to load workspaces config: {:?}", update.errors); } Msg::Config(update.config) }); let comp_config_subscription = cosmic_config::config_subscription::<_, CosmicCompConfig>( "comp-config-sub", "com.system76.CosmicComp".into(), 1, ) .map(|update| { if !update.errors.is_empty() { log::error!("Failed to load compositor config: {:?}", update.errors); } Msg::CompConfig(Box::new(update.config)) }); let bg_subscription = cosmic_config::config_state_subscription::<_, cosmic_bg_config::state::State>( "bg-sub", cosmic_bg_config::NAME.into(), cosmic_bg_config::state::State::version(), ) .map(|update| { if !update.errors.is_empty() { log::error!("Failed to load bg config: {:?}", update.errors); } Msg::BgConfig(update.config) }); let mut subscriptions = vec![ events, config_subscription, comp_config_subscription, bg_subscription, ]; if let Some(conn) = self.conn.clone() { subscriptions.push(backend::subscription(conn).map(Msg::Wayland)); } if let Some(interface) = &self.dbus_interface { subscriptions.push(interface.subscription().map(Msg::DBus)); } subscriptions.push(panel_subscriptions(self.panel_configs.keys())); iced::Subscription::batch(subscriptions) } fn view(&self) -> cosmic::Element<'_, Self::Message> { unreachable!() } fn view_window(&self, id: iced::window::Id) -> cosmic::Element<'_, Self::Message> { if let Some(surface) = self.layer_surfaces.get(&id) { return view::layer_surface(self, surface); } log::error!("non-existant layer shell id {}?", id); cosmic::widget::text("workspaces").into() } fn on_close_requested(&self, _id: SurfaceId) -> Option { None } fn core(&self) -> &cosmic::app::Core { &self.core } fn core_mut(&mut self) -> &mut cosmic::app::Core { &mut self.core } } fn panel_subscriptions<'a>( container_entries: impl Iterator, ) -> Subscription { let mut subscriptions = vec![ cosmic_config::config_subscription::<_, CosmicPanelContainerConfigEntry>( "panel-config-subscription", "com.system76.CosmicPanel".into(), 1, ) .map(|update| Msg::PanelContainerEntries(update.config.entries)), ]; for entry in container_entries { subscriptions.push( cosmic_config::config_subscription::<_, CosmicPanelConfig>( ("panel-config-subscription", entry.to_owned()), format!("com.system76.CosmicPanel.{}", entry).into(), 1, ) .map(|update| Msg::PanelConfig(update.config)), ); } iced::Subscription::batch(subscriptions) } fn init_localizer() { let localizer = crate::localize::localizer(); let requested_languages = DesktopLanguageRequester::requested_languages(); if let Err(why) = localizer.select(&requested_languages) { log::error!("error while loading fluent localizations: {}", why); } } pub fn main() -> iced::Result { env_logger::init(); init_localizer(); cosmic::app::run_single_instance::( cosmic::app::Settings::default() .no_main_window(true) .exit_on_close(false), Args::parse(), ) }