2023-10-21 00:02:00 +02:00
|
|
|
//! Output management.
|
|
|
|
|
2023-10-20 01:18:34 +02:00
|
|
|
use crate::{
|
2023-10-20 03:19:00 +02:00
|
|
|
msg::{Args, CallbackId, Msg, Request, RequestResponse},
|
2023-10-20 02:26:12 +02:00
|
|
|
request, send_msg,
|
2023-10-21 00:02:00 +02:00
|
|
|
tag::TagHandle,
|
2023-10-20 02:26:12 +02:00
|
|
|
CALLBACK_VEC,
|
2023-10-20 01:18:34 +02:00
|
|
|
};
|
|
|
|
|
2023-10-20 03:19:00 +02:00
|
|
|
/// A unique identifier for an output.
|
|
|
|
///
|
|
|
|
/// An empty string represents an invalid output.
|
|
|
|
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
2023-10-21 00:02:00 +02:00
|
|
|
pub(crate) struct OutputName(pub String);
|
2023-10-20 01:18:34 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Get an [`OutputHandle`] by its name.
|
|
|
|
///
|
|
|
|
/// `name` is the name of the port the output is plugged in to.
|
|
|
|
/// This is something like `HDMI-1` or `eDP-0`.
|
|
|
|
pub fn get_by_name(name: &str) -> Option<OutputHandle> {
|
|
|
|
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
|
|
|
unreachable!()
|
|
|
|
};
|
|
|
|
|
|
|
|
output_names
|
|
|
|
.into_iter()
|
|
|
|
.find(|s| s == name)
|
|
|
|
.map(|s| OutputHandle(OutputName(s)))
|
|
|
|
}
|
2023-10-20 01:18:34 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Get a handle to all connected outputs.
|
|
|
|
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
|
|
|
|
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
|
|
|
unreachable!()
|
|
|
|
};
|
2023-10-20 01:18:34 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
output_names
|
|
|
|
.into_iter()
|
|
|
|
.map(|name| OutputHandle(OutputName(name)))
|
|
|
|
}
|
2023-10-20 01:18:34 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Get the currently focused output.
|
|
|
|
///
|
|
|
|
/// This is currently defined as the one with the cursor on it.
|
|
|
|
pub fn get_focused() -> Option<OutputHandle> {
|
|
|
|
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
|
|
|
|
unreachable!()
|
|
|
|
};
|
|
|
|
|
|
|
|
output_names
|
|
|
|
.into_iter()
|
|
|
|
.map(|s| OutputHandle(OutputName(s)))
|
|
|
|
.find(|op| op.properties().focused == Some(true))
|
|
|
|
}
|
2023-10-20 02:26:12 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Connect a function to be run on all current and future outputs.
|
|
|
|
///
|
|
|
|
/// When called, `connect_for_all` will run `func` with all currently connected outputs.
|
|
|
|
/// If a new output is connected, `func` will also be called with it.
|
|
|
|
///
|
|
|
|
/// This will *not* be called if it has already been called for a given connector.
|
|
|
|
/// This means turning your monitor off and on or unplugging and replugging it *to the same port*
|
|
|
|
/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`.
|
|
|
|
/// This is intended to prevent duplicate setup.
|
|
|
|
///
|
|
|
|
/// Please note: this function will be run *after* Pinnacle processes your entire config.
|
|
|
|
/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`,
|
|
|
|
/// nothing will happen as the tags haven't been added yet.
|
|
|
|
pub fn connect_for_all<F>(mut func: F)
|
|
|
|
where
|
|
|
|
F: FnMut(OutputHandle) + Send + 'static,
|
|
|
|
{
|
|
|
|
let args_callback = move |args: Option<Args>| {
|
|
|
|
if let Some(Args::ConnectForAllOutputs { output_name }) = args {
|
|
|
|
func(OutputHandle(OutputName(output_name)));
|
|
|
|
}
|
|
|
|
};
|
2023-10-20 02:26:12 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
let mut callback_vec = CALLBACK_VEC.lock().unwrap();
|
|
|
|
let len = callback_vec.len();
|
|
|
|
callback_vec.push(Box::new(args_callback));
|
2023-10-20 02:26:12 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
let msg = Msg::ConnectForAllOutputs {
|
|
|
|
callback_id: CallbackId(len as u32),
|
|
|
|
};
|
2023-10-20 02:26:12 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
send_msg(msg).unwrap();
|
2023-10-20 01:18:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// An output handle.
|
|
|
|
///
|
|
|
|
/// This is a handle to one of your monitors.
|
|
|
|
/// It serves to make it easier to deal with them, defining methods for getting properties and
|
|
|
|
/// helpers for things like positioning multiple monitors.
|
2023-10-20 02:26:12 +02:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
2023-10-21 00:02:00 +02:00
|
|
|
pub struct OutputHandle(pub(crate) OutputName);
|
2023-10-20 01:18:34 +02:00
|
|
|
|
|
|
|
/// Properties of an output.
|
|
|
|
pub struct OutputProperties {
|
|
|
|
/// The make.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub make: Option<String>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// The model.
|
|
|
|
///
|
|
|
|
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
|
|
|
|
/// displays.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub model: Option<String>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// The location of the output in the global space.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub loc: Option<(i32, i32)>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the
|
|
|
|
/// height.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub res: Option<(i32, i32)>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// The refresh rate of the output in millihertz.
|
|
|
|
///
|
|
|
|
/// For example, 60Hz is returned as 60000.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub refresh_rate: Option<i32>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// The physical size of the output in millimeters.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub physical_size: Option<(i32, i32)>,
|
2023-10-20 01:18:34 +02:00
|
|
|
/// Whether or not the output is focused.
|
2023-10-20 02:26:12 +02:00
|
|
|
pub focused: Option<bool>,
|
|
|
|
/// The tags on this output.
|
|
|
|
pub tags: Vec<TagHandle>,
|
2023-10-20 01:18:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl OutputHandle {
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Get this output's name.
|
|
|
|
pub fn name(&self) -> String {
|
|
|
|
self.0 .0.clone()
|
|
|
|
}
|
|
|
|
|
2023-10-20 01:18:34 +02:00
|
|
|
// TODO: Make OutputProperties an option, make non null fields not options
|
|
|
|
/// Get all properties of this output.
|
|
|
|
pub fn properties(&self) -> OutputProperties {
|
|
|
|
let RequestResponse::OutputProps {
|
|
|
|
make,
|
|
|
|
model,
|
|
|
|
loc,
|
|
|
|
res,
|
|
|
|
refresh_rate,
|
|
|
|
physical_size,
|
|
|
|
focused,
|
|
|
|
tag_ids,
|
|
|
|
} = request(Request::GetOutputProps {
|
|
|
|
output_name: self.0 .0.clone(),
|
|
|
|
})
|
|
|
|
else {
|
|
|
|
unreachable!()
|
|
|
|
};
|
|
|
|
|
|
|
|
OutputProperties {
|
|
|
|
make,
|
|
|
|
model,
|
|
|
|
loc,
|
|
|
|
res,
|
|
|
|
refresh_rate,
|
|
|
|
physical_size,
|
|
|
|
focused,
|
2023-10-20 02:26:12 +02:00
|
|
|
tags: tag_ids
|
|
|
|
.unwrap_or(vec![])
|
|
|
|
.into_iter()
|
|
|
|
.map(TagHandle)
|
|
|
|
.collect(),
|
2023-10-20 01:18:34 +02:00
|
|
|
}
|
|
|
|
}
|
2023-10-20 02:49:36 +02:00
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Add tags with the given `names` to this output.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn add_tags(&self, names: &[&str]) {
|
2023-10-21 00:02:00 +02:00
|
|
|
crate::tag::add(self, names);
|
2023-10-20 02:49:36 +02:00
|
|
|
}
|
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Set this output's location in the global space.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn set_loc(&self, x: Option<i32>, y: Option<i32>) {
|
|
|
|
let msg = Msg::SetOutputLocation {
|
|
|
|
output_name: self.0.clone(),
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
};
|
|
|
|
|
|
|
|
send_msg(msg).unwrap();
|
|
|
|
}
|
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Set this output's location to the right of `other`.
|
|
|
|
///
|
|
|
|
/// It will be aligned vertically based on the given `alignment`.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
|
|
|
self.set_loc_horizontal(other, LeftOrRight::Right, alignment);
|
|
|
|
}
|
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Set this output's location to the left of `other`.
|
|
|
|
///
|
|
|
|
/// It will be aligned vertically based on the given `alignment`.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
|
|
|
|
self.set_loc_horizontal(other, LeftOrRight::Left, alignment);
|
|
|
|
}
|
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Set this output's location to the top of `other`.
|
|
|
|
///
|
|
|
|
/// It will be aligned horizontally based on the given `alignment`.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
|
|
|
self.set_loc_vertical(other, TopOrBottom::Top, alignment);
|
|
|
|
}
|
|
|
|
|
2023-10-21 00:02:00 +02:00
|
|
|
/// Set this output's location to the bottom of `other`.
|
|
|
|
///
|
|
|
|
/// It will be aligned horizontally based on the given `alignment`.
|
2023-10-20 02:49:36 +02:00
|
|
|
pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
|
|
|
|
self.set_loc_vertical(other, TopOrBottom::Bottom, alignment);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_loc_horizontal(
|
|
|
|
&self,
|
|
|
|
other: &OutputHandle,
|
|
|
|
left_or_right: LeftOrRight,
|
|
|
|
alignment: AlignmentVertical,
|
|
|
|
) {
|
|
|
|
let op1_props = self.properties();
|
|
|
|
let op2_props = other.properties();
|
|
|
|
|
|
|
|
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
|
|
|
|
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let x = match left_or_right {
|
|
|
|
LeftOrRight::Left => other_loc.0 - self_res.0,
|
|
|
|
LeftOrRight::Right => other_loc.0 + self_res.0,
|
|
|
|
};
|
|
|
|
|
|
|
|
let y = match alignment {
|
|
|
|
AlignmentVertical::Top => other_loc.1,
|
|
|
|
AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2,
|
|
|
|
AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1),
|
|
|
|
};
|
|
|
|
|
|
|
|
self.set_loc(Some(x), Some(y));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_loc_vertical(
|
|
|
|
&self,
|
|
|
|
other: &OutputHandle,
|
|
|
|
top_or_bottom: TopOrBottom,
|
|
|
|
alignment: AlignmentHorizontal,
|
|
|
|
) {
|
|
|
|
let op1_props = self.properties();
|
|
|
|
let op2_props = other.properties();
|
|
|
|
|
|
|
|
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
|
|
|
|
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let y = match top_or_bottom {
|
|
|
|
TopOrBottom::Top => other_loc.1 - self_res.1,
|
|
|
|
TopOrBottom::Bottom => other_loc.1 + other_res.1,
|
|
|
|
};
|
|
|
|
|
|
|
|
let x = match alignment {
|
|
|
|
AlignmentHorizontal::Left => other_loc.0,
|
|
|
|
AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2,
|
|
|
|
AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0),
|
|
|
|
};
|
|
|
|
|
|
|
|
self.set_loc(Some(x), Some(y));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum TopOrBottom {
|
|
|
|
Top,
|
|
|
|
Bottom,
|
|
|
|
}
|
|
|
|
|
|
|
|
enum LeftOrRight {
|
|
|
|
Left,
|
|
|
|
Right,
|
|
|
|
}
|
|
|
|
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Horizontal alignment.
|
2023-10-20 04:44:33 +02:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
2023-10-20 02:49:36 +02:00
|
|
|
pub enum AlignmentHorizontal {
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Align the outputs such that the left edges are in line.
|
2023-10-20 02:49:36 +02:00
|
|
|
Left,
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Center the outputs horizontally.
|
2023-10-20 02:49:36 +02:00
|
|
|
Center,
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Align the outputs such that the right edges are in line.
|
2023-10-20 02:49:36 +02:00
|
|
|
Right,
|
|
|
|
}
|
|
|
|
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Vertical alignment.
|
2023-10-20 04:44:33 +02:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
2023-10-20 02:49:36 +02:00
|
|
|
pub enum AlignmentVertical {
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Align the outputs such that the top edges are in line.
|
2023-10-20 02:49:36 +02:00
|
|
|
Top,
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Center the outputs vertically.
|
2023-10-20 02:49:36 +02:00
|
|
|
Center,
|
2023-10-20 05:35:12 +02:00
|
|
|
/// Align the outputs such that the bottom edges are in line.
|
2023-10-20 02:49:36 +02:00
|
|
|
Bottom,
|
2023-10-20 01:18:34 +02:00
|
|
|
}
|