Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 205 additions & 6 deletions src/components/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ use cosmic::{
alignment::Horizontal,
event::{
self, listen_with,
wayland::{self, LayerEvent, OverlapNotifyEvent},
wayland::{self, LayerEvent, OutputEvent, OverlapNotifyEvent},
},
keyboard::{Key, key::Named},
platform_specific::shell::commands::activation::request_token,
time,
window::Id as SurfaceId,
},
iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings,
iced_runtime::platform_specific::wayland::layer_surface::{
IcedOutput, SctkLayerSurfaceSettings,
},
iced_winit::commands::layer_surface::{
Anchor, KeyboardInteractivity, destroy_layer_surface, get_layer_surface,
},
Expand All @@ -49,6 +51,9 @@ use std::{
};
use zbus::Connection;

// Type alias for Wayland output. Matches what's used in SctkLayerSurfaceSettings
type WlOutput = cosmic::cctk::sctk::reexports::client::protocol::wl_output::WlOutput;

const COUNTDOWN_LENGTH: u8 = 60;
static CONFIRM_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("confirm-id"));
static AUTOSIZE_DIALOG_ID: LazyLock<iced::id::Id> =
Expand All @@ -66,6 +71,10 @@ pub struct Args {
pub enum OsdTask {
#[clap(about = "Display external display toggle indicator")]
Display,
#[clap(about = "Show numbers on all displays for identification")]
IdentifyDisplays,
#[clap(about = "Dismiss display identification numbers")]
DismissDisplayIdentifiers,
#[clap(about = "Toggle the on screen display and start the log out timer")]
LogOut,
#[clap(about = "Toggle the on screen display and start the restart timer")]
Expand Down Expand Up @@ -114,6 +123,8 @@ impl OsdTask {
.map(msg),
OsdTask::Touchpad => Task::none(),
OsdTask::Display => Task::none(),
OsdTask::IdentifyDisplays => Task::none(),
OsdTask::DismissDisplayIdentifiers => Task::none(),
}
}
}
Expand Down Expand Up @@ -269,10 +280,16 @@ pub enum Msg {
SoundSettings,
TouchpadEnabled(Option<TouchpadOverride>),
ActivationToken(Option<String>),
DisplayIdentifierSurface((SurfaceId, osd_indicator::Msg)),
ResetDisplayIdentifierTimer(SurfaceId),
CreateDisplayIdentifiers(Vec<(String, u32)>),
DismissDisplayIdentifiers,
OutputInfo(WlOutput, String),
}

enum Surface {
PolkitDialog(polkit_dialog::State),
OsdIndicator(osd_indicator::State),
}

struct App {
Expand All @@ -294,6 +311,7 @@ struct App {
overlap: HashMap<String, Rectangle>,
size: Option<Size>,
action_to_confirm: Option<(SurfaceId, OsdTask, u8)>,
wayland_outputs: HashMap<String, (WlOutput, String)>,
}

impl App {
Expand Down Expand Up @@ -388,6 +406,7 @@ impl cosmic::Application for App {
overlap: HashMap::new(),
size: None,
action_to_confirm: None,
wayland_outputs: HashMap::new(),
},
Task::none(),
)
Expand Down Expand Up @@ -498,6 +517,18 @@ impl cosmic::Application for App {
}
Task::none()
}
Msg::DisplayIdentifierSurface((id, msg)) => {
if let Some(Surface::OsdIndicator(state)) = self.surfaces.remove(&id) {
let (state, cmd) = state.update(msg);
if let Some(state) = state {
self.surfaces.insert(id, Surface::OsdIndicator(state));
}
return cmd.map(move |msg| {
cosmic::action::app(Msg::DisplayIdentifierSurface((id, msg)))
});
}
Task::none()
}
Msg::OsdIndicator(msg) => {
if let Some((id, state)) = self.indicator.take() {
let (state, cmd) = state.update(msg);
Expand Down Expand Up @@ -717,6 +748,110 @@ impl cosmic::Application for App {
self.indicator = Some((id, state));
cmd.map(|x| cosmic::Action::App(Msg::OsdIndicator(x)))
}
Msg::OutputInfo(output, name) => {
self.wayland_outputs.insert(name.clone(), (output, name));
Task::none()
}
Msg::CreateDisplayIdentifiers(displays) => {
if displays.is_empty() {
return Task::none();
}

// Build a map of display name to number from the requested displays
let requested_displays: std::collections::HashMap<String, u32> =
displays.iter().cloned().collect();

let mut existing_identifiers: std::collections::HashMap<u32, SurfaceId> =
std::collections::HashMap::new();

// Find existing display identifiers and collect ones to remove
let mut ids_to_remove = Vec::new();
for (id, surface) in &self.surfaces {
if let Surface::OsdIndicator(state) = surface {
if let osd_indicator::Params::DisplayNumber(num) = state.params() {
// Check if this display number is still in the requested list
if requested_displays.values().any(|&n| n == *num) {
existing_identifiers.insert(*num, *id);
} else {
ids_to_remove.push(*id);
}
}
}
}

// Remove display identifiers that are no longer needed
let mut tasks = Vec::new();
for id in ids_to_remove {
self.surfaces.remove(&id);
tasks.push(destroy_layer_surface(id));
}

// Process each requested display
for (display_name, display_number) in displays {
if let Some(&existing_id) = existing_identifiers.get(&display_number) {
// Display identifier already exists, reset its timer
tasks.push(Task::done(cosmic::Action::App(
Msg::ResetDisplayIdentifierTimer(existing_id),
)));
} else {
// Create new display identifier
let id = SurfaceId::unique();

// Find the matching wayland output for this display
let iced_output = self
.wayland_outputs
.get(&display_name)
.map(|(output, _)| IcedOutput::Output(output.clone()))
.unwrap_or(IcedOutput::Active);

let (state, cmd) = osd_indicator::State::new_with_output(
id,
osd_indicator::Params::DisplayNumber(display_number),
iced_output,
);

self.surfaces.insert(id, Surface::OsdIndicator(state));
tasks.push(cmd.map(move |msg| {
cosmic::action::app(Msg::DisplayIdentifierSurface((id, msg)))
}));
}
}

Task::batch(tasks)
}
Msg::ResetDisplayIdentifierTimer(id) => {
if let Some(Surface::OsdIndicator(state)) = self.surfaces.get_mut(&id) {
return state.reset_display_identifier_timer().map(move |msg| {
cosmic::action::app(Msg::DisplayIdentifierSurface((id, msg)))
});
}
Task::none()
}
Msg::DismissDisplayIdentifiers => {
let mut tasks = Vec::new();
let ids_to_remove: Vec<SurfaceId> = self
.surfaces
.iter()
.filter_map(|(id, surface)| {
if let Surface::OsdIndicator(state) = surface {
if matches!(state.params(), osd_indicator::Params::DisplayNumber(_)) {
Some(*id)
} else {
None
}
} else {
None
}
})
.collect();

for id in ids_to_remove {
self.surfaces.remove(&id);
tasks.push(destroy_layer_surface(id));
}

Task::batch(tasks)
}
}
}

