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
|
||||
|
||||
---@diagnostic disable:redefined-local
|
||||
|
||||
---Output management.
|
||||
---
|
||||
---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_res = op2:res()
|
||||
|
||||
if
|
||||
self_loc == nil
|
||||
or self_res == nil
|
||||
or other_loc == nil
|
||||
or other_res == nil
|
||||
then
|
||||
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
|
||||
return
|
||||
end
|
||||
|
||||
|
@ -184,15 +181,9 @@ local function set_loc_horizontal(op1, op2, left_or_right, alignment)
|
|||
if alignment == "top" then
|
||||
output_module.set_loc(op1, { x = x, y = other_loc.y })
|
||||
elseif alignment == "center" then
|
||||
output_module.set_loc(
|
||||
op1,
|
||||
{ x = x, y = other_loc.y + (other_res.h - self_res.h) // 2 }
|
||||
)
|
||||
output_module.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) // 2 })
|
||||
elseif alignment == "bottom" then
|
||||
output_module.set_loc(
|
||||
op1,
|
||||
{ x = x, y = other_loc.y + (other_res.h - self_res.h) }
|
||||
)
|
||||
output_module.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -243,12 +234,7 @@ local function set_loc_vertical(op1, op2, top_or_bottom, alignment)
|
|||
local other_loc = op2:loc()
|
||||
local other_res = op2:res()
|
||||
|
||||
if
|
||||
self_loc == nil
|
||||
or self_res == nil
|
||||
or other_loc == nil
|
||||
or other_res == nil
|
||||
then
|
||||
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
|
||||
return
|
||||
end
|
||||
|
||||
|
@ -263,15 +249,9 @@ local function set_loc_vertical(op1, op2, top_or_bottom, alignment)
|
|||
if alignment == "left" then
|
||||
output_module.set_loc(op1, { x = other_loc.x, y = y })
|
||||
elseif alignment == "center" then
|
||||
output_module.set_loc(
|
||||
op1,
|
||||
{ x = other_loc.x + (other_res.w - self_res.w) // 2, y = y }
|
||||
)
|
||||
output_module.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w) // 2, y = y })
|
||||
elseif alignment == "right" then
|
||||
output_module.set_loc(
|
||||
op1,
|
||||
{ x = other_loc.x + (other_res.w - self_res.w), y = y }
|
||||
)
|
||||
output_module.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w), y = y })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -424,6 +404,11 @@ end
|
|||
---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.
|
||||
---
|
||||
---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.
|
||||
---@param func fun(output: Output) The function that will be run.
|
||||
|
|
|
@ -88,7 +88,11 @@ use smithay_drm_extras::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
config::api::msg::{Args, OutgoingMsg},
|
||||
config::{
|
||||
api::msg::{Args, OutgoingMsg},
|
||||
ConnectorSavedState,
|
||||
},
|
||||
output::OutputName,
|
||||
render::{pointer::PointerElement, take_presentation_feedback, CustomRenderElements},
|
||||
state::{Backend, CalloopData, State, SurfaceDmabufFeedback, WithState},
|
||||
window::WindowElement,
|
||||
|
@ -828,6 +832,15 @@ impl State {
|
|||
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
||||
|
||||
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(
|
||||
output_name,
|
||||
PhysicalProperties {
|
||||
|
@ -946,7 +959,18 @@ impl State {
|
|||
self.schedule_initial_render(node, crtc, self.loop_handle.clone());
|
||||
|
||||
// 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();
|
||||
self.schedule(
|
||||
|dt| dt.state.api_state.stream.is_some(),
|
||||
|
@ -982,6 +1006,8 @@ impl State {
|
|||
_connector: connector::Info,
|
||||
crtc: crtc::Handle,
|
||||
) {
|
||||
tracing::debug!(?crtc, "connector_disconnected");
|
||||
|
||||
let Backend::Udev(backend) = &mut self.backend else {
|
||||
unreachable!()
|
||||
};
|
||||
|
@ -1006,6 +1032,13 @@ impl State {
|
|||
.cloned();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
pub mod api;
|
||||
|
||||
use crate::config::api::{msg::ModifierMask, PinnacleSocketSource};
|
||||
use crate::{
|
||||
config::api::{msg::ModifierMask, PinnacleSocketSource},
|
||||
output::OutputName,
|
||||
tag::Tag,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use smithay::input::keyboard::keysyms;
|
||||
use smithay::{
|
||||
input::keyboard::keysyms,
|
||||
utils::{Logical, Point},
|
||||
};
|
||||
use toml::Table;
|
||||
|
||||
use api::msg::Modifier;
|
||||
|
@ -115,8 +123,18 @@ pub enum Key {
|
|||
pub struct Config {
|
||||
pub window_rules: Vec<(WindowRuleCondition, WindowRule)>,
|
||||
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> {
|
||||
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")
|
||||
}
|
||||
|
||||
/// Get the config dir. This is $PINNACLE_CONFIG_DIR, then $XDG_CONFIG_HOME/pinnacle,
|
||||
/// then ~/.config/pinnacle.
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
let config_dir = std::env::var("PINNACLE_CONFIG_DIR")
|
||||
.ok()
|
||||
|
@ -135,6 +155,9 @@ pub fn get_config_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
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<()> {
|
||||
let config_dir = config_dir.as_ref();
|
||||
|
||||
|
@ -154,7 +177,6 @@ impl State {
|
|||
self.config.window_rules.clear();
|
||||
|
||||
tracing::debug!("Killing old config");
|
||||
|
||||
if let Some(channel) = self.api_state.kill_channel.as_ref() {
|
||||
if let Err(err) = futures_lite::future::block_on(channel.send(())) {
|
||||
tracing::warn!("failed to send kill ping to config future: {err}");
|
||||
|
@ -290,12 +312,12 @@ impl State {
|
|||
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 {
|
||||
let which = futures_lite::future::race(
|
||||
async move {
|
||||
tracing::debug!("awaiting child");
|
||||
let _ = child.status().await;
|
||||
tracing::debug!("child ded");
|
||||
Either::First
|
||||
},
|
||||
async move {
|
||||
|
|
|
@ -95,7 +95,7 @@ pub enum Msg {
|
|||
},
|
||||
AddTags {
|
||||
/// The name of the output you want these tags on.
|
||||
output_name: String,
|
||||
output_name: OutputName,
|
||||
tag_names: Vec<String>,
|
||||
},
|
||||
RemoveTags {
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
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);
|
||||
|
||||
impl OutputName {
|
||||
|
|
|
@ -11,8 +11,11 @@ use smithay::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
config::api::msg::{
|
||||
Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse,
|
||||
config::{
|
||||
api::msg::{
|
||||
Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse,
|
||||
},
|
||||
ConnectorSavedState,
|
||||
},
|
||||
focus::FocusTarget,
|
||||
tag::Tag,
|
||||
|
@ -267,12 +270,27 @@ impl State {
|
|||
output_name,
|
||||
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
|
||||
.space
|
||||
.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| {
|
||||
state.tags.extend(new_tags.clone());
|
||||
tracing::debug!("tags added, are now {:?}", state.tags);
|
||||
|
@ -294,8 +312,15 @@ impl State {
|
|||
}
|
||||
}
|
||||
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 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 };
|
||||
output.with_state(|state| {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
|
@ -331,6 +356,24 @@ impl State {
|
|||
self.config.output_callback_ids.push(callback_id);
|
||||
}
|
||||
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 mut loc = output.current_location();
|
||||
if let Some(x) = x {
|
||||
|
|
|
@ -327,7 +327,6 @@ impl WindowElementState {
|
|||
impl Default for WindowElementState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// INFO: I think this will assign the id on use of the state, not on window spawn.
|
||||
id: WindowId::next(),
|
||||
loc_request_state: LocationRequestState::Idle,
|
||||
tags: vec![],
|
||||
|
|
Loading…
Reference in a new issue