Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 23 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
[workspace]
members = ["hardware", "data", "ui"]
default-members = [".", "hardware", "data", "ui"]
resolver = "3"

[workspace.package]
version = "0.1.6"
edition = "2024"
license = "MIT"

[package]
name = "windisplaymanager_rs"
version = "0.1.5"
edition = "2024"
description = "Windows Display Manager - DDC/CI monitor control with GUI"
license = "MIT"
version.workspace = true
edition.workspace = true
license.workspace = true

[[bin]]
name = "windisplaymanager_rs"
Expand All @@ -17,6 +27,15 @@ lto = "thin"
winres = "0.1"

[dependencies]
data.workspace = true
ui.workspace = true
env_logger.workspace = true
libcosmic.workspace = true

[workspace.dependencies]
hardware = { path = "hardware" }
data = { path = "data" }
ui = { path = "ui" }
ddc = "0.2"
ddc-winapi = "0.2"
global-hotkey = "0.8"
Expand All @@ -38,7 +57,7 @@ windows-sys = { version = "0.61", features = [
"Win32_UI_WindowsAndMessaging",
] }

[dependencies.libcosmic]
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git"
default-features = false
features = [
Expand Down
23 changes: 23 additions & 0 deletions data/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "data"
description = "Application state and logic for Windows Display Manager"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
hardware.workspace = true
global-hotkey.workspace = true
thiserror.workspace = true
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
dirs.workspace = true
log.workspace = true
tray-icon.workspace = true
winreg.workspace = true
anyhow.workspace = true
windows-sys.workspace = true

[dev-dependencies]
hardware = { workspace = true, features = ["test-util"] }
2 changes: 1 addition & 1 deletion src/app/actions.rs → data/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ impl<Job> ActionExecutor<Job> {
#[cfg(test)]
mod tests {
use super::*;
use crate::ddc::tests::{FakeVcp, fake_session, key, monitor_state};
use crate::ddc::test_util::{FakeVcp, fake_session, key, monitor_state};

#[derive(Default)]
struct FakeHardware {
Expand Down
12 changes: 6 additions & 6 deletions src/config.rs → data/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ mod tests {
let path = dir.0.join("config.json");
let mut config = AppConfig::default();
let mut hotkey = Hotkey::new_empty();
let key = crate::ddc::tests::key(1);
let key = crate::ddc::test_util::key(1);
let unknown: MonitorTarget = serde_json::from_str("\"future-monitor:v2:abc\"").unwrap();
hotkey.actions[0] = HotkeyActionSpec {
target: ActionTarget::InputSource,
Expand Down Expand Up @@ -1171,7 +1171,7 @@ mod tests {
assert_eq!(action.monitor_inputs[0].input_source, InputSource::Hdmi2);
assert!(
action
.resolve_monitors(&[crate::ddc::tests::monitor_state().info])
.resolve_monitors(&[crate::ddc::test_util::monitor_state().info])
.is_err()
);
}
Expand All @@ -1194,7 +1194,7 @@ mod tests {
],
..Default::default()
};
let live = crate::ddc::tests::monitor_state().info;
let live = crate::ddc::test_util::monitor_state().info;
assert!(
action
.resolve_monitors(std::slice::from_ref(&live))
Expand All @@ -1219,14 +1219,14 @@ mod tests {

#[test]
fn whole_action_resolution_rejects_legacy_missing_duplicate_and_empty_targets_including_off() {
let live = crate::ddc::tests::monitor_state().info;
let live = crate::ddc::test_util::monitor_state().info;
let stable = MonitorTarget::Stable(live.key.clone());
for action_type in [ActionType::Set, ActionType::Offset, ActionType::Off] {
for targets in [
vec![stable.clone(), 1.into()],
vec![
stable.clone(),
MonitorTarget::Stable(crate::ddc::tests::key(2)),
MonitorTarget::Stable(crate::ddc::test_util::key(2)),
],
vec![stable.clone(), stable.clone()],
Vec::new(),
Expand Down Expand Up @@ -1261,7 +1261,7 @@ mod tests {

#[test]
fn explicit_resolution_uses_keys_after_numbers_change() {
let mut live = crate::ddc::tests::monitor_state().info;
let mut live = crate::ddc::test_util::monitor_state().info;
let action = HotkeyActionSpec {
all_monitors: false,
monitors: vec![MonitorTarget::Stable(live.key.clone())],
Expand Down
4 changes: 2 additions & 2 deletions src/app/coordination.rs → data/src/coordination.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::actions::{self, ActionExecutor, HardwareJob, HardwareOutcome, PreparedJob};
use crate::actions::{self, ActionExecutor, HardwareJob, HardwareOutcome, PreparedJob};
use crate::config::{HotkeyActionSpec, MonitorTarget, TurnOffBehavior};
use crate::ddc::{self, MonitorInfo, MonitorKey, MonitorState};
use std::collections::HashMap;
Expand Down Expand Up @@ -357,7 +357,7 @@ impl HardwareCoordinator {
mod tests {
use super::*;
use crate::config::ActionTarget;
use crate::ddc::tests::{FakeVcp, fake_session, key, monitor_state};
use crate::ddc::test_util::{FakeVcp, fake_session, key, monitor_state};
use crate::ddc::{InputSource, VCP_BRIGHTNESS, VCP_CONTRAST, VCP_INPUT_SOURCE};
use std::collections::VecDeque;

Expand Down
4 changes: 2 additions & 2 deletions src/app/debounce.rs → data/src/debounce.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum SliderFeature {
pub enum SliderFeature {
Brightness,
Contrast,
}
Expand All @@ -20,7 +20,7 @@ struct PendingChange {

/// Drafts are separate from both confirmed readings and submitted hardware jobs.
#[derive(Debug, Default)]
pub(super) struct SliderDebounce {
pub struct SliderDebounce {
revision: u64,
pending: HashMap<(u32, SliderFeature), PendingChange>,
}
Expand Down
68 changes: 14 additions & 54 deletions src/hotkeys.rs → data/src/hotkeys.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
use crate::config::{AppConfig, HotkeyActionSpec, build_hotkey_map};
use cosmic::iced::futures::SinkExt;
use cosmic::iced::{Subscription, stream};
use global_hotkey::hotkey::HotKey;
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::Duration;

// ---------------------------------------------------------------------------
// HotkeyManager – registers hotkeys and keeps the ID → action-chain mapping
Expand Down Expand Up @@ -126,62 +122,26 @@ impl HotkeyManager {
}

// ---------------------------------------------------------------------------
// Subscription – polls global hotkey events and emits action chains
// Event polling – drained by the UI's hotkey subscription
// ---------------------------------------------------------------------------

/// Identity/data wrapper for the hotkey subscription. Hashing the set of
/// registered hotkey ids ensures the subscription restarts when the bindings
/// change.
struct HotkeyData {
generation: u64,
registered_ids: Vec<u32>,
}

impl Hash for HotkeyData {
fn hash<H: Hasher>(&self, state: &mut H) {
self.generation.hash(state);
self.registered_ids.hash(state);
}
}

fn should_emit(registered_ids: &[u32], event_id: u32, state: HotKeyState) -> bool {
state == HotKeyState::Pressed && registered_ids.binary_search(&event_id).is_ok()
}

/// Create an iced `Subscription` that polls global hotkey events.
///
/// The subscription emits the triggered hotkey's OS id whenever a registered
/// hotkey is pressed. The caller resolves the current action chain by id.
pub fn hotkey_subscription(mut registered_ids: Vec<u32>, generation: u64) -> Subscription<u32> {
registered_ids.sort_unstable();
registered_ids.dedup();

Subscription::run_with(
HotkeyData {
generation,
registered_ids,
},
|data| {
let registered_ids = data.registered_ids.clone();
stream::channel(
16,
move |mut emitter: cosmic::iced::futures::channel::mpsc::Sender<u32>| async move {
let receiver = GlobalHotKeyEvent::receiver();
for _ in receiver.try_iter() {}
loop {
while let Ok(event) = receiver.try_recv() {
if should_emit(&registered_ids, event.id(), event.state)
&& emitter.send(event.id()).await.is_err()
{
return;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
},
)
},
)
/// Drops events queued before a listener started.
pub fn discard_pending_events() {
for _ in GlobalHotKeyEvent::receiver().try_iter() {}
}

/// OS ids of registered hotkeys pressed since the last call. `registered_ids`
/// must be sorted.
pub fn take_pressed(registered_ids: &[u32]) -> Vec<u32> {
GlobalHotKeyEvent::receiver()
.try_iter()
.filter(|event| should_emit(registered_ids, event.id(), event.state))
.map(|event| event.id())
.collect()
}

#[cfg(test)]
Expand Down
13 changes: 13 additions & 0 deletions data/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pub use hardware::{ccd, ddc};

pub mod actions;
pub mod config;
pub mod coordination;
pub mod debounce;
pub mod hotkeys;
pub mod persistence;
pub mod presentation;
pub mod profiles;
pub mod startup;
pub mod state;
pub mod tray;
File renamed without changes.
Loading
Loading