Merge pull request #98 from pinnacle-comp/output_hotplug

Save output state on disconnect, restore on reconnect
This commit is contained in:
Ottatop 2023-09-29 04:43:53 -05:00 committed by GitHub
commit 19601fdafd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 42 deletions

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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![],