feat(themes): generate base16 palette from wallpaper

This commit adds a new Wallpaper configuration option to the
WorkspaceConfig, allowing the user to specify a path to a wallpaper
image file, and to specify whether to generate a base16 palette from the
colours of that image file.

Theme generation is enabled by default when a wallpaper is selected.

A set of theme options can be given to customize the colours of various
borders and accents.

The themes generated are also plumbed through to the komorebi-bar.

The palette generation algorithm from "flavours" (which has been forked
and updated) is quite slow, so the outputs are cached to file in
DATA_DIR, and keyed by ThemeVariant (Light or Dark).

The Win32 COM API to set the desktop wallpaper is also quite slow,
however this calls is async so it doesn't block komorebi's main thread.
This commit is contained in:
LGUG2Z
2025-04-01 17:43:33 -07:00
parent b4b400b236
commit 4968b0fe37
11 changed files with 1227 additions and 189 deletions

813
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,8 @@ feature-depth = 1
[advisories]
ignore = [
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" }
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
]
[licenses]
@@ -93,4 +94,6 @@ allow-git = [
"https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
]

View File

@@ -13,6 +13,7 @@ serde = { workspace = true }
serde_variant = "0.1"
strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]

View File

@@ -57,7 +57,7 @@ impl From<Colour> for Color32 {
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(HexColor);
pub struct Hex(pub HexColor);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex {

View File

@@ -0,0 +1,77 @@
use crate::colour::Colour;
use crate::colour::Hex;
use crate::Base16ColourPalette;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ThemeVariant {
#[default]
Dark,
Light,
}
impl Display for ThemeVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ThemeVariant::Dark => write!(f, "dark"),
ThemeVariant::Light => write!(f, "light"),
}
}
}
impl From<ThemeVariant> for flavours::operations::generate::Mode {
fn from(value: ThemeVariant) -> Self {
match value {
ThemeVariant::Dark => Self::Dark,
ThemeVariant::Light => Self::Light,
}
}
}
pub fn generate_base16_palette(
image_path: &Path,
variant: ThemeVariant,
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
Base16ColourPalette::try_from(
&flavours::operations::generate::generate(image_path, variant.into(), false)
.unwrap_or_default(),
)
}
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
type Error = hex_color::ParseHexColorError;
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
if fixed.len() != 16 {
return Err(hex_color::ParseHexColorError::Empty);
}
Ok(Self {
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
})
}
}

View File

@@ -2,6 +2,10 @@
#![allow(clippy::missing_errors_doc)]
pub mod colour;
mod generator;
pub use generator::generate_base16_palette;
pub use generator::ThemeVariant;
use schemars::JsonSchema;
use serde::Deserialize;

View File

