mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-26 21:58:10 +01:00
Save output state on disconnect
This commit is contained in:
parent
4466882f6e
commit
0e5a4f0621
7 changed files with 124 additions and 42 deletions
|
@ -1,5 +1,7 @@
|
||||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
---@diagnostic disable:redefined-local
|
||||||
|
|
||||||
---Output management.
|
---Output management.
|
||||||
---
|
---
|
||||||
---An output is what you would call a monitor. It presents windows, your cursor, and other UI elements.
|
---An output is what you would call a monitor. It presents windows, your cursor, and other UI elements.
|
||||||
|
@ -164,12 +166,7 @@ local function set_loc_horizontal(op1, op2, left_or_right, alignment)
|
||||||
local other_loc = op2:loc()
|
local other_loc = op2:loc()
|
||||||
local other_res = op2:res()
|
local other_res = op2:res()
|
||||||
|
|
||||||
if
|
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
|
||||||
self_loc == nil
|
|
||||||
or self_res == nil
|
|
||||||
or other_loc == nil
|
|
||||||
or other_res == nil
|
|
||||||
then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -184,15 +181,9 @@ local function set_loc_horizontal(op1, op2, left_or_right, alignment)
|
||||||
if alignment == "top" then
|
if alignment == "top" then
|
||||||
output_module.set_loc(op1, { x = x, y = other_loc.y })
|
output_module.set_loc(op1, { x = x, y = other_loc.y })
|
||||||
elseif alignment == "center" then
|
elseif alignment == "center" then
|
||||||
output_module.set_loc(
|
output_module.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) // 2 })
|
||||||
op1,
|
|
||||||
{ x = x, y = other_loc.y + (other_res.h - self_res.h) // 2 }
|
|
||||||
)
|
|
||||||
elseif alignment == "bottom" then
|
elseif alignment == "bottom" then
|
||||||
output_module.set_loc(
|
output_module.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) })
|
||||||
op1,
|
|
||||||
{ x = x, y = other_loc.y + (other_res.h - self_res.h) }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -243,12 +234,7 @@ local function set_loc_vertical(op1, op2, top_or_bottom, alignment)
|
||||||
local other_loc = op2:loc()
|
local other_loc = op2:loc()
|
||||||
local other_res = op2:res()
|
local other_res = op2:res()
|
||||||
|
|
||||||
if
|
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
|
||||||
self_loc == nil
|
|
||||||
or self_res == nil
|
|
||||||
or other_loc == nil
|
|
||||||
or other_res == nil
|
|
||||||
then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -263,15 +249,9 @@ local function set_loc_vertical(op1, op2, top_or_bottom, alignment)
|
||||||
if alignment == "left" then
|
if alignment == "left" then
|
||||||
output_module.set_loc(op1, { x = other_loc.x, y = y })
|
output_module.set_loc(op1, { x = other_loc.x, y = y })
|
||||||
elseif alignment == "center" then
|
elseif alignment == "center" then
|
||||||
output_module.set_loc(
|
output_module.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w) // 2, y = y })
|
||||||
op1,
|
|
||||||
{ x = other_loc.x + (other_res.w - self_res.w) // 2, y = y }
|
|
||||||
)
|
|
||||||
elseif alignment == "right" then
|
elseif alignment == "right" then
|
||||||
output_module.set_loc(
|
output_module.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w), y = y })
|
||||||
op1,
|
|
||||||
{ x = other_loc.x + (other_res.w - self_res.w), y = y }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -424,6 +404,11 @@ end
|
||||||
---When called, `connect_for_all` will immediately run `func` with all currently connected outputs.
|
---When called, `connect_for_all` will immediately run `func` with all currently connected outputs.
|
||||||
---If a new output is connected, `func` will also be called with it.
|
---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.
|
---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.
|
---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.
|
||||||
---@param func fun(output: Output) The function that will be run.
|
---@param func fun(output: Output) The function that will be run.
|
||||||
|
|
|
@ -88,7 +88,11 @@ use smithay_drm_extras::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::api::msg::{Args, OutgoingMsg},
|
config::{
|
||||||
|
api::msg::{Args, OutgoingMsg},
|
||||||
|
ConnectorSavedState,
|
||||||
|
},
|
||||||
|
output::OutputName,
|
||||||
render::{pointer::PointerElement, take_presentation_feedback, CustomRenderElements},
|
render::{pointer::PointerElement, take_presentation_feedback, CustomRenderElements},
|
||||||
state::{Backend, CalloopData, State, SurfaceDmabufFeedback, WithState},
|
state::{Backend, CalloopData, State, SurfaceDmabufFeedback, WithState},
|
||||||
window::WindowElement,
|
window::WindowElement,
|
||||||
|
@ -828,6 +832,15 @@ impl State {
|
||||||
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
||||||
|
|
||||||
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
|
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
|
||||||
|
|
||||||
|
if self.space.outputs().any(|op| {
|
||||||
|
op.user_data()
|
||||||
|
.get::<UdevOutputId>()
|
||||||
|
.is_some_and(|op_id| op_id.crtc == crtc)
|
||||||
|
}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let output = Output::new(
|
let output = Output::new(
|
||||||
output_name,
|
output_name,
|
||||||
PhysicalProperties {
|
PhysicalProperties {
|
||||||
|
@ -946,7 +959,18 @@ impl State {
|
||||||
self.schedule_initial_render(node, crtc, self.loop_handle.clone());
|
self.schedule_initial_render(node, crtc, self.loop_handle.clone());
|
||||||
|
|
||||||
// Run any connected callbacks
|
// Run any connected callbacks
|
||||||
|
if let Some(saved_state) = self
|
||||||
|
.config
|
||||||
|
.connector_saved_states
|
||||||
|
.get(&OutputName(output.name()))
|
||||||
{
|
{
|
||||||
|
let ConnectorSavedState { loc, tags } = saved_state;
|
||||||
|
|
||||||
|
output.change_current_state(None, None, None, Some(*loc));
|
||||||
|
self.space.map_output(&output, *loc);
|
||||||
|
|
||||||
|
output.with_state(|state| state.tags = tags.clone());
|
||||||
|
} else {
|
||||||
let clone = output.clone();
|
let clone = output.clone();
|
||||||
self.schedule(
|
self.schedule(
|
||||||
|dt| dt.state.api_state.stream.is_some(),
|
|dt| dt.state.api_state.stream.is_some(),
|
||||||
|
@ -982,6 +1006,8 @@ impl State {
|
||||||
_connector: connector::Info,
|
_connector: connector::Info,
|
||||||
crtc: crtc::Handle,
|
crtc: crtc::Handle,
|
||||||
) {
|
) {
|
||||||
|
tracing::debug!(?crtc, "connector_disconnected");
|
||||||
|
|
||||||
let Backend::Udev(backend) = &mut self.backend else {
|
let Backend::Udev(backend) = &mut self.backend else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
@ -1006,6 +1032,13 @@ impl State {
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
if let Some(output) = output {
|
if let Some(output) = output {
|
||||||
|
self.config.connector_saved_states.insert(
|
||||||
|
OutputName(output.name()),
|
||||||
|
ConnectorSavedState {
|
||||||
|
loc: output.current_location(),
|
||||||
|
tags: output.with_state(|state| state.tags.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
self.space.unmap_output(&output);
|
self.space.unmap_output(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
|
||||||
use crate::config::api::{msg::ModifierMask, PinnacleSocketSource};
|
use crate::{
|
||||||
|
config::api::{msg::ModifierMask, PinnacleSocketSource},
|
||||||
|
output::OutputName,
|
||||||
|
tag::Tag,
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use smithay::input::keyboard::keysyms;
|
use smithay::{
|
||||||
|
input::keyboard::keysyms,
|
||||||
|
utils::{Logical, Point},
|
||||||
|
};
|
||||||
use toml::Table;
|
use toml::Table;
|
||||||
|
|
||||||
use api::msg::Modifier;
|
use api::msg::Modifier;
|
||||||
|
@ -115,8 +123,18 @@ pub enum Key {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
|
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
|
||||||
pub output_callback_ids: Vec<CallbackId>,
|
pub output_callback_ids: Vec<CallbackId>,
|
||||||
|
pub connector_saved_states: HashMap<OutputName, ConnectorSavedState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State saved when an output is disconnected. When the output is reconnected to the same
|
||||||
|
/// connector, the saved state will apply to restore its state.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct ConnectorSavedState {
|
||||||
|
pub loc: Point<i32, Logical>,
|
||||||
|
pub tags: Vec<Tag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a metaconfig file in `config_dir`, if any.
|
||||||
fn parse(config_dir: &Path) -> anyhow::Result<Metaconfig> {
|
fn parse(config_dir: &Path) -> anyhow::Result<Metaconfig> {
|
||||||
let config_dir = config_dir.join("metaconfig.toml");
|
let config_dir = config_dir.join("metaconfig.toml");
|
||||||
|
|
||||||
|
@ -126,6 +144,8 @@ fn parse(config_dir: &Path) -> anyhow::Result<Metaconfig> {
|
||||||
toml::from_str(&metaconfig).context("Failed to deserialize toml")
|
toml::from_str(&metaconfig).context("Failed to deserialize toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the config dir. This is $PINNACLE_CONFIG_DIR, then $XDG_CONFIG_HOME/pinnacle,
|
||||||
|
/// then ~/.config/pinnacle.
|
||||||
pub fn get_config_dir() -> PathBuf {
|
pub fn get_config_dir() -> PathBuf {
|
||||||
let config_dir = std::env::var("PINNACLE_CONFIG_DIR")
|
let config_dir = std::env::var("PINNACLE_CONFIG_DIR")
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -135,6 +155,9 @@ pub fn get_config_dir() -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
/// Start the config in `config_dir`.
|
||||||
|
///
|
||||||
|
/// If this method is called while a config is already running, it will be replaced.
|
||||||
pub fn start_config(&mut self, config_dir: impl AsRef<Path>) -> anyhow::Result<()> {
|
pub fn start_config(&mut self, config_dir: impl AsRef<Path>) -> anyhow::Result<()> {
|
||||||
let config_dir = config_dir.as_ref();
|
let config_dir = config_dir.as_ref();
|
||||||
|
|
||||||
|
@ -154,7 +177,6 @@ impl State {
|
||||||
self.config.window_rules.clear();
|
self.config.window_rules.clear();
|
||||||
|
|
||||||
tracing::debug!("Killing old config");
|
tracing::debug!("Killing old config");
|
||||||
|
|
||||||
if let Some(channel) = self.api_state.kill_channel.as_ref() {
|
if let Some(channel) = self.api_state.kill_channel.as_ref() {
|
||||||
if let Err(err) = futures_lite::future::block_on(channel.send(())) {
|
if let Err(err) = futures_lite::future::block_on(channel.send(())) {
|
||||||
tracing::warn!("failed to send kill ping to config future: {err}");
|
tracing::warn!("failed to send kill ping to config future: {err}");
|
||||||
|
@ -290,12 +312,12 @@ impl State {
|
||||||
Second,
|
Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We can't get at the child while it's in the executor, so in order to kill it we need a
|
||||||
|
// channel that, when notified, will cause the child to be dropped and terminated.
|
||||||
self.async_scheduler.schedule(async move {
|
self.async_scheduler.schedule(async move {
|
||||||
let which = futures_lite::future::race(
|
let which = futures_lite::future::race(
|
||||||
async move {
|
async move {
|
||||||
tracing::debug!("awaiting child");
|
|
||||||
let _ = child.status().await;
|
let _ = child.status().await;
|
||||||
tracing::debug!("child ded");
|
|
||||||
Either::First
|
Either::First
|
||||||
},
|
},
|
||||||
async move {
|
async move {
|
||||||
|
|
|
@ -95,7 +95,7 @@ pub enum Msg {
|
||||||
},
|
},
|
||||||
AddTags {
|
AddTags {
|
||||||
/// The name of the output you want these tags on.
|
/// The name of the output you want these tags on.
|
||||||
output_name: String,
|
output_name: OutputName,
|
||||||
tag_names: Vec<String>,
|
tag_names: Vec<String>,
|
||||||
},
|
},
|
||||||
RemoveTags {
|
RemoveTags {
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
tag::Tag,
|
tag::Tag,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
pub struct OutputName(pub String);
|
pub struct OutputName(pub String);
|
||||||
|
|
||||||
impl OutputName {
|
impl OutputName {
|
||||||
|
|
|
@ -11,8 +11,11 @@ use smithay::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::api::msg::{
|
config::{
|
||||||
Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse,
|
api::msg::{
|
||||||
|
Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse,
|
||||||
|
},
|
||||||
|
ConnectorSavedState,
|
||||||
},
|
},
|
||||||
focus::FocusTarget,
|
focus::FocusTarget,
|
||||||
tag::Tag,
|
tag::Tag,
|
||||||
|
@ -267,12 +270,27 @@ impl State {
|
||||||
output_name,
|
output_name,
|
||||||
tag_names,
|
tag_names,
|
||||||
} => {
|
} => {
|
||||||
|
let new_tags = tag_names.into_iter().map(Tag::new).collect::<Vec<_>>();
|
||||||
|
if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name)
|
||||||
|
{
|
||||||
|
let mut tags = saved_state.tags.clone();
|
||||||
|
tags.extend(new_tags.clone());
|
||||||
|
saved_state.tags = tags;
|
||||||
|
} else {
|
||||||
|
self.config.connector_saved_states.insert(
|
||||||
|
output_name.clone(),
|
||||||
|
ConnectorSavedState {
|
||||||
|
tags: new_tags.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(output) = self
|
if let Some(output) = self
|
||||||
.space
|
.space
|
||||||
.outputs()
|
.outputs()
|
||||||
.find(|output| output.name() == output_name)
|
.find(|output| output.name() == output_name.0)
|
||||||
{
|
{
|
||||||
let new_tags = tag_names.into_iter().map(Tag::new).collect::<Vec<_>>();
|
|
||||||
output.with_state(|state| {
|
output.with_state(|state| {
|
||||||
state.tags.extend(new_tags.clone());
|
state.tags.extend(new_tags.clone());
|
||||||
tracing::debug!("tags added, are now {:?}", state.tags);
|
tracing::debug!("tags added, are now {:?}", state.tags);
|
||||||
|
@ -294,8 +312,15 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::RemoveTags { tag_ids } => {
|
Msg::RemoveTags { tag_ids } => {
|
||||||
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
|
let tags = tag_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|tag_id| tag_id.tag(self))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
|
for saved_state in self.config.connector_saved_states.values_mut() {
|
||||||
|
saved_state.tags.retain(|tg| tg != &tag);
|
||||||
|
}
|
||||||
let Some(output) = tag.output(self) else { continue };
|
let Some(output) = tag.output(self) else { continue };
|
||||||
output.with_state(|state| {
|
output.with_state(|state| {
|
||||||
state.tags.retain(|tg| tg != &tag);
|
state.tags.retain(|tg| tg != &tag);
|
||||||
|
@ -331,6 +356,24 @@ impl State {
|
||||||
self.config.output_callback_ids.push(callback_id);
|
self.config.output_callback_ids.push(callback_id);
|
||||||
}
|
}
|
||||||
Msg::SetOutputLocation { output_name, x, y } => {
|
Msg::SetOutputLocation { output_name, x, y } => {
|
||||||
|
if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name)
|
||||||
|
{
|
||||||
|
if let Some(x) = x {
|
||||||
|
saved_state.loc.x = x;
|
||||||
|
}
|
||||||
|
if let Some(y) = y {
|
||||||
|
saved_state.loc.y = y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.config.connector_saved_states.insert(
|
||||||
|
output_name.clone(),
|
||||||
|
ConnectorSavedState {
|
||||||
|
loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let Some(output) = output_name.output(self) else { return };
|
let Some(output) = output_name.output(self) else { return };
|
||||||
let mut loc = output.current_location();
|
let mut loc = output.current_location();
|
||||||
if let Some(x) = x {
|
if let Some(x) = x {
|
||||||
|
|
|
@ -327,7 +327,6 @@ impl WindowElementState {
|
||||||
impl Default for WindowElementState {
|
impl Default for WindowElementState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// INFO: I think this will assign the id on use of the state, not on window spawn.
|
|
||||||
id: WindowId::next(),
|
id: WindowId::next(),
|
||||||
loc_request_state: LocationRequestState::Idle,
|
loc_request_state: LocationRequestState::Idle,
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
|
|
Loading…
Reference in a new issue