mirror of
https://gitlab.freedesktop.org/pipewire/helvum.git
synced 2026-05-09 07:28:12 +02:00
feat: add TOML configuration for node layout
Add custom column based layout configuration using simple glob style pattern matching on node names. It will look for configuration files located in ~/.config/helvum or in the cwd (which takes precedence). The configuration is validated before being used.
This commit is contained in:
parent
980bb139e9
commit
ac08b1c312
7 changed files with 558 additions and 22 deletions
183
Cargo.lock
generated
183
Cargo.lock
generated
|
|
@ -172,6 +172,27 @@ version = "0.8.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
|
|
@ -335,6 +356,17 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.19.3"
|
||||
|
|
@ -363,7 +395,7 @@ dependencies = [
|
|||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -560,6 +592,7 @@ name = "helvum"
|
|||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"dirs",
|
||||
"glib",
|
||||
"libadwaita",
|
||||
"libc",
|
||||
|
|
@ -567,6 +600,8 @@ dependencies = [
|
|||
"natord",
|
||||
"once_cell",
|
||||
"pipewire",
|
||||
"serde",
|
||||
"toml 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -634,9 +669,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
|
|
@ -648,6 +683,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libspa"
|
||||
version = "0.8.0"
|
||||
|
|
@ -736,6 +781,12 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.19.3"
|
||||
|
|
@ -863,6 +914,17 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.5"
|
||||
|
|
@ -994,7 +1056,7 @@ dependencies = [
|
|||
"cfg-expr",
|
||||
"heck 0.4.1",
|
||||
"pkg-config",
|
||||
"toml",
|
||||
"toml 0.7.8",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
|
|
@ -1036,6 +1098,18 @@ dependencies = [
|
|||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.5"
|
||||
|
|
@ -1058,6 +1132,19 @@ dependencies = [
|
|||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.1"
|
||||
|
|
@ -1099,6 +1186,12 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
@ -1121,13 +1214,37 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1136,51 +1253,93 @@ version = "0.52.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.52.4",
|
||||
"windows_aarch64_msvc 0.52.4",
|
||||
"windows_i686_gnu 0.52.4",
|
||||
"windows_i686_msvc 0.52.4",
|
||||
"windows_x86_64_gnu 0.52.4",
|
||||
"windows_x86_64_gnullvm 0.52.4",
|
||||
"windows_x86_64_msvc 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.4"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ async-channel = "2.2"
|
|||
log = "0.4.11"
|
||||
|
||||
once_cell = "1.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
dirs = "5.0"
|
||||
|
||||
libc = "0.2"
|
||||
natord = "1.0.9"
|
||||
|
|
|
|||
21
docs/example_layout.toml
Normal file
21
docs/example_layout.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Example Helvum Configuration
|
||||
|
||||
[layout]
|
||||
columns = [
|
||||
{ x = 20, name = "sources" },
|
||||
{ x = 500, name = "filters" },
|
||||
{ x = 950, name = "apps" },
|
||||
{ x = 1200, name = "sinks" }
|
||||
]
|
||||
|
||||
rules = [
|
||||
{ pattern = "Firefox*", column = "sources" },
|
||||
{ pattern = "vlc*", column = "sources" },
|
||||
{ pattern = "Built-in*", column = "sources", node_type = "source" },
|
||||
{ pattern = "Built-in*", column = "sinks", node_type = "sink" },
|
||||
]
|
||||
|
||||
[layout.defaults]
|
||||
source = 20
|
||||
other = 500
|
||||
sink = 1200
|
||||
76
docs/layout_configuration.md
Normal file
76
docs/layout_configuration.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Layout Configuration
|
||||
|
||||
This feature allows you to customize node placement in Helvum using a configuration file with column-based layout and pattern matching.
|
||||
|
||||
## Configuration File Format
|
||||
|
||||
The configuration uses TOML format with three main sections:
|
||||
|
||||
### 1. Columns Section
|
||||
Define columns with their X positions:
|
||||
```toml
|
||||
[layout]
|
||||
columns = [
|
||||
{ x = 50.0, name = "sources" },
|
||||
{ x = 300.0, name = "filters" },
|
||||
{ x = 550.0, name = "apps" },
|
||||
{ x = 800.0, name = "sinks" }
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Rules Section
|
||||
Pattern matching rules (processed in order, first match wins):
|
||||
```toml
|
||||
[[layout.rules]]
|
||||
pattern = "Firefox*" # Simple glob patterns
|
||||
column = "apps"
|
||||
node_type = "filter" # Optional: restrict to specific node types
|
||||
|
||||
[[layout.rules]]
|
||||
pattern = "*Microphone*"
|
||||
column = "sources"
|
||||
node_type = "source" # Optional: only match source nodes
|
||||
|
||||
[[layout.rules]]
|
||||
pattern = "*sink*" # Case-insensitive matching
|
||||
column = "sinks"
|
||||
```
|
||||
|
||||
**Supported Patterns:**
|
||||
- `*` - matches any number of characters
|
||||
- `?` - matches exactly one character
|
||||
- Case-insensitive matching
|
||||
|
||||
**Node Type Constraints (optional):**
|
||||
- `"source"` - matches NodeType::Output nodes only
|
||||
- `"sink"` - matches NodeType::Input nodes only
|
||||
- `"filter"` - matches nodes that are neither input nor output
|
||||
|
||||
### 3. Defaults Section
|
||||
Default column positions when no pattern matches:
|
||||
```toml
|
||||
[layout.defaults]
|
||||
sink = 800.0
|
||||
source = 50.0
|
||||
other = 300.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Standard Locations
|
||||
The application will automatically look for configuration files in this order:
|
||||
1. `./layout.toml` (current working directory where helvum is launched)
|
||||
2. `~/.config/helvum/layout.toml` (user config directory)
|
||||
|
||||
|
||||
## Example Configuration
|
||||
|
||||
See `example_layout.toml` for a complete example with common audio application patterns.
|
||||
|
||||
## Validation Requirements
|
||||
|
||||
- Layout must contain at least one column
|
||||
- Layout must contain at least one rule
|
||||
- All rule column references must exist in the columns list
|
||||
- Columns must be spaced at least `COLUMN_WIDTH` pixels apart to prevent overlap
|
||||
- Invalid configurations will log warnings and fall back to original behavior
|
||||
260
src/config.rs
Normal file
260
src/config.rs
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::NodeType;
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub layout: Layout,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct Layout {
|
||||
pub columns: Vec<Column>,
|
||||
pub rules: Vec<MatchRule>,
|
||||
#[serde(default)]
|
||||
pub defaults: Option<Defaults>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Defaults {
|
||||
pub source: f32,
|
||||
pub sink: f32,
|
||||
pub other: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Column {
|
||||
pub x: f32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct MatchRule {
|
||||
pub pattern: String,
|
||||
pub column: String,
|
||||
pub node_type: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new Config instance and load from default locations
|
||||
pub fn new() -> Self {
|
||||
match Self::load() {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
log::info!("No configuration file found");
|
||||
Config::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Option<Self> {
|
||||
let paths = [
|
||||
Some(std::path::PathBuf::from("layout.toml")),
|
||||
dirs::config_dir().map(|mut p| {
|
||||
p.push("helvum");
|
||||
p.push("layout.toml");
|
||||
p
|
||||
}),
|
||||
];
|
||||
|
||||
for path in paths.into_iter().flatten() {
|
||||
log::debug!(
|
||||
"trying to load configuration from '{}'",
|
||||
path.canonicalize().unwrap_or(path.clone()).display()
|
||||
);
|
||||
if path.exists() {
|
||||
match Self::load_from_file(&path) {
|
||||
Ok(config) => {
|
||||
log::info!(
|
||||
"Loaded configuration from '{}'",
|
||||
path.canonicalize().unwrap_or(path).display()
|
||||
);
|
||||
return Some(config);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to load configuration from {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
if self.layout.columns.is_empty() {
|
||||
return Err("Layout must contain at least one column".to_string());
|
||||
}
|
||||
|
||||
if self.layout.rules.is_empty() {
|
||||
return Err(
|
||||
"Layout must contain at least one rule to assign nodes to columns".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let column_names: std::collections::HashSet<_> =
|
||||
self.layout.columns.iter().map(|c| &c.name).collect();
|
||||
|
||||
for rule in &self.layout.rules {
|
||||
if !column_names.contains(&rule.column) {
|
||||
return Err(format!(
|
||||
"Rule references unknown column '{}'. Available columns: {:?}",
|
||||
rule.column,
|
||||
column_names.iter().collect::<Vec<_>>()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_columns = self.layout.columns.clone();
|
||||
sorted_columns.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
for window in sorted_columns.windows(2) {
|
||||
let distance = (window[1].x - window[0].x).abs();
|
||||
if distance < 50.0 {
|
||||
return Err(format!(
|
||||
"Columns '{}' (x={}) and '{}' (x={}) are too close together. Minimum distance should be {} pixels.",
|
||||
window[0].name, window[0].x,
|
||||
window[1].name, window[1].x,
|
||||
50.0
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get column position for a node, trying rules first, then defaults
|
||||
pub fn get_column(
|
||||
&self,
|
||||
node_name: &str,
|
||||
node_type: Option<&NodeType>,
|
||||
) -> Option<(f32, String)> {
|
||||
for rule in &self.layout.rules {
|
||||
if glob_match(&rule.pattern, node_name)
|
||||
&& self.node_type_matches(&rule.node_type, node_type)
|
||||
{
|
||||
for column in &self.layout.columns {
|
||||
if column.name == rule.column {
|
||||
return Some((column.x, column.name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.get_defaults_column(node_type)
|
||||
}
|
||||
|
||||
fn get_defaults_column(&self, node_type: Option<&NodeType>) -> Option<(f32, String)> {
|
||||
if let Some(defaults) = &self.layout.defaults {
|
||||
match node_type {
|
||||
Some(NodeType::Output) => Some((defaults.source, "source".to_string())),
|
||||
Some(NodeType::Input) => Some((defaults.sink, "sink".to_string())),
|
||||
None => Some((defaults.other, "other".to_string())),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type_matches(
|
||||
&self,
|
||||
rule_node_type: &Option<String>,
|
||||
node_type: Option<&NodeType>,
|
||||
) -> bool {
|
||||
match rule_node_type.as_deref() {
|
||||
None => true, // Rule has no type constraint, matches any node
|
||||
Some("source") => node_type == Some(&NodeType::Output),
|
||||
Some("sink") => node_type == Some(&NodeType::Input),
|
||||
Some("filter") => node_type.is_none(),
|
||||
Some(unknown_type) => {
|
||||
log::warn!("Unknown node type in rule: {}", unknown_type);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn glob_match(pattern: &str, text: &str) -> bool {
|
||||
glob_match_recursive(pattern, text)
|
||||
}
|
||||
|
||||
fn glob_match_recursive(pattern: &str, text: &str) -> bool {
|
||||
match (pattern.chars().next(), text.chars().next()) {
|
||||
(None, None) => true,
|
||||
(Some('*'), _) => {
|
||||
if glob_match_recursive(&pattern[1..], text) {
|
||||
return true;
|
||||
}
|
||||
if text.chars().next().is_some() {
|
||||
glob_match_recursive(pattern, &text[1..])
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
(Some('?'), Some(_)) => glob_match_recursive(&pattern[1..], &text[1..]),
|
||||
(Some(p), Some(t)) if p.eq_ignore_ascii_case(&t) => {
|
||||
glob_match_recursive(&pattern[1..], &text[1..])
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_glob_match() {
|
||||
assert!(glob_match("hello", "hello"));
|
||||
assert!(glob_match("hello*", "hello"));
|
||||
assert!(glob_match("hello*", "hello world"));
|
||||
assert!(glob_match("*world", "hello world"));
|
||||
assert!(glob_match("*", "anything"));
|
||||
assert!(glob_match("h?llo", "hello"));
|
||||
assert!(glob_match("h?llo", "hallo"));
|
||||
assert!(!glob_match("hello", "world"));
|
||||
assert!(!glob_match("h?llo", "hllo"));
|
||||
assert!(glob_match("FIREFOX*", "firefox browser"));
|
||||
assert!(glob_match("firefox*", "FIREFOX BROWSER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation() {
|
||||
let config = Config {
|
||||
layout: Layout {
|
||||
columns: vec![
|
||||
Column {
|
||||
x: 50.0,
|
||||
name: "sources".to_string(),
|
||||
},
|
||||
Column {
|
||||
x: 300.0,
|
||||
name: "apps".to_string(),
|
||||
},
|
||||
],
|
||||
rules: vec![MatchRule {
|
||||
pattern: "Firefox*".to_string(),
|
||||
column: "apps".to_string(),
|
||||
node_type: None,
|
||||
}],
|
||||
defaults: Some(Defaults {
|
||||
source: 50.0,
|
||||
sink: 300.0,
|
||||
other: 550.0,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod application;
|
||||
mod config;
|
||||
mod graph_manager;
|
||||
mod pipewire_connection;
|
||||
mod ui;
|
||||
|
|
@ -86,7 +87,7 @@ pub enum PipewireMessage {
|
|||
Disconnected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NodeType {
|
||||
Input,
|
||||
Output,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use adw::{
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use super::{Link, Node, Port};
|
||||
use crate::NodeType;
|
||||
use crate::{config::Config, NodeType};
|
||||
|
||||
const CANVAS_SIZE: f64 = 5000.0;
|
||||
|
||||
|
|
@ -96,6 +96,9 @@ mod imp {
|
|||
|
||||
// This keeps track of an ongoing move view gesture.
|
||||
pub move_view_state: Cell<(f64, f64)>,
|
||||
|
||||
// Cache configuration from file
|
||||
pub config: RefCell<Option<Config>>,
|
||||
}
|
||||
|
||||
impl Default for GraphView {
|
||||
|
|
@ -112,6 +115,7 @@ mod imp {
|
|||
zoom_gesture_initial_zoom: Default::default(),
|
||||
zoom_gesture_anchor: Default::default(),
|
||||
move_view_state: Default::default(),
|
||||
config: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -731,6 +735,17 @@ impl GraphView {
|
|||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// Get the cached configuration, creating it if not already cached
|
||||
fn get_config(&self) -> Config {
|
||||
let mut cached = self.imp().config.borrow_mut();
|
||||
|
||||
if cached.is_none() {
|
||||
*cached = Some(Config::new());
|
||||
}
|
||||
|
||||
cached.as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn zoom_factor(&self) -> f64 {
|
||||
self.property("zoom-factor")
|
||||
}
|
||||
|
|
@ -777,14 +792,15 @@ impl GraphView {
|
|||
let imp = self.imp();
|
||||
node.set_parent(self);
|
||||
|
||||
// Place widgets in colums of 3, growing down
|
||||
let x = if let Some(node_type) = node_type {
|
||||
match node_type {
|
||||
NodeType::Output => 20.0,
|
||||
NodeType::Input => 820.0,
|
||||
}
|
||||
} else {
|
||||
420.0
|
||||
let node_name = node.property::<String>("node-name");
|
||||
|
||||
let x = match self.get_config().get_column(&node_name, node_type.as_ref()) {
|
||||
Some((x_from_config, _)) => x_from_config,
|
||||
None => match node_type {
|
||||
Some(NodeType::Output) => 20.0,
|
||||
Some(NodeType::Input) => 820.0,
|
||||
None => 420.0,
|
||||
},
|
||||
};
|
||||
|
||||
// FIXME: We are currently using a constant 120 for the height of the nodes, this could cause problems for nodes with a lot of ports.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue