diff --git a/Cargo.lock b/Cargo.lock index 811b65a..61ff5b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "rustbus" version = "0.19.3" -source = "git+https://github.com/KillingSpark/rustbus?rev=bf87291#bf872910143397c6dd3185dd989fbf606ded3a40" +source = "git+https://github.com/KillingSpark/rustbus?rev=20af8f2#20af8f2a4296196779e1044c0c08d4e5f784fc61" dependencies = [ "nix", "rustbus_derive", @@ -202,7 +202,7 @@ dependencies = [ [[package]] name = "rustbus-service" version = "0.1.0" -source = "git+https://github.com/MaxVerevkin/rustbus-service?rev=cffa791#cffa79167032ce5b5b23097d5be87603e958f8f9" +source = "git+https://github.com/MaxVerevkin/rustbus-service?rev=7401913#7401913977ccf665748b1f4dee033921e1c86afd" dependencies = [ "rustbus", "rustbus-service-macros", @@ -211,7 +211,7 @@ dependencies = [ [[package]] name = "rustbus-service-macros" version = "0.1.0" -source = "git+https://github.com/MaxVerevkin/rustbus-service?rev=cffa791#cffa79167032ce5b5b23097d5be87603e958f8f9" +source = "git+https://github.com/MaxVerevkin/rustbus-service?rev=7401913#7401913977ccf665748b1f4dee033921e1c86afd" dependencies = [ "proc-macro-crate", "quote", @@ -220,8 +220,8 @@ dependencies = [ [[package]] name = "rustbus_derive" -version = "0.5.0" -source = "git+https://github.com/KillingSpark/rustbus?rev=bf87291#bf872910143397c6dd3185dd989fbf606ded3a40" +version = "0.6.0" +source = "git+https://github.com/KillingSpark/rustbus?rev=20af8f2#20af8f2a4296196779e1044c0c08d4e5f784fc61" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b81ea18..1be36a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ bytemuck = "1.13" clap = { version = "4.0", default-features = false, features = ["std", "help", "usage", "derive"] } libc = "0.2" memmap2 = "0.9" -rustbus-service = { git = "https://github.com/MaxVerevkin/rustbus-service", rev = "cffa791" } +rustbus-service = { git = "https://github.com/MaxVerevkin/rustbus-service", rev = "7401913" } shmemfdrs2 = "1.0" wayrs-client = "1.0" wayrs-protocols = { version = "0.13", features = ["wlr-gamma-control-unstable-v1"] } diff --git a/README.md b/README.md index 21292ce..b9328a2 100644 --- a/README.md +++ b/README.md @@ -114,3 +114,53 @@ busctl --user call rs.wl-gammarelay / rs.wl.gammarelay UpdateGamma d 0.1 # Decrease gamma by `0.1`: busctl --user -- call rs.wl-gammarelay / rs.wl.gammarelay UpdateGamma d -0.1 ``` + +## With multiple outputs + +Each connected output is listed under `/outputs` and its properties can be seen and edited separately. For example, a laptop with an internal "eDP-1" monitor and a "HDMI-A-1" output has the following DBus objects: + +```sh +$ busctl --user tree rs.wl-gammarelay +└─ /outputs + ├─ /outputs/HDMI_A_1 + └─ /outputs/eDP_1 +``` + +You can operate on a specific output using its object path: + +```sh +# Set the temperature to `5000` for the HDMI output +busctl --user set-property rs.wl-gammarelay /outputs/HDMI_A_1 rs.wl.gammarelay Temperature q 5000 + +# Set the temperature to `9000` for the internal monitor +busctl --user set-property rs.wl-gammarelay /outputs/eDP_1 rs.wl.gammarelay Temperature q 9000 +``` + +When there are several outputs, the values shown are: + +- for the brightness, temperature and gamma, the average of all outputs' values +- for the inverted boolean, true if all outputs are inverted and false otherwise + +When updating the brightness, temperature or gamma value, the modification is applied to each output: + +```sh +# Get the values +$ busctl --user -- get-property rs.wl-gammarelay /outputs/eDP_1 rs.wl.gammarelay Brightness +d 0.7 +$ busctl --user -- get-property rs.wl-gammarelay /outputs/HDMI_A_1 rs.wl.gammarelay Brightness +d 0.5 + +# Update all outputs +$ busctl --user -- call rs.wl-gammarelay / rs.wl.gammarelay UpdateBrightness d 0.05 + +# Get the values again +$ busctl --user -- get-property rs.wl-gammarelay /outputs/eDP_1 rs.wl.gammarelay Brightness +d 0.75 +$ busctl --user -- get-property rs.wl-gammarelay /outputs/HDMI_A_1 rs.wl.gammarelay Brightness +d 0.55 +``` + +When toggling the inverted status: + +- if all the monitors are inverted, then all of them are reverted to normal +- otherwise, all monitors are made inverted, even if they already were inverted diff --git a/src/dbus_server.rs b/src/dbus_server.rs index d961e2c..bfbd219 100644 --- a/src/dbus_server.rs +++ b/src/dbus_server.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use std::os::fd::{AsRawFd, RawFd}; -use crate::State; +use crate::color::Color; +use crate::WaylandState; use anyhow::Result; use rustbus::{ connection::Timeout, @@ -16,7 +17,7 @@ use rustbus_service::{Access, InterfaceImp, MethodContext, PropContext, Service} pub struct DbusServer { conn: DuplexConn, - service: Service, + service: Service, } impl AsRawFd for DbusServer { @@ -45,61 +46,316 @@ impl DbusServer { return Ok(None); } - let gammarelay_iface = InterfaceImp::new("rs.wl.gammarelay") - .with_method::<(), ()>("ToggleInverted", toggle_inverted_cb) - .with_method::("UpdateTemperature", update_temperature_cb) - .with_method::("UpdateGamma", update_gamma_cb) - .with_method::("UpdateBrightness", update_brightness_cb) + let gammarelay_root_iface = InterfaceImp::new("rs.wl.gammarelay") + .with_method::<(), ()>("ToggleInverted", toggle_inverted_root_cb) + .with_method::( + "UpdateTemperature", + update_temperature_root_cb, + ) + .with_method::("UpdateGamma", update_gamma_root_cb) + .with_method::("UpdateBrightness", update_brightness_root_cb) .with_prop( "Inverted", - Access::ReadWrite(get_inverted_cb, set_inverted_cb), + Access::ReadWrite(get_inverted_root_cb, set_inverted_root_cb), ) .with_prop( "Temperature", - Access::ReadWrite(get_temperature_cb, set_temperature_cb), + Access::ReadWrite(get_temperature_root_cb, set_temperature_root_cb), + ) + .with_prop( + "Gamma", + Access::ReadWrite(get_gamma_root_cb, set_gamma_root_cb), ) - .with_prop("Gamma", Access::ReadWrite(get_gamma_cb, set_gamma_cb)) .with_prop( "Brightness", - Access::ReadWrite(get_brightness_cb, set_brightness_cb), + Access::ReadWrite(get_brightness_root_cb, set_brightness_root_cb), ); - service.root_mut().add_interface(gammarelay_iface); + let root = service.root_mut(); + root.add_interface(gammarelay_root_iface); + root.add_child("outputs", rustbus_service::Object::new()); Ok(Some(Self { conn, service })) } - pub fn poll(&mut self, state: &mut State) -> Result<()> { + pub fn add_output(&mut self, reg_name: u32, name: &str) { + let toggle_inverted_output_cb = move |ctx: &mut MethodContext, _args: ()| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let inverted = !color.inverted; + output.set_color(Color { inverted, ..color }); + + let value = inverted.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Inverted", value); + + if ctx.state.color().inverted != global_color.inverted { + let value = inverted.into(); + signal_change(&mut ctx.conn.send, "/", "Inverted", value); + } + }; + + let get_inverted_output_cb = move |ctx: PropContext| { + ctx.state + .output_by_reg_name(reg_name) + .unwrap() + .color() + .inverted + }; + + let set_inverted_output_cb = move |ctx: PropContext, val: UnVariant| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let inverted = val.get::().unwrap(); + + if color.inverted != inverted { + output.set_color(Color { inverted, ..color }); + + let value = inverted.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Inverted", value); + + if ctx.state.color().inverted != global_color.inverted { + let value = inverted.into(); + signal_change(&mut ctx.conn.send, "/", "Inverted", value); + } + } + }; + + let update_brightness_output_cb = + move |ctx: &mut MethodContext, args: UpdateBrightnessArgs| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let brightness = (color.brightness + args.delta).clamp(0.0, 1.0); + + if color.brightness != brightness { + output.set_color(Color { + brightness, + ..color + }); + + let value = brightness.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Brightness", value); + + let brightness = ctx.state.color().brightness; + if brightness != global_color.brightness { + let value = brightness.into(); + signal_change(&mut ctx.conn.send, "/", "Brightness", value); + } + } + }; + + let get_brightness_output_cb = move |ctx: PropContext| { + ctx.state + .output_by_reg_name(reg_name) + .unwrap() + .color() + .brightness + }; + + let set_brightness_output_cb = move |ctx: PropContext, val: UnVariant| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let brightness = val.get::().unwrap().clamp(0.0, 1.0); + + if color.brightness != brightness { + output.set_color(Color { + brightness, + ..color + }); + + let value = brightness.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Brightness", value); + + let brightness = ctx.state.color().brightness; + if brightness != global_color.brightness { + let value = brightness.into(); + signal_change(&mut ctx.conn.send, "/", "Brightness", value); + } + } + }; + + let update_temperature_output_cb = + move |ctx: &mut MethodContext, args: UpdateTemperatureArgs| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let temp = (color.temp as i16 + args.delta).clamp(1_000, 10_000) as u16; + + if color.temp != temp { + output.set_color(Color { temp, ..color }); + + let value = temp.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Temperature", value); + + let temp = ctx.state.color().temp; + if temp != global_color.temp { + let value = temp.into(); + signal_change(&mut ctx.conn.send, "/", "Temperature", value); + } + } + }; + + let get_temperature_output_cb = move |ctx: PropContext| { + ctx.state.output_by_reg_name(reg_name).unwrap().color().temp + }; + + let set_temperature_output_cb = move |ctx: PropContext, val: UnVariant| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let temp = val.get::().unwrap().clamp(1_000, 10_000); + + if color.temp != temp { + output.set_color(Color { temp, ..color }); + + let value = temp.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Temperature", value); + + let temp = ctx.state.color().temp; + if temp != global_color.temp { + let value = temp.into(); + signal_change(&mut ctx.conn.send, "/", "Temperature", value); + } + } + }; + + let update_gamma_output_cb = + move |ctx: &mut MethodContext, args: UpdateGammaArgs| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let gamma = (color.gamma + args.delta).max(0.1); + + if color.gamma != gamma { + output.set_color(Color { gamma, ..color }); + + let value = gamma.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Gamma", value); + + let gamma = ctx.state.color().gamma; + if gamma != global_color.gamma { + let value = gamma.into(); + signal_change(&mut ctx.conn.send, "/", "Gamma", value); + } + } + }; + + let get_gamma_output_cb = move |ctx: PropContext| { + ctx.state + .output_by_reg_name(reg_name) + .unwrap() + .color() + .gamma + }; + + let set_gamma_output_cb = move |ctx: PropContext, val: UnVariant| { + let global_color = ctx.state.color(); + + let output = ctx.state.mut_output_by_reg_name(reg_name).unwrap(); + let color = output.color(); + let gamma = val.get::().unwrap().max(0.1); + + if color.gamma != gamma { + output.set_color(Color { gamma, ..color }); + + let value = gamma.into(); + signal_change(&mut ctx.conn.send, ctx.object_path, "Gamma", value); + + let gamma = ctx.state.color().gamma; + if gamma != global_color.gamma { + let value = gamma.into(); + signal_change(&mut ctx.conn.send, "/", "Gamma", value); + } + } + }; + + let gammarelay_output_iface = InterfaceImp::new("rs.wl.gammarelay") + .with_method::<(), ()>("ToggleInverted", toggle_inverted_output_cb) + .with_method::( + "UpdateTemperature", + update_temperature_output_cb, + ) + .with_method::("UpdateGamma", update_gamma_output_cb) + .with_method::( + "UpdateBrightness", + update_brightness_output_cb, + ) + .with_prop( + "Inverted", + Access::ReadWrite(get_inverted_output_cb, set_inverted_output_cb), + ) + .with_prop( + "Temperature", + Access::ReadWrite(get_temperature_output_cb, set_temperature_output_cb), + ) + .with_prop( + "Gamma", + Access::ReadWrite(get_gamma_output_cb, set_gamma_output_cb), + ) + .with_prop( + "Brightness", + Access::ReadWrite(get_brightness_output_cb, set_brightness_output_cb), + ); + + let mut object = rustbus_service::Object::new(); + object.add_interface(gammarelay_output_iface); + + let outputs_object = self + .service + .get_object_mut("/outputs") + .expect("object /outputs not found"); + outputs_object.add_child(name.replace('-', "_"), object); + } + + pub fn remove_output(&mut self, name: &str) { + let outputs_object = self + .service + .get_object_mut("/outputs") + .expect("object /outputs not found"); + + outputs_object.remove_child(&name.replace('-', "_")); + } + + pub fn poll(&mut self, state: &mut WaylandState) -> Result<()> { self.service.run(&mut self.conn, state, Timeout::Nonblock)?; Ok(()) } } -fn toggle_inverted_cb(ctx: &mut MethodContext, _args: ()) { - ctx.state.color.inverted = !ctx.state.color.inverted; - ctx.state.color_changed = true; +fn toggle_inverted_root_cb(ctx: &mut MethodContext, _args: ()) { + let inverted = !ctx.state.color().inverted; + ctx.state.set_inverted(inverted); - let sig = prop_changed_message( + signal_change( + &mut ctx.conn.send, ctx.object_path, - "rs.wl.gammarelay", "Inverted", - ctx.state.color.inverted.into(), + inverted.into(), ); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_updated_property_to_outputs(ctx, "Inverted", inverted.into()); } -fn get_inverted_cb(ctx: PropContext) -> bool { - ctx.state.color.inverted +fn get_inverted_root_cb(ctx: PropContext) -> bool { + ctx.state.color().inverted } -fn set_inverted_cb(ctx: PropContext, val: UnVariant) { +fn set_inverted_root_cb(ctx: PropContext, val: UnVariant) { let val = val.get::().unwrap(); - if ctx.state.color.inverted != val { - ctx.state.color.inverted = val; - ctx.state.color_changed = true; + if ctx.state.color().inverted != val { + ctx.state.set_inverted(val); - let sig = prop_changed_message(ctx.object_path, "rs.wl.gammarelay", ctx.name, val.into()); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_change(&mut ctx.conn.send, ctx.object_path, ctx.name, val.into()); + signal_set_property_to_outputs(ctx, val.into()); } } @@ -108,36 +364,32 @@ struct UpdateBrightnessArgs { delta: f64, } -fn update_brightness_cb(ctx: &mut MethodContext, args: UpdateBrightnessArgs) { - let val = (ctx.state.color.brightness + args.delta).clamp(0.0, 1.0); - - if ctx.state.color.brightness != val { - ctx.state.color.brightness = val; - ctx.state.color_changed = true; +fn update_brightness_root_cb(ctx: &mut MethodContext, args: UpdateBrightnessArgs) { + let updated = ctx.state.update_brightness(args.delta); - let sig = prop_changed_message( + if updated { + let val = ctx.state.color().brightness; + signal_change( + &mut ctx.conn.send, ctx.object_path, - "rs.wl.gammarelay", "Brightness", val.into(), ); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_updated_property_to_outputs(ctx, "Brightness", val.into()); } } -fn get_brightness_cb(ctx: PropContext) -> f64 { - ctx.state.color.brightness +fn get_brightness_root_cb(ctx: PropContext) -> f64 { + ctx.state.color().brightness } -fn set_brightness_cb(ctx: PropContext, val: UnVariant) { +fn set_brightness_root_cb(ctx: PropContext, val: UnVariant) { let val = val.get::().unwrap().clamp(0.0, 1.0); + if ctx.state.color().brightness != val { + ctx.state.set_brightness(val); - if ctx.state.color.brightness != val { - ctx.state.color.brightness = val; - ctx.state.color_changed = true; - - let sig = prop_changed_message(ctx.object_path, "rs.wl.gammarelay", ctx.name, val.into()); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_change(&mut ctx.conn.send, ctx.object_path, ctx.name, val.into()); + signal_set_property_to_outputs(ctx, val.into()); } } @@ -146,35 +398,32 @@ struct UpdateTemperatureArgs { delta: i16, } -fn update_temperature_cb(ctx: &mut MethodContext, args: UpdateTemperatureArgs) { - let val = (ctx.state.color.temp as i16 + args.delta).clamp(1_000, 10_000) as u16; - - if ctx.state.color.temp != val { - ctx.state.color.temp = val; - ctx.state.color_changed = true; +fn update_temperature_root_cb(ctx: &mut MethodContext, args: UpdateTemperatureArgs) { + let updated = ctx.state.update_temperature(args.delta); - let sig = prop_changed_message( + if updated { + let val = ctx.state.color().temp; + signal_change( + &mut ctx.conn.send, ctx.object_path, - "rs.wl.gammarelay", "Temperature", val.into(), ); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_updated_property_to_outputs(ctx, "Temperature", val.into()); } } -fn get_temperature_cb(ctx: PropContext) -> u16 { - ctx.state.color.temp +fn get_temperature_root_cb(ctx: PropContext) -> u16 { + ctx.state.color().temp } -fn set_temperature_cb(ctx: PropContext, val: UnVariant) { +fn set_temperature_root_cb(ctx: PropContext, val: UnVariant) { let val = val.get::().unwrap().clamp(1_000, 10_000); - if ctx.state.color.temp != val { - ctx.state.color.temp = val; - ctx.state.color_changed = true; + if ctx.state.color().temp != val { + ctx.state.set_temperature(val); - let sig = prop_changed_message(ctx.object_path, "rs.wl.gammarelay", ctx.name, val.into()); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_change(&mut ctx.conn.send, ctx.object_path, ctx.name, val.into()); + signal_set_property_to_outputs(ctx, val.into()); } } @@ -183,30 +432,27 @@ struct UpdateGammaArgs { delta: f64, } -fn update_gamma_cb(ctx: &mut MethodContext, args: UpdateGammaArgs) { - let val = (ctx.state.color.gamma + args.delta).max(0.1); +fn update_gamma_root_cb(ctx: &mut MethodContext, args: UpdateGammaArgs) { + let updated = ctx.state.update_gamma(args.delta); - if ctx.state.color.gamma != val { - ctx.state.color.gamma = val; - ctx.state.color_changed = true; - - let sig = prop_changed_message(ctx.object_path, "rs.wl.gammarelay", "Gamma", val.into()); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + if updated { + let val = ctx.state.color().gamma; + signal_change(&mut ctx.conn.send, ctx.object_path, "Gamma", val.into()); + signal_updated_property_to_outputs(ctx, "Gamma", val.into()); } } -fn get_gamma_cb(ctx: PropContext) -> f64 { - ctx.state.color.gamma +fn get_gamma_root_cb(ctx: PropContext) -> f64 { + ctx.state.color().gamma } -fn set_gamma_cb(ctx: PropContext, val: UnVariant) { +fn set_gamma_root_cb(ctx: PropContext, val: UnVariant) { let val = val.get::().unwrap().max(0.1); - if ctx.state.color.gamma != val { - ctx.state.color.gamma = val; - ctx.state.color_changed = true; + if ctx.state.color().gamma != val { + ctx.state.set_gamma(val); - let sig = prop_changed_message(ctx.object_path, "rs.wl.gammarelay", ctx.name, val.into()); - ctx.conn.send.send_message_write_all(&sig).unwrap(); + signal_change(&mut ctx.conn.send, ctx.object_path, ctx.name, val.into()); + signal_set_property_to_outputs(ctx, val.into()); } } @@ -228,3 +474,44 @@ fn prop_changed_message(path: &str, iface: &str, prop: &str, value: Param) -> Ma sig.body.push_param::<&[&str]>(&[]).unwrap(); sig } + +fn signal_change(send: &mut rustbus::SendConn, path: &str, prop: &str, value: Param) { + let output_sig = prop_changed_message(path, "rs.wl.gammarelay", prop, value); + send.send_message_write_all(&output_sig).unwrap(); +} + +fn signal_set_property_to_outputs(ctx: PropContext, value: Param) { + for output in ctx + .state + .outputs + .iter() + .filter(|output| output.color_changed()) + { + signal_change( + &mut ctx.conn.send, + &output.object_path(), + ctx.name, + value.clone(), + ); + } +} + +fn signal_updated_property_to_outputs( + ctx: &mut MethodContext, + name: &str, + value: Param, +) { + for output in ctx + .state + .outputs + .iter() + .filter(|output| output.color_changed()) + { + signal_change( + &mut ctx.conn.send, + &output.object_path(), + name, + value.clone(), + ); + } +} diff --git a/src/main.rs b/src/main.rs index bb5974c..d71ccb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use std::io; use std::os::fd::AsRawFd; use clap::{Parser, Subcommand}; +use dbus_server::DbusServer; use wayrs_protocols::wlr_gamma_control_unstable_v1::ZwlrGammaControlManagerV1; use color::Color; @@ -26,31 +27,167 @@ enum Command { Watch { format: String }, } -struct State { - color: Color, - color_changed: bool, +pub struct WaylandState { outputs: Vec, gamma_manager: ZwlrGammaControlManagerV1, } +pub struct State { + pub wayland_state: WaylandState, + pub dbus_server: DbusServer, +} + +impl WaylandState { + pub fn output_by_reg_name(&self, reg_name: u32) -> Option<&wayland::Output> { + self.outputs + .iter() + .find(|output| output.reg_name() == reg_name) + } + + pub fn mut_output_by_reg_name(&mut self, reg_name: u32) -> Option<&mut wayland::Output> { + self.outputs + .iter_mut() + .find(|output| output.reg_name() == reg_name) + } + + /// Returns the average color of all outputs, or the default color if there are no outputs + pub fn color(&self) -> Color { + if self.outputs.is_empty() { + Color::default() + } else { + let color = self.outputs.iter().fold( + Color { + inverted: true, + brightness: 0.0, + temp: 0, + gamma: 0.0, + }, + |color, output| { + let output_color = output.color(); + Color { + inverted: color.inverted && output_color.inverted, + brightness: color.brightness + output_color.brightness, + temp: color.temp + output_color.temp, + gamma: color.gamma + output_color.gamma, + } + }, + ); + + Color { + temp: color.temp / self.outputs.len() as u16, + gamma: color.gamma / self.outputs.len() as f64, + brightness: color.brightness / self.outputs.len() as f64, + inverted: color.inverted, + } + } + } + + pub fn color_changed(&self) -> bool { + self.outputs.iter().any(|output| output.color_changed()) + } + + pub fn set_inverted(&mut self, inverted: bool) { + for output in &mut self.outputs { + let color = output.color(); + output.set_color(Color { inverted, ..color }); + } + } + + pub fn set_brightness(&mut self, brightness: f64) { + for output in &mut self.outputs { + let color = output.color(); + output.set_color(Color { + brightness, + ..color + }); + } + } + + /// Returns `true` if any output was updated + pub fn update_brightness(&mut self, delta: f64) -> bool { + let mut updated = false; + for output in &mut self.outputs { + let color = output.color(); + let brightness = (color.brightness + delta).clamp(0.0, 1.0); + if brightness != color.brightness { + updated = true; + output.set_color(Color { + brightness, + ..color + }); + } + } + + updated + } + + pub fn set_temperature(&mut self, temp: u16) { + for output in &mut self.outputs { + let color = output.color(); + output.set_color(Color { temp, ..color }); + } + } + + /// Returns `true` if any output was updated + pub fn update_temperature(&mut self, delta: i16) -> bool { + let mut updated = false; + for output in &mut self.outputs { + let color = output.color(); + let temp = (color.temp as i16 + delta).clamp(1_000, 10_000) as u16; + if temp != color.temp { + updated = true; + output.set_color(Color { temp, ..color }); + } + } + + updated + } + + pub fn set_gamma(&mut self, gamma: f64) { + for output in &mut self.outputs { + let color = output.color(); + output.set_color(Color { gamma, ..color }); + } + } + + /// Returns `true` if any output was updated + pub fn update_gamma(&mut self, delta: f64) -> bool { + let mut updated = false; + for output in &mut self.outputs { + let color = output.color(); + let gamma = (output.color().gamma + delta).max(0.1); + if gamma != color.gamma { + updated = true; + output.set_color(Color { gamma, ..color }); + } + } + + updated + } +} + fn main() -> anyhow::Result<()> { - let commnad = Cli::parse().command.unwrap_or(Command::Run); + let command = Cli::parse().command.unwrap_or(Command::Run); let dbus_server = dbus_server::DbusServer::new()?; - match commnad { + match command { Command::Run => { - if let Some(mut dbus_server) = dbus_server { - let (mut wayland, mut state) = wayland::Wayland::new()?; + if let Some(dbus_server) = dbus_server { + let (mut wayland, wayland_state) = wayland::Wayland::new()?; let mut fds = [pollin(&dbus_server), pollin(&wayland)]; + let mut state = State { + wayland_state, + dbus_server, + }; loop { poll(&mut fds)?; if fds[0].revents != 0 { - dbus_server.poll(&mut state)?; + state.dbus_server.poll(&mut state.wayland_state)?; } - if fds[1].revents != 0 || state.color_changed { - wayland.poll(&mut state)?; + if fds[1].revents != 0 || state.wayland_state.color_changed() { + state = wayland.poll(state)?; } } } else { @@ -59,18 +196,22 @@ fn main() -> anyhow::Result<()> { } Command::Watch { format } => { let mut dbus_client = dbus_client::DbusClient::new(format, dbus_server.is_none())?; - if let Some(mut dbus_server) = dbus_server { - let (mut wayland, mut state) = wayland::Wayland::new()?; + if let Some(dbus_server) = dbus_server { + let (mut wayland, state) = wayland::Wayland::new()?; let mut fds = [pollin(&dbus_server), pollin(&wayland), pollin(&dbus_client)]; + let mut state = State { + wayland_state: state, + dbus_server, + }; loop { poll(&mut fds)?; if fds[0].revents != 0 { - dbus_server.poll(&mut state)?; + state.dbus_server.poll(&mut state.wayland_state)?; } - if fds[1].revents != 0 || state.color_changed { - wayland.poll(&mut state)?; + if fds[1].revents != 0 || state.wayland_state.color_changed() { + state = wayland.poll(state)?; } if fds[2].revents != 0 { dbus_client.run(false)?; diff --git a/src/wayland.rs b/src/wayland.rs index 902d2d2..7752aca 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -13,7 +13,7 @@ use std::io::ErrorKind; use std::os::fd::{AsRawFd, RawFd}; use crate::color::{colorramp_fill, Color}; -use crate::State; +use crate::{State, WaylandState}; pub struct Wayland { conn: Connection, @@ -26,7 +26,7 @@ impl AsRawFd for Wayland { } impl Wayland { - pub fn new() -> Result<(Self, State)> { + pub fn new() -> Result<(Self, WaylandState)> { let (mut conn, globals) = Connection::connect_and_collect_globals()?; conn.add_registry_cb(wl_registry_cb); @@ -38,9 +38,7 @@ impl Wayland { .map(|output| Output::bind(&mut conn, output, gamma_manager)) .collect(); - let state = State { - color: Default::default(), - color_changed: false, + let state = WaylandState { outputs, gamma_manager, }; @@ -50,23 +48,20 @@ impl Wayland { Ok((Self { conn }, state)) } - pub fn poll(&mut self, state: &mut State) -> Result<()> { + pub fn poll(&mut self, mut state: State) -> Result { match self.conn.recv_events(IoMode::NonBlocking) { - Ok(()) => self.conn.dispatch_events(state), + Ok(()) => self.conn.dispatch_events(&mut state), Err(e) if e.kind() == ErrorKind::WouldBlock => (), Err(e) => return Err(e.into()), } - if state.color_changed { - state.color_changed = false; - state - .outputs - .iter_mut() - .try_for_each(|o| o.set_color(&mut self.conn, state.color))?; + for output in &mut state.wayland_state.outputs { + if output.color_changed { + output.update_displayed_color(&mut self.conn)?; + } } - self.conn.flush(IoMode::Blocking)?; - Ok(()) + Ok(state) } } @@ -74,9 +69,11 @@ impl Wayland { pub struct Output { reg_name: u32, wl: WlOutput, + name: Option, color: Color, gamma_control: ZwlrGammaControlV1, ramp_size: usize, + color_changed: bool, } impl Output { @@ -86,13 +83,15 @@ impl Output { gamma_manager: ZwlrGammaControlManagerV1, ) -> Self { eprintln!("New output: {}", global.name); - let output = global.bind(conn, 1..=3).unwrap(); + let output = global.bind_with_cb(conn, 4, wl_output_cb).unwrap(); Self { reg_name: global.name, wl: output, + name: None, color: Default::default(), gamma_control: gamma_manager.get_gamma_control_with_cb(conn, output, gamma_control_cb), ramp_size: 0, + color_changed: true, } } @@ -104,12 +103,40 @@ impl Output { } } - fn set_color(&mut self, conn: &mut Connection, color: Color) -> Result<()> { + pub fn reg_name(&self) -> u32 { + self.reg_name + } + + pub fn name(&self) -> &str { + self.name.as_ref().unwrap() + } + + pub fn color(&self) -> Color { + self.color + } + + pub fn color_changed(&self) -> bool { + self.color_changed + } + + pub fn set_color(&mut self, color: Color) { if self.ramp_size == 0 || color == self.color { - return Ok(()); + return; } self.color = color; + self.color_changed = true; + } + + pub fn object_path(&self) -> String { + format!("/outputs/{}", self.name().replace('-', "_")) + } + + fn update_displayed_color(&mut self, conn: &mut Connection) -> Result<()> { + if self.ramp_size == 0 { + return Ok(()); + } + let file = shmemfdrs2::create_shmem(cstr!("/ramp-buffer"))?; file.set_len(self.ramp_size as u64 * 6)?; let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file)? }; @@ -118,6 +145,8 @@ impl Output { let (g, b) = rest.split_at_mut(self.ramp_size); colorramp_fill(r, g, b, self.ramp_size, self.color); self.gamma_control.set_gamma(conn, file.into()); + + self.color_changed = false; Ok(()) } } @@ -125,13 +154,22 @@ impl Output { fn wl_registry_cb(conn: &mut Connection, state: &mut State, event: &wl_registry::Event) { match event { wl_registry::Event::Global(global) if global.is::() => { - let mut output = Output::bind(conn, global, state.gamma_manager); - output.set_color(conn, state.color).unwrap(); - state.outputs.push(output); + let mut output = Output::bind(conn, global, state.wayland_state.gamma_manager); + output.set_color(state.wayland_state.color()); + output.update_displayed_color(conn).unwrap(); + state.wayland_state.outputs.push(output); } wl_registry::Event::GlobalRemove(name) => { - if let Some(output_index) = state.outputs.iter().position(|o| o.reg_name == *name) { - let output = state.outputs.swap_remove(output_index); + if let Some(output_index) = state + .wayland_state + .outputs + .iter() + .position(|o| o.reg_name == *name) + { + if let Some(output_name) = &state.wayland_state.outputs[output_index].name { + state.dbus_server.remove_output(output_name); + } + let output = state.wayland_state.outputs.swap_remove(output_index); output.destroy(conn); } } @@ -142,22 +180,42 @@ fn wl_registry_cb(conn: &mut Connection, state: &mut State, event: &wl_re fn gamma_control_cb(ctx: EventCtx) { let output_index = ctx .state + .wayland_state .outputs .iter() .position(|o| o.gamma_control == ctx.proxy) .expect("Received event for unknown output"); match ctx.event { zwlr_gamma_control_v1::Event::GammaSize(size) => { - let output = &mut ctx.state.outputs[output_index]; + let output = &mut ctx.state.wayland_state.outputs[output_index]; eprintln!("Output {}: ramp_size = {}", output.reg_name, size); output.ramp_size = size as usize; - output.set_color(ctx.conn, ctx.state.color).unwrap(); + output.update_displayed_color(ctx.conn).unwrap(); } zwlr_gamma_control_v1::Event::Failed => { - let output = ctx.state.outputs.swap_remove(output_index); + let output = ctx.state.wayland_state.outputs.swap_remove(output_index); eprintln!("Output {}: gamma_control::Event::Failed", output.reg_name); + if let Some(output_name) = &output.name { + ctx.state.dbus_server.remove_output(output_name); + } output.destroy(ctx.conn); } _ => (), } } + +fn wl_output_cb(ctx: EventCtx) { + if let wl_output::Event::Name(name) = ctx.event { + let output = ctx + .state + .wayland_state + .outputs + .iter_mut() + .find(|o| o.wl == ctx.proxy) + .unwrap(); + let name = String::from_utf8(name.into_bytes()).expect("invalid output name"); + eprintln!("Output {}: name = {name:?}", output.reg_name); + ctx.state.dbus_server.add_output(output.reg_name, &name); + output.name = Some(name); + } +}