@@ -124,6 +124,57 @@ pub struct BorderColours {
pub unfocused_locked: Option<Colour>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ThemeOptions {
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_variant: Option<komorebi_themes::ThemeVariant>,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
pub single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
pub monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
pub bar_accent: Option<komorebi_themes::Base16Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Wallpaper {
/// Path to the wallpaper image file
pub path: PathBuf,
/// Generate and apply Base16 theme for this wallpaper (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_theme: Option<bool>,
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_options: Option<ThemeOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WorkspaceConfig {
@@ -171,6 +222,9 @@ pub struct WorkspaceConfig {
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
/// Specify a wallpaper for this workspace
#[serde(skip_serializing_if = "Option::is_none")]
pub wallpaper: Option<Wallpaper>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -247,6 +301,7 @@ impl From<&Workspace> for WorkspaceConfig {
float_override: *value.float_override(),
layout_flip: value.layout_flip(),
floating_layer_behaviour: Option::from(*value.floating_layer_behaviour()),
wallpaper: None,
}
}
}

View File

@@ -348,6 +348,7 @@ impl From<&WindowManager> for State {
floating_layer_behaviour: workspace.floating_layer_behaviour,
globals: workspace.globals,
locked_containers: workspace.locked_containers.clone(),
wallpaper: workspace.wallpaper.clone(),
workspace_config: None,
})
.collect::<VecDeque<_>>();

View File

@@ -1,13 +1,13 @@
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use core::ffi::c_void;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use std::path::Path;
use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
@@ -47,6 +47,8 @@ use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::Com::CoCreateInstance;
use windows::Win32::System::Com::CLSCTX_ALL;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Power::RegisterPowerSettingNotification;
use windows::Win32::System::Power::HPOWERNOTIFY;
@@ -72,6 +74,9 @@ use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
use windows::Win32::UI::Shell::DesktopWallpaper;
use windows::Win32::UI::Shell::IDesktopWallpaper;
use windows::Win32::UI::Shell::DWPOS_FILL;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
@@ -141,6 +146,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows_core::BOOL;
use windows_core::HSTRING;
use crate::core::Rect;
@@ -1345,4 +1351,22 @@ impl WindowsApi {
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
}
pub fn set_wallpaper(path: &Path) -> Result<()> {
let path = path.canonicalize()?;
let wallpaper: IDesktopWallpaper =
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
let wallpaper_path = HSTRING::from(path.to_str().unwrap_or_default());
unsafe {
wallpaper.SetPosition(DWPOS_FILL)?;
}
// Set the wallpaper
unsafe {
wallpaper.SetWallpaper(PCWSTR::null(), PCWSTR::from_raw(wallpaper_path.as_ptr()))?;
}
Ok(())
}
}

View File

@@ -1,20 +1,16 @@
use std::collections::BTreeSet;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fmt::Display;
use std::fmt::Formatter;
use std::io::Write;
use std::num::NonZeroUsize;
use std::sync::atomic::Ordering;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use serde::Deserialize;
use serde::Serialize;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::core::Axis;
use crate::core::CustomLayout;
use crate::core::CycleDirection;
@@ -22,11 +18,6 @@ use crate::core::DefaultLayout;
use crate::core::Layout;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::FloatingLayerBehaviour;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::locked_deque::LockedDeque;
use crate::ring::Ring;
use crate::should_act;
@@ -36,13 +27,28 @@ use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
use crate::windows_api::WindowsApi;
use crate::FloatingLayerBehaviour;
use crate::KomorebiTheme;
use crate::SocketMessage;
use crate::Wallpaper;
use crate::WindowContainerBehaviour;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use komorebi_themes::Base16ColourPalette;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixStream;
#[allow(clippy::struct_field_names)]
#[derive(
@@ -96,6 +102,8 @@ pub struct Workspace {
pub floating_layer_behaviour: FloatingLayerBehaviour,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub locked_containers: BTreeSet<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub wallpaper: Option<Wallpaper>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
pub workspace_config: Option<WorkspaceConfig>,
@@ -148,6 +156,7 @@ impl Default for Workspace {
globals: Default::default(),
workspace_config: None,
locked_containers: Default::default(),
wallpaper: None,
}
}
}
@@ -255,6 +264,7 @@ impl Workspace {
self.set_float_override(config.float_override);
self.set_layout_flip(config.layout_flip);
self.set_floating_layer_behaviour(config.floating_layer_behaviour.unwrap_or_default());
self.set_wallpaper(config.wallpaper.clone());
self.set_workspace_config(Some(config.clone()));
@@ -291,12 +301,129 @@ impl Workspace {
}
}
pub fn apply_wallpaper(&self) -> Result<()> {
if let Some(wallpaper) = &self.wallpaper {
if let Err(error) = WindowsApi::set_wallpaper(&wallpaper.path) {
tracing::error!("failed to set wallpaper: {error}");
}
// if !cfg!(debug_assertions) && wallpaper.generate_theme.unwrap_or(true) {
if wallpaper.generate_theme.unwrap_or(true) {
let variant = wallpaper
.theme_options
.as_ref()
.and_then(|t| t.theme_variant)
.unwrap_or_default();
let cached_palette = DATA_DIR.join(format!(
"{}.base16.{variant}.json",
wallpaper
.path
.file_name()
.unwrap_or(OsStr::new("tmp"))
.to_string_lossy()
));
let mut base16_palette = None;
if cached_palette.is_file() {
tracing::info!(
"colour palette for wallpaper {} found in cache",
cached_palette.display()
);
// this code is VERY slow on debug builds - should only be a one-time issue when loading
// an uncached wallpaper
if let Ok(palette) = serde_json::from_str::<Base16ColourPalette>(
&std::fs::read_to_string(&cached_palette)?,
) {
base16_palette = Some(palette);
}
};
if base16_palette.is_none() {
base16_palette =
komorebi_themes::generate_base16_palette(&wallpaper.path, variant).ok();
std::fs::write(
&cached_palette,
serde_json::to_string_pretty(&base16_palette)?,
)?;
tracing::info!(
"colour palette for wallpaper {} cached",
cached_palette.display()
);
}
if let Some(palette) = base16_palette {
let komorebi_theme = KomorebiTheme::Custom {
colours: Box::new(palette),
single_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.single_border),
stack_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.stack_border),
monocle_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.monocle_border),
floating_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.floating_border),
unfocused_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.unfocused_border),
unfocused_locked_border: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.unfocused_locked_border),
stackbar_focused_text: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.stackbar_focused_text),
stackbar_unfocused_text: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.stackbar_unfocused_text),
stackbar_background: wallpaper
.theme_options
.as_ref()
.and_then(|o| o.stackbar_background),
bar_accent: wallpaper.theme_options.as_ref().and_then(|o| o.bar_accent),
};
let bytes = SocketMessage::Theme(Box::new(komorebi_theme)).as_bytes()?;
let socket = DATA_DIR.join("komorebi.sock");
match UnixStream::connect(socket) {
Ok(mut stream) => {
if let Err(error) = stream.write_all(&bytes) {
tracing::error!("failed to send theme update message: {error}")
}
}
Err(error) => {
tracing::error!("{error}")
}
}
}
}
}
Ok(())
}
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
if let Some(container) = self.monocle_container() {
if let Some(window) = container.focused_window() {
container.restore();
window.focus(mouse_follows_focus)?;
return Ok(());
return self.apply_wallpaper();
}
}
@@ -340,7 +467,7 @@ impl Workspace {
floating_window.focus(mouse_follows_focus)?;
}
Ok(())
self.apply_wallpaper()
}
pub fn update(&mut self) -> Result<()> {

View File

@@ -1384,6 +1384,257 @@
"description": "Name",
"type": "string"
},
"wallpaper": {
"description": "Specify a wallpaper for this workspace",
"type": "object",
"required": [
"path"
],
"properties": {
"generate_theme": {
"description": "Generate and apply Base16 theme for this wallpaper (default: true)",
"type": "boolean"
},
"path": {
"description": "Path to the wallpaper image file",
"type": "string"
},
"theme_options": {
"description": "Specify Light or Dark variant for theme generation (default: Dark)",
"type": "object",
"properties": {
"bar_accent": {
"description": "Komorebi status bar accent (default: Base0D)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"floating_border": {
"description": "Border colour when the window is floating (default: Base09)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"monocle_border": {
"description": "Border colour when the container is in monocle mode (default: Base0F)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"single_border": {
"description": "Border colour when the container contains a single window (default: Base0D)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stack_border": {
"description": "Border colour when the container contains multiple windows (default: Base0B)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_background": {
"description": "Stackbar tab background colour (default: Base01)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_focused_text": {
"description": "Stackbar focused tab text colour (default: Base0B)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_unfocused_text": {
"description": "Stackbar unfocused tab text colour (default: Base05)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"theme_variant": {
"description": "Specify Light or Dark variant for theme generation (default: Dark)",
"type": "string",
"enum": [
"Dark",
"Light"
]
},
"unfocused_border": {
"description": "Border colour when the container is unfocused (default: Base01)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"unfocused_locked_border": {
"description": "Border colour when the container is unfocused and locked (default: Base08)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
}
}
}
}
},
"window_container_behaviour": {
"description": "Determine what happens when a new window is opened (default: Create)",
"oneOf": [