mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-03-22 17:39:20 +01:00
feat(config): add support for v2 asc json
This commit adds support for a v2 format of the application specific configuration file, centralizing on JSON to maximize the knowledge crossover for people already familiar with the types used in komorebi.json. The biggest difference besides the format change is that matchers must be used explicitly for every kind of rule, rather than being able to specify options on a default rule. This is a bit more verbose, but ultimately allows for significantly more flexibility.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
|
||||
135
komorebi/src/core/asc.rs
Normal file
135
komorebi/src/core/asc.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum AscApplicationRulesOrSchema {
|
||||
AscApplicationRules(AscApplicationRules),
|
||||
Schema(String),
|
||||
}
|
||||
|
||||
impl Deref for ApplicationSpecificConfiguration {
|
||||
type Target = BTreeMap<String, AscApplicationRulesOrSchema>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ApplicationSpecificConfiguration {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationSpecificConfiguration {
|
||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(pathbuf)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rules that determine how an application is handled
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AscApplicationRules {
|
||||
/// Rules to ignore specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to forcibly manage specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage: Option<Vec<MatchingRule>>,
|
||||
/// Rules to manage specific windows as floating windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating: Option<Vec<MatchingRule>>,
|
||||
/// Rules to ignore specific windows from the transparency feature
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency_ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which minimize to the tray or have multiple windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which are slow to send initial event notifications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl From<Vec<ApplicationConfiguration>> for ApplicationSpecificConfiguration {
|
||||
fn from(value: Vec<ApplicationConfiguration>) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for entry in &value {
|
||||
let key = entry.name.clone();
|
||||
let mut rules = AscApplicationRules {
|
||||
ignore: None,
|
||||
manage: None,
|
||||
floating: None,
|
||||
transparency_ignore: None,
|
||||
tray_and_multi_window: None,
|
||||
layered: None,
|
||||
object_name_change: None,
|
||||
slow_application: None,
|
||||
};
|
||||
|
||||
rules.ignore = entry.ignore_identifiers.clone();
|
||||
|
||||
if let Some(options) = &entry.options {
|
||||
for opt in options {
|
||||
match opt {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
rules.object_name_change =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
rules.layered =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
rules.tray_and_multi_window =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
rules.manage =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rules.ignore.is_some()
|
||||
|| rules.manage.is_some()
|
||||
|| rules.floating.is_some()
|
||||
|| rules.transparency_ignore.is_some()
|
||||
|| rules.tray_and_multi_window.is_some()
|
||||
|| rules.layered.is_some()
|
||||
|| rules.object_name_change.is_some()
|
||||
|| rules.slow_application.is_some()
|
||||
{
|
||||
map.insert(key, AscApplicationRulesOrSchema::AscApplicationRules(rules));
|
||||
}
|
||||
}
|
||||
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod asc;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
|
||||
@@ -50,6 +50,8 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
|
||||
use crate::asc::ApplicationSpecificConfiguration;
|
||||
use crate::asc::AscApplicationRulesOrSchema;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::core::config_generation::ApplicationConfiguration;
|
||||
use crate::core::config_generation::ApplicationConfigurationGenerator;
|
||||
@@ -961,51 +963,140 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(path) = &self.app_specific_configuration_path {
|
||||
let path = resolve_home_path(path)?;
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
match path.extension() {
|
||||
None => {}
|
||||
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
|
||||
"yaml" => {
|
||||
tracing::info!("loading applications.yaml from: {}", path.display());
|
||||
let path = resolve_home_path(path)?;
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for mut entry in asc {
|
||||
if let Some(rules) = &mut entry.ignore_identifiers {
|
||||
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
for mut entry in asc {
|
||||
if let Some(rules) = &mut entry.ignore_identifiers {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut ignore_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {} // deprecated
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {} // deprecated
|
||||
}
|
||||
}
|
||||
}
|
||||
"json" => {
|
||||
tracing::info!("loading applications.json from: {}", path.display());
|
||||
let path = resolve_home_path(path)?;
|
||||
let mut asc = ApplicationSpecificConfiguration::load(&path)?;
|
||||
|
||||
for entry in asc.values_mut() {
|
||||
match entry {
|
||||
AscApplicationRulesOrSchema::Schema(_) => {}
|
||||
AscApplicationRulesOrSchema::AscApplicationRules(entry) => {
|
||||
if let Some(rules) = &mut entry.ignore {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut ignore_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.manage {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.floating {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut floating_applications,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.transparency_ignore {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut transparency_blacklist,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.tray_and_multi_window {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.layered {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.object_name_change {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.slow_application {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut slow_application_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ use fs_tail::TailedFile;
|
||||
use komorebi_client::resolve_home_path;
|
||||
use komorebi_client::send_message;
|
||||
use komorebi_client::send_query;
|
||||
use komorebi_client::ApplicationConfiguration;
|
||||
use komorebi_client::ApplicationSpecificConfiguration;
|
||||
use komorebi_client::Notification;
|
||||
use lazy_static::lazy_static;
|
||||
use miette::NamedSource;
|
||||
@@ -844,6 +844,12 @@ struct FormatAppSpecificConfiguration {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct ConvertAppSpecificConfiguration {
|
||||
/// YAML file from which the application-specific configurations should be loaded
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct AltFocusHack {
|
||||
#[clap(value_enum)]
|
||||
@@ -1324,7 +1330,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "pwsh-asc")]
|
||||
PwshAppSpecificConfiguration(PwshAppSpecificConfiguration),
|
||||
/// Format a YAML file for use with the 'ahk-app-specific-configuration' command
|
||||
/// Convert a v1 ASC YAML file to a v2 ASC JSON file
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "convert-asc")]
|
||||
ConvertAppSpecificConfiguration(ConvertAppSpecificConfiguration),
|
||||
/// Format a YAML file for use with the 'app-specific-configuration' command
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "fmt-asc")]
|
||||
FormatAppSpecificConfiguration(FormatAppSpecificConfiguration),
|
||||
@@ -2566,6 +2576,14 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
generated_config.display()
|
||||
);
|
||||
}
|
||||
SubCommand::ConvertAppSpecificConfiguration(arg) => {
|
||||
let file_path = resolve_home_path(arg.path)?;
|
||||
let content = std::fs::read_to_string(&file_path)?;
|
||||
let mut asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
asc.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let v2 = ApplicationSpecificConfiguration::from(asc);
|
||||
println!("{}", serde_json::to_string_pretty(&v2)?);
|
||||
}
|
||||
SubCommand::FormatAppSpecificConfiguration(arg) => {
|
||||
let file_path = resolve_home_path(arg.path)?;
|
||||
let content = std::fs::read_to_string(&file_path)?;
|
||||
@@ -2582,10 +2600,10 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
println!("File successfully formatted for PRs to https://github.com/LGUG2Z/komorebi-application-specific-configuration");
|
||||
}
|
||||
SubCommand::FetchAppSpecificConfiguration => {
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml")?
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.json")?
|
||||
.text()?;
|
||||
|
||||
let output_file = HOME_DIR.join("applications.yaml");
|
||||
let output_file = HOME_DIR.join("applications.json");
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -2602,7 +2620,7 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
);
|
||||
}
|
||||
SubCommand::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
let asc = schema_for!(ApplicationSpecificConfiguration);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
|
||||
143
schema.asc.json
143
schema.asc.json
@@ -1,44 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Array_of_ApplicationConfiguration",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ApplicationConfiguration"
|
||||
"title": "ApplicationSpecificConfiguration",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AscApplicationRulesOrSchema"
|
||||
},
|
||||
"definitions": {
|
||||
"ApplicationConfiguration": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
},
|
||||
"ignore_identifiers": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ApplicationOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApplicationIdentifier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -48,14 +15,100 @@
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"object_name_change",
|
||||
"layered",
|
||||
"tray_and_multi_window",
|
||||
"force",
|
||||
"border_overflow"
|
||||
"AscApplicationRules": {
|
||||
"description": "Rules that determine how an application is handled",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"floating": {
|
||||
"description": "Rules to manage specific windows as floating windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"description": "Rules to ignore specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"layered": {
|
||||
"description": "Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"manage": {
|
||||
"description": "Rules to forcibly manage specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"object_name_change": {
|
||||
"description": "Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"slow_application": {
|
||||
"description": "Rules to identify applications which are slow to send initial event notifications",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"transparency_ignore": {
|
||||
"description": "Rules to ignore specific windows from the transparency feature",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"tray_and_multi_window": {
|
||||
"description": "Rules to identify applications which minimize to the tray or have multiple windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AscApplicationRulesOrSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AscApplicationRules"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"IdWithIdentifier": {
|
||||
|
||||
Reference in New Issue
Block a user