Add automatic idle inhibition for fullscreen playback and selected apps
This commit is contained in:
1 parent
453ec16583
commit
03a265b97f
17 files changed
+842
-22
No files matched your search
@@ -34,6 +34,8 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
lock_screen: crate::session_lock::IdleSettings,
|
||||
#[serde(default)]
|
||||
idle_inhibit: crate::idle_inhibition::Settings,
|
||||
#[serde(default)]
|
||||
desktop_widgets: ferese_config::desktop::DesktopWidgets,
|
||||
#[serde(default)]
|
||||
pub(crate) autostart: Vec<DaemonConfig>,
|
||||
@@ -115,6 +117,7 @@ impl Config {
|
||||
let bindings = self.bindings(&input_settings)?;
|
||||
Ok(crate::RuntimeConfig {
|
||||
lock_idle: self.lock_screen.validate()?,
|
||||
idle_inhibit: self.idle_inhibit,
|
||||
autostart: self.autostart.clone(),
|
||||
layout_mode: self.layout_mode(),
|
||||
workspace_auto_back_and_forth: self.workspaces.auto_back_and_forth,
|
||||
|
||||
@@ -36,6 +36,7 @@ impl XdgShellHandler for Ferese {
|
||||
fn title_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
self.refresh_idle_inhibition();
|
||||
self.send_shell_snapshots();
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
@@ -46,6 +47,7 @@ impl XdgShellHandler for Ferese {
|
||||
fn app_id_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
self.refresh_idle_inhibition();
|
||||
self.send_shell_snapshots();
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
@@ -55,6 +57,7 @@ impl XdgShellHandler for Ferese {
|
||||
|
||||
fn parent_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
self.refresh_idle_inhibition();
|
||||
let Some(parent) = surface.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Automatic policy feeds the same idle notifier as Wayland and portal requests.
|
||||
pub(crate) mod media;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
use serde::Deserialize;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub(crate) struct Settings {
|
||||
pub fullscreen_playback: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fullscreen_playback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Mode {
|
||||
None,
|
||||
Visible,
|
||||
Fullscreen,
|
||||
Playing,
|
||||
FullscreenPlaying,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn inhibits(self, visible: bool, fullscreen: bool, playing: bool) -> bool {
|
||||
visible
|
||||
&& match self {
|
||||
Self::None => false,
|
||||
Self::Visible => true,
|
||||
Self::Fullscreen => fullscreen,
|
||||
Self::Playing => playing,
|
||||
Self::FullscreenPlaying => fullscreen && playing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct AppWindow<'a> {
|
||||
id: WindowId,
|
||||
pid: Option<u32>,
|
||||
app_id: &'a str,
|
||||
}
|
||||
|
||||
/// MPRIS reports application playback, not which of its windows owns a video.
|
||||
/// Prefer process credentials; use DesktopEntry only when no process matches.
|
||||
/// Multiple matching windows are ambiguous, including hidden windows.
|
||||
fn player_window(player: &media::Player, windows: &[AppWindow<'_>]) -> Option<WindowId> {
|
||||
let mut matches = windows
|
||||
.iter()
|
||||
.filter(|window| player.pid.is_some() && window.pid == player.pid);
|
||||
if let Some(window) = matches.next() {
|
||||
return matches.next().is_none().then_some(window.id);
|
||||
}
|
||||
|
||||
let desktop = player.desktop_entry.as_deref()?;
|
||||
let mut matches = windows
|
||||
.iter()
|
||||
.filter(|window| crate::window_rules::normalize_app_id(window.app_id) == desktop);
|
||||
let window = matches.next()?;
|
||||
matches.next().is_none().then_some(window.id)
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
let playing = self.media_players.iter().any(|player| player.playing);
|
||||
let has_rules = self.window_rules.iter().any(|rule| rule.idle_policy().is_some());
|
||||
let selected = self
|
||||
.window_rules
|
||||
.iter()
|
||||
.any(|rule| rule.idle_policy().is_some_and(|mode| mode != Mode::None));
|
||||
let playback_policy = self.idle_inhibit.fullscreen_playback
|
||||
|| self
|
||||
.window_rules
|
||||
.iter()
|
||||
.any(|rule| matches!(rule.idle_policy(), Some(Mode::Playing | Mode::FullscreenPlaying)));
|
||||
let automatic = (playback_policy && playing) || selected;
|
||||
let eligible = if self.idle_inhibitors.is_empty() && !automatic {
|
||||
Default::default()
|
||||
} else {
|
||||
self.callback_outputs()
|
||||
};
|
||||
|
||||
let application = if playback_policy && playing {
|
||||
self.windows
|
||||
.records()
|
||||
.map(|(&id, record)| {
|
||||
let pid = record
|
||||
.surface
|
||||
.as_ref()
|
||||
.and_then(|surface| surface.client())
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
.and_then(|credentials| u32::try_from(credentials.pid).ok());
|
||||
AppWindow {
|
||||
id,
|
||||
pid,
|
||||
app_id: &record.app_id,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let playing_windows = self
|
||||
.media_players
|
||||
.iter()
|
||||
.filter(|player| player.playing)
|
||||
.filter_map(|player| player_window(player, &application))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let automatic = automatic
|
||||
&& !self.session_lock.active()
|
||||
&& self.windows.records().any(|(&id, record)| {
|
||||
let Some(surface) = &record.surface else {
|
||||
return false;
|
||||
};
|
||||
if !eligible.contains_key(&surface.into()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let explicit = has_rules
|
||||
.then(|| {
|
||||
let transient = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.is_some_and(|data| data.lock().unwrap().parent.is_some())
|
||||
});
|
||||
crate::window_rules::resolve(
|
||||
&self.window_rules,
|
||||
Some(&record.app_id),
|
||||
Some(&record.title),
|
||||
transient,
|
||||
)
|
||||
.idle_inhibit
|
||||
})
|
||||
.flatten();
|
||||
let mode = explicit.unwrap_or(if self.idle_inhibit.fullscreen_playback {
|
||||
Mode::FullscreenPlaying
|
||||
} else {
|
||||
Mode::None
|
||||
});
|
||||
let fullscreen = self
|
||||
.workspaces
|
||||
.workspace_for_window(id)
|
||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||
|
||||
mode.inhibits(true, fullscreen, playing_windows.contains(&id))
|
||||
});
|
||||
let requested = self
|
||||
.idle_inhibitors
|
||||
.keys()
|
||||
.any(|surface| eligible.contains_key(&surface.into()));
|
||||
self.automatic_idle_inhibited = automatic;
|
||||
self.idle_notifier_state
|
||||
.set_is_inhibited(automatic || requested || self.portal_session.idle_inhibited());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn policy_releases_on_pause_hidden_window_and_fullscreen_exit() {
|
||||
assert!(Mode::FullscreenPlaying.inhibits(true, true, true));
|
||||
assert!(!Mode::FullscreenPlaying.inhibits(true, true, false));
|
||||
assert!(!Mode::FullscreenPlaying.inhibits(false, true, true));
|
||||
assert!(!Mode::FullscreenPlaying.inhibits(true, false, true));
|
||||
assert!(Mode::Visible.inhibits(true, false, false));
|
||||
assert!(!Mode::Visible.inhibits(false, true, true));
|
||||
assert!(Mode::Playing.inhibits(true, false, true));
|
||||
assert!(!Mode::Playing.inhibits(true, false, false));
|
||||
assert!(Mode::Fullscreen.inhibits(true, true, false));
|
||||
assert!(!Mode::None.inhibits(true, true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_validates_modes_and_keeps_idle_rules_out_of_placement() {
|
||||
let config = crate::config::Config::parse_source(
|
||||
"idle-inhibit { fullscreen-playback #false; }\nwindow-rule app-id=\"player\" idle-inhibit=\"visible\"\nwindow-rule app-id=\"player\" title=\"Private\" idle-inhibit=\"none\"\n",
|
||||
).unwrap().runtime_config().unwrap();
|
||||
assert!(!config.idle_inhibit.fullscreen_playback);
|
||||
let visible = crate::window_rules::resolve(&config.window_rules, Some("player"), None, false);
|
||||
assert_eq!(visible.idle_inhibit, Some(Mode::Visible));
|
||||
let none = crate::window_rules::resolve(&config.window_rules, Some("player"), Some("Private"), false);
|
||||
assert_eq!(none.idle_inhibit, Some(Mode::None));
|
||||
assert_eq!(crate::window_rules::live_result(visible, none, false), None);
|
||||
for mode in ["playing", "fullscreen", "fullscreen-playing", "none"] {
|
||||
assert!(
|
||||
crate::config::Config::parse_source(&format!(
|
||||
"window-rule app-id=\"player\" idle-inhibit=\"{mode}\"\n"
|
||||
))
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
crate::config::Config::parse_source("window-rule app-id=\"player\" idle-inhibit=\"alwayss\"\n").is_err()
|
||||
);
|
||||
assert!(crate::config::Config::parse_source("idle-inhibit { fullscreen-playback \"yes\"; }\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_identity_prefers_pid_and_rejects_ambiguous_or_closed_windows() {
|
||||
let player = media::Player {
|
||||
name: "org.mpris.MediaPlayer2.test".into(),
|
||||
owner: ":1.5".into(),
|
||||
pid: Some(42),
|
||||
desktop_entry: Some("player".into()),
|
||||
playing: true,
|
||||
};
|
||||
let window = AppWindow {
|
||||
id: WindowId(1),
|
||||
pid: Some(42),
|
||||
app_id: "other",
|
||||
};
|
||||
let unrelated = AppWindow {
|
||||
id: WindowId(2),
|
||||
pid: Some(99),
|
||||
app_id: "Player.desktop",
|
||||
};
|
||||
assert_eq!(player_window(&player, &[window, unrelated]), Some(window.id));
|
||||
assert_eq!(player_window(&player, &[unrelated]), Some(unrelated.id));
|
||||
assert_eq!(player_window(&player, &[]), None);
|
||||
assert_eq!(
|
||||
player_window(
|
||||
&player,
|
||||
&[
|
||||
window,
|
||||
AppWindow {
|
||||
id: WindowId(3),
|
||||
..window
|
||||
}
|
||||
]
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
player_window(
|
||||
&player,
|
||||
&[
|
||||
unrelated,
|
||||
AppWindow {
|
||||
id: WindowId(4),
|
||||
..unrelated
|
||||
}
|
||||
]
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! MPRIS reads and signal handling run off the compositor thread, without polling.
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use calloop::EventLoop;
|
||||
use calloop::channel::{Event, channel};
|
||||
use futures_lite::{StreamExt, future, stream};
|
||||
use zbus::zvariant::OwnedValue;
|
||||
use zbus::{Connection, MatchRule, MessageStream};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
const PREFIX: &str = "org.mpris.MediaPlayer2";
|
||||
const PATH: &str = "/org/mpris/MediaPlayer2";
|
||||
const PLAYER: &str = "org.mpris.MediaPlayer2.Player";
|
||||
const PROPERTIES: &str = "org.freedesktop.DBus.Properties";
|
||||
const MAX_PLAYERS: usize = 64;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Player {
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
pub pid: Option<u32>,
|
||||
pub desktop_entry: Option<String>,
|
||||
pub playing: bool,
|
||||
}
|
||||
|
||||
async fn bus_call<T: serde::de::DeserializeOwned + zbus::zvariant::Type>(
|
||||
connection: &Connection,
|
||||
member: &str,
|
||||
name: &str,
|
||||
) -> zbus::Result<T> {
|
||||
connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.DBus"),
|
||||
"/org/freedesktop/DBus",
|
||||
Some("org.freedesktop.DBus"),
|
||||
member,
|
||||
&(name,),
|
||||
)
|
||||
.await?
|
||||
.body()
|
||||
.deserialize()
|
||||
}
|
||||
|
||||
async fn property(connection: &Connection, owner: &str, interface: &str, name: &str) -> zbus::Result<OwnedValue> {
|
||||
connection
|
||||
.call_method(Some(owner), PATH, Some(PROPERTIES), "Get", &(interface, name))
|
||||
.await?
|
||||
.body()
|
||||
.deserialize()
|
||||
}
|
||||
|
||||
async fn refresh_player(connection: &Connection, player: &mut Player, identity: bool) {
|
||||
// Bind reads to the unique owner, never a name that can change mid-query.
|
||||
player.playing = property(connection, &player.owner, PLAYER, "PlaybackStatus")
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|value| String::try_from(value).ok())
|
||||
.is_some_and(|status| status == "Playing");
|
||||
|
||||
if identity {
|
||||
player.pid = bus_call(connection, "GetConnectionUnixProcessID", &player.owner)
|
||||
.await
|
||||
.ok();
|
||||
player.desktop_entry = property(connection, &player.owner, PREFIX, "DesktopEntry")
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|value| String::try_from(value).ok())
|
||||
.map(|value| crate::window_rules::normalize_app_id(&value))
|
||||
.filter(|value| !value.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(
|
||||
players: &HashMap<String, Player>,
|
||||
previous: &mut Vec<Player>,
|
||||
send: &mut impl FnMut(Vec<Player>) -> bool,
|
||||
) -> bool {
|
||||
let mut snapshot: Vec<_> = players.values().cloned().collect();
|
||||
snapshot.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
if snapshot == *previous {
|
||||
return true;
|
||||
}
|
||||
|
||||
*previous = snapshot.clone();
|
||||
send(snapshot)
|
||||
}
|
||||
|
||||
async fn monitor(connection: Connection, mut send: impl FnMut(Vec<Player>) -> bool) -> zbus::Result<()> {
|
||||
// Subscribe before discovery so startup and owner replacement cannot lose
|
||||
// a pause/stop event. Ignore unrelated property traffic such as position.
|
||||
let properties = MatchRule::builder()
|
||||
.msg_type(zbus::message::Type::Signal)
|
||||
.path(PATH)?
|
||||
.interface(PROPERTIES)?
|
||||
.member("PropertiesChanged")?
|
||||
.build();
|
||||
let owners = MatchRule::builder()
|
||||
.msg_type(zbus::message::Type::Signal)
|
||||
.sender("org.freedesktop.DBus")?
|
||||
.interface("org.freedesktop.DBus")?
|
||||
.member("NameOwnerChanged")?
|
||||
.arg0ns(PREFIX)?
|
||||
.build();
|
||||
let properties = MessageStream::for_match_rule(properties, &connection, Some(128)).await?;
|
||||
let owners = MessageStream::for_match_rule(owners, &connection, Some(128)).await?;
|
||||
let mut signals = stream::or(properties, owners);
|
||||
let mut players = HashMap::new();
|
||||
let mut previous = Vec::new();
|
||||
let names: Vec<String> = connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.DBus"),
|
||||
"/org/freedesktop/DBus",
|
||||
Some("org.freedesktop.DBus"),
|
||||
"ListNames",
|
||||
&(),
|
||||
)
|
||||
.await?
|
||||
.body()
|
||||
.deserialize()?;
|
||||
let mut names: Vec<_> = names
|
||||
.into_iter()
|
||||
.filter(|name| name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.')))
|
||||
.collect();
|
||||
names.sort_unstable();
|
||||
for name in names.into_iter().take(MAX_PLAYERS) {
|
||||
if let Ok(owner) = bus_call(&connection, "GetNameOwner", &name).await {
|
||||
let mut player = Player {
|
||||
name: name.clone(),
|
||||
owner,
|
||||
pid: None,
|
||||
desktop_entry: None,
|
||||
playing: false,
|
||||
};
|
||||
refresh_player(&connection, &mut player, true).await;
|
||||
players.insert(name, player);
|
||||
}
|
||||
}
|
||||
|
||||
if !publish(&players, &mut previous, &mut send) {
|
||||
return Ok(());
|
||||
}
|
||||
while let Some(message) = signals.next().await {
|
||||
let message = message?;
|
||||
let header = message.header();
|
||||
if header
|
||||
.member()
|
||||
.is_some_and(|member| member.as_str() == "NameOwnerChanged")
|
||||
{
|
||||
let (name, _, owner): (String, String, String) = message.body().deserialize()?;
|
||||
// Remove the old owner's state before looking up the replacement.
|
||||
players.remove(&name);
|
||||
if !publish(&players, &mut previous, &mut send) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !owner.is_empty() && players.len() < MAX_PLAYERS {
|
||||
let mut player = Player {
|
||||
name: name.clone(),
|
||||
owner,
|
||||
pid: None,
|
||||
desktop_entry: None,
|
||||
playing: false,
|
||||
};
|
||||
refresh_player(&connection, &mut player, true).await;
|
||||
players.insert(name, player);
|
||||
}
|
||||
} else {
|
||||
let Ok((interface, changed, invalidated)) =
|
||||
message
|
||||
.body()
|
||||
.deserialize::<(String, HashMap<String, OwnedValue>, Vec<String>)>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let identity = interface == PREFIX
|
||||
&& (changed.contains_key("DesktopEntry") || invalidated.iter().any(|field| field == "DesktopEntry"));
|
||||
let status = interface == PLAYER
|
||||
&& (changed.contains_key("PlaybackStatus")
|
||||
|| invalidated.iter().any(|field| field == "PlaybackStatus"));
|
||||
if !identity && !status {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(owner) = header.sender() else {
|
||||
continue;
|
||||
};
|
||||
for player in players.values_mut().filter(|player| player.owner == owner.as_str()) {
|
||||
// Re-read current state: a signal queued during discovery may
|
||||
// describe older playback than the startup snapshot.
|
||||
refresh_player(&connection, player, identity).await;
|
||||
}
|
||||
}
|
||||
|
||||
if !publish(&players, &mut previous, &mut send) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(zbus::Error::Failure("MPRIS signal connection ended".into()))
|
||||
}
|
||||
|
||||
pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (sender, receiver) = channel();
|
||||
event_loop.handle().insert_source(receiver, |event, _, state| {
|
||||
if let Event::Msg(players) = event {
|
||||
state.media_players = players;
|
||||
state.refresh_idle_inhibition();
|
||||
}
|
||||
})?;
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-playback".into())
|
||||
.spawn(move || {
|
||||
let mut had_players = false;
|
||||
loop {
|
||||
let result = future::block_on(async {
|
||||
let connection = zbus::connection::Builder::session()?
|
||||
.method_timeout(Duration::from_millis(500))
|
||||
.build()
|
||||
.await?;
|
||||
monitor(connection, |players| {
|
||||
had_players = !players.is_empty();
|
||||
sender.send(players).is_ok()
|
||||
})
|
||||
.await
|
||||
});
|
||||
match result {
|
||||
Ok(()) => break,
|
||||
Err(error) => {
|
||||
tracing::debug!(%error, "playback monitor disconnected; releasing automatic inhibition")
|
||||
}
|
||||
}
|
||||
|
||||
if had_players && sender.send(Vec::new()).is_err() {
|
||||
break;
|
||||
}
|
||||
had_players = false;
|
||||
// Reconnect only after failure; healthy operation sleeps on signals.
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -692,6 +692,17 @@ impl Ferese {
|
||||
"close" => self.close_focused_window(),
|
||||
"get-focused-window" => return Ok(self.focused_window_json()),
|
||||
"get-windows" => return Ok(self.windows_json()),
|
||||
"get-idle-inhibition" => {
|
||||
return Ok(json!({
|
||||
"inhibited": self.idle_notifier_state.is_inhibited(),
|
||||
"automatic": self.automatic_idle_inhibited,
|
||||
"fullscreen-playback": self.idle_inhibit.fullscreen_playback,
|
||||
"portal": self.portal_session.idle_inhibited(),
|
||||
"players": self.media_players.iter().map(|player| json!({
|
||||
"name": player.name, "desktop-entry": player.desktop_entry, "playing": player.playing,
|
||||
})).collect::<Vec<_>>(),
|
||||
}));
|
||||
}
|
||||
"get-keybindings" => {
|
||||
let map = crate::config::physical_keymap(&self.input_settings).ok();
|
||||
return Ok(json!(
|
||||
|
||||
@@ -12,6 +12,7 @@ mod frame_scheduler;
|
||||
mod gestures;
|
||||
mod grabs;
|
||||
mod handlers;
|
||||
mod idle_inhibition;
|
||||
mod input;
|
||||
mod input_capture;
|
||||
mod ipc;
|
||||
@@ -109,6 +110,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
});
|
||||
theme::init(&mut event_loop, &mut state, candidate)?;
|
||||
resume::init(&mut event_loop)?;
|
||||
idle_inhibition::media::init(&mut event_loop)?;
|
||||
overview::init_font_loader(&mut event_loop, &mut state)?;
|
||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||
|
||||
@@ -116,7 +116,7 @@ impl PortalSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_inhibited(&self) -> bool {
|
||||
pub(crate) fn idle_inhibited(&self) -> bool {
|
||||
self.inhibitors.values().any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
}
|
||||
|
||||
@@ -234,21 +234,6 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
let eligible = if self.idle_inhibitors.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
self.callback_outputs()
|
||||
};
|
||||
|
||||
self.idle_notifier_state.set_is_inhibited(
|
||||
self.idle_inhibitors
|
||||
.keys()
|
||||
.any(|surface| eligible.contains_key(&surface.into()))
|
||||
|| self.portal_session.idle_inhibited(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn end_portal_session(&mut self) {
|
||||
let token = self.portal_session.query.as_ref().map(|query| query.token);
|
||||
let result =
|
||||
|
||||
+14
-1
@@ -443,7 +443,7 @@ pub struct Ferese {
|
||||
pub(crate) input_settings: InputSettings,
|
||||
pub(crate) bindings: BindingSet,
|
||||
workspace_auto_back_and_forth: bool,
|
||||
window_rules: Vec<WindowRule>,
|
||||
pub(crate) window_rules: Vec<WindowRule>,
|
||||
pub(crate) theme_settings: ThemeSettings,
|
||||
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
||||
animations_enabled: bool,
|
||||
@@ -464,6 +464,9 @@ pub struct Ferese {
|
||||
pub intercepted_keys: HashSet<Keycode>,
|
||||
pub(crate) swipe: Swipe,
|
||||
pub idle_inhibitors: HashMap<WlSurface, usize>,
|
||||
pub(crate) idle_inhibit: crate::idle_inhibition::Settings,
|
||||
pub(crate) media_players: Vec<crate::idle_inhibition::media::Player>,
|
||||
pub(crate) automatic_idle_inhibited: bool,
|
||||
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
|
||||
pub direct_backend: Option<DirectBackendState>,
|
||||
_ipc_socket: Option<IpcSocketGuard>,
|
||||
@@ -517,6 +520,7 @@ pub struct Ferese {
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeConfig {
|
||||
pub(crate) lock_idle: IdleSettings,
|
||||
pub(crate) idle_inhibit: crate::idle_inhibition::Settings,
|
||||
pub(crate) autostart: Vec<DaemonConfig>,
|
||||
pub(crate) overview_font_family: String,
|
||||
pub(crate) wallpaper: WallpaperConfig,
|
||||
@@ -701,6 +705,9 @@ impl Ferese {
|
||||
intercepted_keys: HashSet::new(),
|
||||
swipe: Swipe::default(),
|
||||
idle_inhibitors: HashMap::new(),
|
||||
idle_inhibit: config.idle_inhibit,
|
||||
media_players: Vec::new(),
|
||||
automatic_idle_inhibited: false,
|
||||
active_shortcuts_inhibitor: None,
|
||||
direct_backend: None,
|
||||
_ipc_socket: None,
|
||||
@@ -852,6 +859,8 @@ impl Ferese {
|
||||
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
|
||||
}
|
||||
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
|
||||
let idle_policy_changed = self.idle_inhibit != config.idle_inhibit || old_rules != self.window_rules;
|
||||
self.idle_inhibit = config.idle_inhibit;
|
||||
self.theme_settings = config.theme_settings;
|
||||
self.inactive_dim = config.inactive_dim;
|
||||
self.column_width_presets = config.column_width_presets;
|
||||
@@ -885,6 +894,10 @@ impl Ferese {
|
||||
self.reapply_window_rules(&old_rules);
|
||||
}
|
||||
|
||||
if idle_policy_changed {
|
||||
self.refresh_idle_inhibition();
|
||||
}
|
||||
|
||||
self.overview.set_font_family(config.overview_font_family);
|
||||
if self.wallpaper.configuration() != &config.wallpaper {
|
||||
self.wallpaper.reload(config.wallpaper);
|
||||
|
||||
@@ -11,6 +11,7 @@ pub(crate) struct WindowRuleConfig {
|
||||
pub height: Option<f64>,
|
||||
pub fullscreen: Option<bool>,
|
||||
pub block_out_from_screencasts: Option<bool>,
|
||||
pub idle_inhibit: Option<crate::idle_inhibition::Mode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -24,6 +25,13 @@ pub struct WindowRule {
|
||||
height: Option<f64>,
|
||||
fullscreen: Option<bool>,
|
||||
block_out_from_screencasts: Option<bool>,
|
||||
idle_inhibit: Option<crate::idle_inhibition::Mode>,
|
||||
}
|
||||
|
||||
impl WindowRule {
|
||||
pub(crate) fn idle_policy(&self) -> Option<crate::idle_inhibition::Mode> {
|
||||
self.idle_inhibit
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
@@ -34,6 +42,7 @@ pub struct WindowRuleResult {
|
||||
pub height: Option<f64>,
|
||||
pub fullscreen: Option<bool>,
|
||||
pub block_out_from_screencasts: Option<bool>,
|
||||
pub idle_inhibit: Option<crate::idle_inhibition::Mode>,
|
||||
}
|
||||
|
||||
/// Only changed matches should override an existing window's manual state.
|
||||
@@ -42,9 +51,11 @@ pub(crate) fn live_result(
|
||||
mut new: WindowRuleResult,
|
||||
transient: bool,
|
||||
) -> Option<WindowRuleResult> {
|
||||
// Capture policy is resolved independently of placement at capture time.
|
||||
// Capture and idle policies are reevaluated without changing placement.
|
||||
old.block_out_from_screencasts = None;
|
||||
new.block_out_from_screencasts = None;
|
||||
old.idle_inhibit = None;
|
||||
new.idle_inhibit = None;
|
||||
if old == new {
|
||||
return None;
|
||||
}
|
||||
@@ -97,6 +108,7 @@ pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>,
|
||||
result.height = rule.height.or(result.height);
|
||||
result.fullscreen = rule.fullscreen.or(result.fullscreen);
|
||||
result.block_out_from_screencasts = rule.block_out_from_screencasts.or(result.block_out_from_screencasts);
|
||||
result.idle_inhibit = rule.idle_inhibit.or(result.idle_inhibit);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -137,6 +149,7 @@ fn validate_rule(index: usize, rule: &WindowRuleConfig) -> Result<WindowRule, St
|
||||
height,
|
||||
fullscreen: rule.fullscreen,
|
||||
block_out_from_screencasts: rule.block_out_from_screencasts,
|
||||
idle_inhibit: rule.idle_inhibit,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,7 +184,7 @@ pub(crate) fn is_native_dialog(app_id: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_app_id(value: &str) -> String {
|
||||
pub(crate) fn normalize_app_id(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
.strip_suffix(".desktop")
|
||||
@@ -215,6 +228,7 @@ mod tests {
|
||||
height: None,
|
||||
fullscreen: None,
|
||||
block_out_from_screencasts: None,
|
||||
idle_inhibit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +360,7 @@ mod tests {
|
||||
height: None,
|
||||
fullscreen: None,
|
||||
block_out_from_screencasts: None,
|
||||
idle_inhibit: None,
|
||||
};
|
||||
let invalid_size = WindowRuleConfig {
|
||||
width: Some(f64::NAN),
|
||||
|
||||
Reference in new issue
Block a user