feat: load and apply output configs

fix: typo

chore: update deps

cleanup
This commit is contained in:
Ashley Wulber 2025-08-19 13:22:47 -04:00 committed by Ashley Wulber
parent 7317353a32
commit f3f7d5cfb6
5 changed files with 804 additions and 189 deletions

630
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -77,6 +77,9 @@ i18n-embed-fl = "0.7"
rust-embed = "8"
futures-util = "0.3.30"
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr", default-features = false }
tachyonix = "0.3.1"
itoa = "1.0.15"
[dependencies.greetd_ipc]
version = "0.10.3"
@ -89,6 +92,7 @@ networkmanager = ["cosmic-dbus-networkmanager", "zbus"]
upower = ["upower_dbus", "zbus"]
zbus = ["dep:zbus", "nix"]
[profile.dev.package.tiny-skia]
opt-level = 2
@ -108,6 +112,7 @@ rust-version = "1.85.0"
[workspace.dependencies]
env_logger = "0.10.2"
log = "0.4.22"
# Fix zbus compilation by manually adding nix with user feature
nix = { version = "0.29", features = ["user"] }
pwd = "1.4.0"
@ -126,7 +131,10 @@ default-features = false
[workspace.dependencies.cosmic-comp-config]
git = "https://github.com/pop-os/cosmic-comp"
branch = "refactor-config"
# path = "../cosmic-comp/cosmic-comp-config"
default-features = false
features = ["output"]
[workspace.dependencies.cosmic-greeter-config]
path = "cosmic-greeter-config"

View file

@ -21,3 +21,4 @@ serde.workspace = true
zbus.workspace = true
#TODO: reduce features
tokio = { workspace = true, features = ["full"] }
xdg = "3.0.0"

View file