Expand Down Expand Up @@ -746,8 +881,31 @@ impl cosmic::Application for App {
event::Event::Window(iced::window::Event::Resized(s)) => Some(Msg::Size(s)),
event::Event::PlatformSpecific(event::PlatformSpecific::Wayland(wayland_event)) => {
match wayland_event {
event::wayland::Event::OverlapNotify(event, _, _) => Some(Msg::Overlap(event)),
event::wayland::Event::OverlapNotify(event, ..) => Some(Msg::Overlap(event)),
wayland::Event::Layer(LayerEvent::Unfocused, ..) => Some(Msg::Cancel),
event::wayland::Event::Output(output_event, output) => {
match output_event {
OutputEvent::Created(Some(info)) => {
// Track this output for creating per-display surfaces
if let Some(name) = info.name.clone() {
log::debug!("Output Created: {}", name);
Some(Msg::OutputInfo(output.clone(), name))
} else {
None
}
}
OutputEvent::InfoUpdate(info) => {
// Update existing output info
if let Some(name) = info.name.clone() {
log::debug!("Output InfoUpdate: {}", name);
Some(Msg::OutputInfo(output.clone(), name))
} else {
None
}
}
OutputEvent::Created(None) | OutputEvent::Removed => None,
}
}
_ => None,
}
}
Expand All @@ -760,9 +918,16 @@ impl cosmic::Application for App {
_ => None,
}));

subscriptions.extend(self.surfaces.iter().map(|(id, surface)| match surface {
Surface::PolkitDialog(state) => state.subscription().with(*id).map(Msg::PolkitDialog),
}));
subscriptions.extend(
self.surfaces
.iter()
.filter_map(|(id, surface)| match surface {
Surface::PolkitDialog(state) => {
Some(state.subscription().with(*id).map(Msg::PolkitDialog))
}
Surface::OsdIndicator(_) => None, // OSD indicators don't have subscriptions
}),
);
if self.action_to_confirm.is_some() {
subscriptions.push(time::every(Duration::from_millis(1000)).map(|_| Msg::Countdown));
}
Expand All @@ -780,6 +945,9 @@ impl cosmic::Application for App {
Surface::PolkitDialog(state) => {
state.view().map(move |msg| Msg::PolkitDialog((id, msg)))
}
Surface::OsdIndicator(state) => state
.view()
.map(move |msg| Msg::DisplayIdentifierSurface((id, msg))),
};
} else if let Some((indicator_id, state)) = &self.indicator {
if id == *indicator_id {
Expand All @@ -796,6 +964,8 @@ impl cosmic::Application for App {
OsdTask::ConfirmHeadphones { .. } => "confirm-device-type",
OsdTask::Touchpad => "touchpad",
OsdTask::Display => "external-display",
OsdTask::IdentifyDisplays => "identify-displays",
OsdTask::DismissDisplayIdentifiers => "dismiss-display-identifiers",
};

let title = fl!(
Expand Down Expand Up @@ -1099,6 +1269,35 @@ impl cosmic::Application for App {

Msg::Display(enabled)
});
} else if let OsdTask::IdentifyDisplays = cmd {
return cosmic::task::future(async move {
let Ok(output_lists) = cosmic_randr_shell::list().await else {
log::error!("Failed to list displays with cosmic-randr");
return Msg::CreateDisplayIdentifiers(Vec::new());
};

// Get all enabled outputs and number them the same way cosmic-settings does:
// Sort alphabetically by output name, then assign numbers 1, 2, 3...
use std::collections::BTreeMap;

let sorted_outputs: BTreeMap<&str, _> = output_lists
.outputs
.iter()
.filter(|(_, o)| o.enabled)
.map(|(key, output)| (output.name.as_str(), (key, output)))
.collect();

let displays: Vec<(String, u32)> = sorted_outputs
.into_iter()
.enumerate()
.map(|(index, (name, _))| (name.to_string(), (index + 1) as u32))
.collect();

Msg::CreateDisplayIdentifiers(displays)
})
.map(cosmic::Action::App);
} else if let OsdTask::DismissDisplayIdentifiers = cmd {
return Task::done(cosmic::Action::App(Msg::DismissDisplayIdentifiers));
}

if let Some(prev) = self.action_to_confirm.take() {
Expand Down
Loading