@ -1,3 +1,4 @@
use cosmic_comp_config::output::{self, OutputsConfig};
use cosmic_config::CosmicConfigEntry;
use std::{
collections::BTreeMap,
@ -23,6 +24,7 @@ pub struct UserData {
pub xkb_config_opt: Option<XkbConfig>,
pub time_applet_config: TimeAppletConfig,
pub accessibility_zoom: ZoomConfig,
pub outputs: Option<OutputsConfig>,
}
impl UserData {
@ -175,6 +177,12 @@ impl UserData {
}
};
let xdg = xdg::BaseDirectories::new();
self.outputs = xdg.get_state_home().map(|mut s| {
s.push("cosmic-comp/outputs.ron");
output::load_outputs(Some(&s))
});
match cosmic_config::Config::new("com.system76.CosmicAppletTime", TimeAppletConfig::VERSION)
{
Ok(config_handler) => match TimeAppletConfig::get_entry(&config_handler) {

View file

@ -17,7 +17,7 @@ use cosmic::{
iced::{
self, Background, Border, Length, Subscription, alignment,
event::wayland::OutputEvent,
futures::SinkExt,
futures::{self, SinkExt},
platform_specific::{
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
shell::wayland::commands::layer_surface::{
@ -32,8 +32,12 @@ use cosmic::{
cosmic_theme::{self, CosmicPalette},
surface,
};
use cosmic_comp_config::output::{OutputConfig, OutputInfo, OutputState};
use cosmic_greeter_config::Config as CosmicGreeterConfig;
use cosmic_greeter_daemon::UserData;
use cosmic_randr_shell::{
AdaptiveSyncAvailability, AdaptiveSyncState, List, Output, OutputKey, Transform,
};
use cosmic_settings_subscriptions::cosmic_a11y_manager::{
AccessibilityEvent, AccessibilityRequest,
};
@ -50,6 +54,7 @@ use std::{
time::{Duration, Instant},
};
use tokio::process::Child;
use tokio::sync::oneshot;
use tokio::time;
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
use zbus::{Connection, proxy};
@ -352,6 +357,11 @@ pub enum Message {
Exit,
// Sets channel used to communicate with the greetd IPC subscription.
GreetdChannel(tokio::sync::mpsc::Sender<Request>),
/// Refreshes display outputs.
RandrUpdate {
/// Available outputs from cosmic-randr.
randr: Arc<Result<List, cosmic_randr_shell::Error>>,
},
Heartbeat,
KeyboardLayout(usize),
Login,
@ -394,9 +404,23 @@ pub struct App {
entering_name: bool,
theme_builder: cosmic_theme::ThemeBuilder,
randr_list: Option<cosmic_randr_shell::List>,
accessibility: Accessibility,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Randr {
Mirror(OutputKey),
Position(i32, i32),
RefreshRate(u32),
VariableRefreshRate(AdaptiveSyncState),
Resolution(u32, u32),
Scale(u32),
Transform(Transform),
Toggle(bool),
}
#[derive(Default)]
struct Accessibility {
pub wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
@ -412,6 +436,260 @@ struct Accessibility {
}
impl App {
/// Applies a display configuration via `cosmic-randr`.
fn exec_randr(&self, output: Output, user_config: OutputConfig) -> Task<Message> {
let Some(current_mode) = output.current else {
log::warn!("Current output mode missing...");
return Task::none();
};
let Some(current_mode) = self
.randr_list
.as_ref()
.and_then(|l| l.modes.get(current_mode))
else {
log::warn!("Mode key does not exist...");
return Task::none();
};
let Some(list) = self.randr_list.as_ref() else {
return Task::none();
};
let mut tasks = Vec::new();
let name = &*output.name;
// Mirror
let cur_state = if output.enabled {
output
.mirroring
.map(|n| OutputState::Mirroring(n))
.unwrap_or(OutputState::Enabled)
} else {
OutputState::Disabled
};
// Enable/Disable or Mirror
if user_config.enabled != cur_state {
match user_config.enabled {
OutputState::Enabled => {
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("enable").arg(name);
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
OutputState::Disabled => {
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("disable").arg(name);
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
OutputState::Mirroring(ref mirror_name) => {
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mirror").arg(&output.name).arg(mirror_name);
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
}
}
// Position
if (user_config.position.0 as i32, user_config.position.1 as i32) != output.position {
let (x, y) = user_config.position;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg("--pos-x")
.arg(itoa::Buffer::new().format(x))
.arg("--pos-y")
.arg(itoa::Buffer::new().format(y))
.arg(name)
.arg(itoa::Buffer::new().format(current_mode.size.0))
.arg(itoa::Buffer::new().format(current_mode.size.1));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
// RefreshRate
if user_config.mode.1 != Some(current_mode.refresh_rate) {
let rate = current_mode.refresh_rate;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg("--refresh")
.arg(format!("{}.{:03}", rate / 1000, rate % 1000))
.arg(name)
.arg(itoa::Buffer::new().format(current_mode.size.0))
.arg(itoa::Buffer::new().format(current_mode.size.1));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
let configured_vrr = match user_config.vrr {
cosmic_comp_config::output::AdaptiveSync::Enabled => {
cosmic_randr_shell::AdaptiveSyncState::Auto
}
cosmic_comp_config::output::AdaptiveSync::Disabled => {
cosmic_randr_shell::AdaptiveSyncState::Disabled
}
cosmic_comp_config::output::AdaptiveSync::Force => {
cosmic_randr_shell::AdaptiveSyncState::Always
}
};
// VariableRefreshRate
if Some(configured_vrr) != output.adaptive_sync {
let mode = configured_vrr;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg("--adaptive-sync")
.arg(format!("{}", mode))
.arg(name)
.arg(itoa::Buffer::new().format(current_mode.size.0))
.arg(itoa::Buffer::new().format(current_mode.size.1));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
// Resolution
if (user_config.mode.0.0 as u32, user_config.mode.0.1 as u32) != current_mode.size {
let (width, height) = user_config.mode.0;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg(name)
.arg(itoa::Buffer::new().format(width))
.arg(itoa::Buffer::new().format(height));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
// Scale
if user_config.scale != user_config.scale {
let scale = user_config.scale;
let rate = current_mode.refresh_rate;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg("--scale")
.arg(format!("{:02}", scale / 100.))
.arg("--refresh")
.arg(format!("{}.{:03}", rate / 1000, rate % 1000))
.arg(name)
.arg(itoa::Buffer::new().format(current_mode.size.0))
.arg(itoa::Buffer::new().format(current_mode.size.1));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
// Transform
let configured_transform = match user_config.transform {
cosmic_comp_config::output::TransformDef::Normal => {
cosmic_randr_shell::Transform::Normal
}
cosmic_comp_config::output::TransformDef::_90 => {
cosmic_randr_shell::Transform::Rotate90
}
cosmic_comp_config::output::TransformDef::_180 => {
cosmic_randr_shell::Transform::Rotate180
}
cosmic_comp_config::output::TransformDef::_270 => {
cosmic_randr_shell::Transform::Rotate270
}
cosmic_comp_config::output::TransformDef::Flipped => {
cosmic_randr_shell::Transform::Flipped
}
cosmic_comp_config::output::TransformDef::Flipped90 => {
cosmic_randr_shell::Transform::Flipped90
}
cosmic_comp_config::output::TransformDef::Flipped180 => {
cosmic_randr_shell::Transform::Flipped180
}
cosmic_comp_config::output::TransformDef::Flipped270 => {
cosmic_randr_shell::Transform::Flipped270
}
};
if Some(configured_transform) != output.transform {
let transform = configured_transform;
let mut task = tokio::process::Command::new("cosmic-randr");
task.arg("mode")
.arg("--transform")
.arg(&*format!("{transform}"))
.arg(name)
.arg(itoa::Buffer::new().format(current_mode.size.0))
.arg(itoa::Buffer::new().format(current_mode.size.1));
tasks.push(
cosmic::task::future::<(), ()>(async move {
log::debug!("executing {task:?}");
let status = task.status().await;
if let Err(err) = status {
log::error!("Randr error: {err:?}");
}
})
.discard(),
);
}
Task::batch(tasks)
}
fn menu(&self, id: SurfaceId) -> Element<Message> {
let window_width = self
.common
@ -587,7 +865,7 @@ impl App {
"applications-accessibility-symbolic",
))
.padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Accessibility)), // We'll use Dropdown::Keyboard as a dummy, since we don't have a dedicated Dropdown for accessibility
.on_press(Message::DropdownToggle(Dropdown::Accessibility)),
)
.position(widget::popover::Position::Bottom);
@ -916,7 +1194,13 @@ impl App {
let mut tasks = Vec::new();
self.accessibility.magnifier = user_data.accessibility_zoom.start_on_login;
self.randr_list = None;
tasks.push(cosmic::Task::future(async {
let randr_fut = cosmic_randr_shell::list().await;
cosmic::action::app(Message::RandrUpdate {
randr: Arc::new(randr_fut),
})
}));
if let Some(theme) = &user_data.theme_opt {
self.accessibility.high_contrast = theme.is_high_contrast;
tasks.push(cosmic::command::set_theme(cosmic::Theme::custom(Arc::new(
@ -952,10 +1236,12 @@ impl cosmic::Application for App {
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
let mut tasks = Vec::new();
let (mut common, common_task) = Common::init(core);
common.on_output_event = Some(Box::new(|output_event, output| {
Message::OutputEvent(output_event, output)
}));
tasks.push(common_task);
//TODO: use full_name?
let mut usernames: Vec<_> = flags
@ -1017,8 +1303,9 @@ impl cosmic::Application for App {
entering_name: false,
accessibility,
theme_builder: Default::default(),
randr_list: None,
};
(app, common_task)
(app, Task::batch(tasks))
}
/// Handle application events here.
@ -1027,7 +1314,6 @@ impl cosmic::Application for App {
Message::Common(common_message) => {
return self.common.update(common_message);
}
Message::OutputEvent(output_event, output) => {
match output_event {
OutputEvent::Created(output_info_opt) => {
@ -1579,6 +1865,56 @@ impl cosmic::Application for App {
self.accessibility.wayland_sender = Some(tx);
}
},
Message::RandrUpdate { randr } => match randr.as_ref() {
Ok(outputs) => {
let mut tasks = Vec::new();
self.randr_list = Some(outputs.clone());
let mut output_pairs: Vec<(Output, OutputInfo, OutputConfig)> = Vec::new();
let Some(cur_user_output_state) = self
.selected_username
.data_idx
.and_then(|i| self.flags.user_datas.get(i))
.and_then(|user_data| user_data.outputs.as_ref())
else {
return Task::none();
};
'outer: for (i, (configured_info, output_configs)) in
cur_user_output_state.config.iter().enumerate()
{
if configured_info.len() != outputs.outputs.len() {
continue;
}
let mut matching_outputs = Vec::new();
for o in outputs.outputs.values() {
if let Some(pos) = configured_info.iter().position(|configured| {
configured.connector == o.name
&& configured.make == o.make.clone().unwrap_or_default()
&& configured.model == o.model
}) {
matching_outputs.push((
o.clone(),
configured_info[pos].clone(),
output_configs[pos].clone(),
));
} else {
continue 'outer;
}
}
output_pairs = matching_outputs;
}
for (randr_o, _info, user_config) in output_pairs {
tasks.push(self.exec_randr(randr_o, user_config))
}
return Task::batch(tasks);
}
Err(err) => {
log::error!("Randr error: {err}");
}
},
}
Task::none()
}