hlctl/src/config.rs

992 lines
36 KiB
Rust

use std::{
borrow::BorrowMut,
collections::HashMap,
env,
fmt::{Debug, Display},
fs::{self},
io,
ops::Deref,
path::Path,
str::FromStr,
string::FromUtf8Error,
};
use log::info;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use thiserror::Error;
use crate::{
environ::{self, ActiveKeybinds},
hlwm::{
self,
attribute::{Attribute, AttributeType},
color::Color,
command::{CommandError, HlwmCommand},
hook::Hook,
key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction},
parser::{ArgParser, FromCommandArgs, FromStrings, ParseError},
rule::{Condition, Consequence, FloatPlacement, Rule, Unrule},
setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings},
theme::ThemeAttr,
window::Window,
Align, Client, Direction, Index, Operator, Separator, ToggleBool,
},
logerr::UnwrapLog,
rule, split, window_types,
};
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("IO error")]
IoError(#[from] io::Error),
#[error("toml deserialization error: {0}")]
TomlDeserializeError(#[from] toml::de::Error),
#[error("toml serialization error: {0}")]
TomlSerializeError(#[from] toml::ser::Error),
#[error("json error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("$HOME must be set")]
HomeNotSet,
#[error("herbstluft client command error: {0}")]
CommandError(#[from] CommandError),
#[error("non-utf8 string error: {0}")]
Utf8StringError(#[from] FromUtf8Error),
#[error("failed parsing value: {0}")]
ParseError(#[from] ParseError),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Config {
pub use_panel: bool,
pub font: String,
pub font_bold: String,
/// Pango font for use where pango fonts are
/// used (i.e. panel)
pub font_pango: String,
/// Bold version of `font_pango`
pub font_pango_bold: String,
pub mod_key: Key,
pub keybinds: Vec<Keybind>,
pub mousebinds: Vec<Mousebind>,
// services won't be spawned through HlwmCommand::Spawn
// so that the running instance can hold the handles
// and a reset would kill them
pub services: Vec<Service>,
pub tags: Vec<Tag>,
pub theme: Theme,
pub rules: Vec<Rule>,
pub attributes: Vec<SetAttribute>,
pub settings: Vec<Setting>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SetAttribute {
pub path: String,
pub value: Attribute,
}
impl Display for SetAttribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"({}){}={}",
self.value.type_string(),
&self.path,
self.value.to_string()
)
}
}
impl FromStrings for SetAttribute {
fn from_strings<I: Iterator<Item = String>>(s: I) -> Result<Self, ParseError> {
let s = s.collect::<Vec<_>>();
let original_str = s.join(" ");
let mut parser = ArgParser::from_strings(s.into_iter());
let x = parser.must_string("set_attribute(type/name)")?;
let (attr_ty, remainder) = split::parens(&x).ok_or(ParseError::InvalidValue {
value: original_str,
expected: "does not have a valid type string (type)",
})?;
if remainder.contains('=') {
let mut parser = ArgParser::from_strings(remainder.split('=').map(|c| c.to_string()));
return Ok(Self {
path: parser.must_string("set_attribute(path)")?,
value: Attribute::new(
AttributeType::from_str(&attr_ty)?,
&parser.must_string("set_attribute(value)")?,
)?,
});
}
if parser.must_string("set_attribute(equals)")? != "=" {
return Err(ParseError::ValueMissing);
}
Ok(Self {
path: remainder,
value: Attribute::new(
AttributeType::from_str(&attr_ty)?,
&parser.must_string("set_attribute(value)")?,
)?,
})
}
}
impl Serialize for SetAttribute {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SetAttribute {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ExpectedKey;
impl serde::de::Expected for ExpectedKey {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("Expected a supported key")
}
}
let str_val: String = Deserialize::deserialize(deserializer)?;
Ok(
Self::from_strings(split::tab_or_space(&str_val).into_iter()).map_err(|_| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey)
})?,
)
}
}
impl Config {
const USE_PANEL: &'static str = "settings.my_hlctl_panel_enabled";
const FONT: &'static str = "theme.my_font";
const FONT_BOLD: &'static str = "theme.my_font_bold";
const FONT_PANGO: &'static str = "theme.my_font_pango";
const FONT_PANGO_BOLD: &'static str = "theme.my_font_pango_bold";
const SERVICES: &'static str = "settings.my_services";
const MOD_KEY: &'static str = "settings.my_mod_key";
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
info!(
"reading config from: {}",
path.to_str().unwrap_or_log("<not a valid utf8 string>")
);
Self::deserialize(&fs::read_to_string(path)?)
}
pub fn write_to_file(&self, path: &Path) -> Result<(), ConfigError> {
info!(
"saving config to: {}",
path.to_str().unwrap_or_log("<not a valid utf8 string>")
);
Ok(fs::write(path, self.serialize()?)?)
}
pub fn serialize(&self) -> Result<String, ConfigError> {
Ok(toml::ser::to_string_pretty(self)?)
}
pub fn deserialize(str: &str) -> Result<Self, ConfigError> {
Ok(toml::from_str(&str)?)
}
pub fn default_path() -> Result<String, ConfigError> {
Ok(format!(
"{home}/.config/herbstluftwm/hlctl.toml",
home = env::var("XDG_CONFIG_HOME")
.unwrap_or(env::var("HOME").map_err(|_| ConfigError::HomeNotSet)?)
))
}
fn new_or_set_attr(path: String, attr: Attribute) -> HlwmCommand {
HlwmCommand::Or {
separator: Separator::Comma,
commands: vec![
HlwmCommand::NewAttr {
path: path.clone(),
attr: attr.clone().into(),
},
HlwmCommand::SetAttr {
path,
new_value: attr,
},
],
}
}
fn attrs_set(&self) -> Result<Vec<HlwmCommand>, ConfigError> {
info!("loading attr settings command set");
Ok([
(Self::USE_PANEL, Some(self.use_panel.to_string())),
(Self::FONT, Some(self.font.clone())),
(Self::FONT_BOLD, Some(self.font_bold.clone())),
(Self::FONT_PANGO, Some(self.font_pango.clone())),
(Self::FONT_PANGO_BOLD, Some(self.font_pango_bold.clone())),
(
Self::SERVICES,
if self.services.len() == 0 {
None
} else {
Some(serde_json::ser::to_string(&self.services)?)
},
),
(Self::MOD_KEY, Some(self.mod_key.to_string())),
]
.into_iter()
.filter(|(_, attr)| attr.is_some())
.map(|(name, attr)| {
Self::new_or_set_attr(name.to_string(), Attribute::String(attr.unwrap()))
})
.collect())
}
fn theme_command_set(&self) -> Vec<HlwmCommand> {
info!("loading theme attr command set");
[
HlwmCommand::SetAttr {
path: "theme.reset".into(),
new_value: Attribute::String("1".into()),
},
HlwmCommand::SetAttr {
path: "theme.tiling.reset".into(),
new_value: Attribute::String("1".into()),
},
HlwmCommand::SetAttr {
path: "theme.floating.reset".into(),
new_value: Attribute::String("1".into()),
},
]
.into_iter()
.chain(
(&self.theme.attributes)
.into_iter()
.map(|attr| HlwmCommand::SetAttr {
path: attr.attr_path(),
new_value: Attribute::from(attr.clone()).into(),
}),
)
.collect()
}
fn settings_command_set(&self) -> Vec<HlwmCommand> {
info!("loading settings command set");
self.settings
.clone()
.into_iter()
.map(|s| HlwmCommand::Set(s))
.collect()
}
fn service_command_set(&self) -> Vec<HlwmCommand> {
info!("loading service command set");
self.services
.clone()
.into_iter()
.map(|s| {
[
HlwmCommand::Spawn {
executable: "pkill".into(),
args: vec!["-9".into(), s.name.clone()],
}
.to_try()
.silent(),
HlwmCommand::Spawn {
executable: s.name,
args: s.arguments,
},
]
})
.flatten()
.collect()
}
fn rule_command_set(&self) -> Vec<HlwmCommand> {
[HlwmCommand::Unrule(Unrule::All)]
.into_iter()
.chain(self.rules.clone().into_iter().map(|r| HlwmCommand::Rule(r)))
.collect()
}
pub fn to_command_set(&self) -> Result<Vec<HlwmCommand>, ConfigError> {
Ok([
HlwmCommand::EmitHook(Hook::Reload),
HlwmCommand::Keyunbind(KeyUnbind::All),
HlwmCommand::Mouseunbind,
]
.into_iter()
.chain(
self.keybinds
.clone()
.into_iter()
.map(|key| HlwmCommand::Keybind(key)),
)
.chain(
self.mousebinds
.clone()
.into_iter()
.map(|mb| HlwmCommand::Mousebind(mb)),
)
.chain(self.theme_command_set())
.chain(self.attrs_set()?)
.chain(self.tag_command_set())
.chain(self.settings_command_set())
.chain(self.rule_command_set())
.chain(self.service_command_set())
.chain([HlwmCommand::Unlock])
.collect())
}
fn tag_command_set(&self) -> Vec<HlwmCommand> {
info!("loading tag command set");
(&self.tags)
.into_iter()
.map(|tag| tag.to_command_set(self.mod_key))
.flatten()
.chain([HlwmCommand::And {
separator: Separator::Comma,
commands: vec![
HlwmCommand::Substitute {
identifier: "CURRENT_INDEX".into(),
attribute_path: "tags.focus.index".into(),
command: Box::new(HlwmCommand::Compare {
attribute: "tags.by-name.default.index".into(),
operator: Operator::Equal,
value: "CURRENT_INDEX".into(),
}),
},
HlwmCommand::UseIndex {
index: Index::Relative(1),
skip_visible: false,
},
HlwmCommand::MergeTag {
tag: "default".to_string(),
target: Some(hlwm::TagSelect::Name(
self.tags.first().cloned().unwrap().name().to_string(),
)),
},
],
}
.to_try()])
.collect()
}
/// Create a config gathered from the herbstluftwm configs,
/// using default mouse binds/attributes set
pub fn from_herbstluft() -> Self {
fn setting<T, E: std::error::Error, F: FnOnce(&str) -> Result<T, E>>(
name: &str,
f: F,
default: T,
) -> T
where
T: Debug,
{
match Client::new().get_attr(name.to_string()) {
Ok(setting) => f(&setting.to_string()).unwrap_or_log(default),
Err(_) => default,
}
}
let client = Client::new();
let default = Config::default();
let mod_key = setting(Self::MOD_KEY, Key::from_str, default.mod_key);
Config {
use_panel: client
.get_from_str_attr(Self::USE_PANEL.to_string())
.unwrap_or_log(default.use_panel),
font: client
.get_str_attr(Self::FONT.to_string())
.unwrap_or_log(default.font),
font_bold: client
.get_str_attr(Self::FONT_BOLD.to_string())
.unwrap_or_log(default.font_bold),
font_pango: client
.get_str_attr(Self::FONT_PANGO.to_string())
.unwrap_or_log(default.font_pango),
font_pango_bold: client
.get_str_attr(Self::FONT_PANGO_BOLD.to_string())
.unwrap_or_log(default.font_pango_bold),
mod_key,
services: setting(
Self::SERVICES,
|v| serde_json::de::from_str(v),
default.services,
),
theme: Theme {
attributes: (|| -> Vec<_> {
ThemeAttr::iter()
.map(|attr| {
let attr_path = attr.attr_path();
let default = (&default.theme.attributes)
.into_iter()
.find(|f| (*f).eq(&attr))
.unwrap();
ThemeAttr::from_command_args(
&attr_path,
[client
.get_attr(attr_path.clone())
.map(|t| t.to_string())
.unwrap_or_log(default.to_string())]
.into_iter(),
)
.unwrap_or_log(default.clone())
})
.collect()
})(),
},
keybinds: environ::active_keybinds(ActiveKeybinds::OmitNamedTagBinds)
.unwrap_or_log(default.keybinds),
tags: Self::active_tags(mod_key).unwrap_or_log(default.tags),
rules: (|| -> Result<Vec<Rule>, ConfigError> {
Ok(HlwmCommand::ListRules
.execute_str()?
.split('\n')
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.map(|line| Rule::from_strings(split::tab_or_space(line).into_iter()))
.collect::<Result<_, _>>()?)
})()
.unwrap_or_log(default.rules),
settings: (|| -> Result<Vec<_>, CommandError> {
default
.settings
.clone()
.into_iter()
.map(|s| Ok(client.get_setting(s.into())?))
.collect::<Result<Vec<_>, CommandError>>()
})()
.unwrap_or_log(default.settings),
..default
}
}
fn active_tags(mod_key: Key) -> Result<Vec<Tag>, ConfigError> {
let mut by_tag: HashMap<String, Vec<Keybind>> = HashMap::new();
environ::active_keybinds(ActiveKeybinds::OnlyNamedTagBinds)?
.into_iter()
.filter(|bind| match bind.command.deref() {
HlwmCommand::UseTag(_) | HlwmCommand::MoveTag(_) => true,
_ => false,
})
.for_each(|bind| match bind.command.deref() {
HlwmCommand::UseTag(tag) | HlwmCommand::MoveTag(tag) => match by_tag.get_mut(tag) {
Some(coll) => coll.push(bind.clone()),
None => {
by_tag.insert(tag.to_string(), vec![bind]);
}
},
_ => unreachable!(),
});
Ok(by_tag
.into_iter()
.map(|(name, binds)| {
let standard = binds.len() != 0
&& (&binds).into_iter().all(|bind| match bind.command.deref() {
HlwmCommand::UseTag(_) => {
bind.keys.len() == 2
&& bind.keys[0] == mod_key
&& bind.keys[1].is_standard()
}
HlwmCommand::MoveTag(_) => {
bind.keys.len() == 3
&& bind.keys[0] == mod_key
&& bind.keys[1] == Key::Shift
&& bind.keys[2].is_standard()
}
// Filtered out above
_ => unreachable!(),
});
if standard {
let key = *binds.first().unwrap().keys.last().unwrap();
let key_same = (&binds)
.into_iter()
.all(|bind| bind.keys.last().unwrap().eq(&key));
if key_same {
// Actually standard
return Tag::standard(name, key);
}
}
let mut use_keys = None;
let mut move_keys = None;
for bind in binds {
match bind.command.deref() {
HlwmCommand::UseTag(_) => {
if use_keys.is_none() {
use_keys = Some(bind.keys);
}
}
HlwmCommand::MoveTag(_) => {
if move_keys.is_none() {
move_keys = Some(bind.keys);
}
}
// Filtered out above
_ => unreachable!(),
}
}
Tag::other(name, use_keys, move_keys)
})
.collect())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Theme {
pub attributes: Vec<ThemeAttr>,
}
#[allow(unused)]
impl Theme {
pub fn active_color(&self) -> Option<Color> {
for attr in &self.attributes {
if let ThemeAttr::ActiveColor(col) = attr {
return Some(col.clone());
}
}
None
}
pub fn normal_color(&self) -> Option<Color> {
for attr in &self.attributes {
if let ThemeAttr::NormalColor(col) = attr {
return Some(col.clone());
}
}
None
}
pub fn urgent_color(&self) -> Option<Color> {
for attr in &self.attributes {
if let ThemeAttr::UrgentColor(col) = attr {
return Some(col.clone());
}
}
None
}
pub fn text_color(&self) -> Option<Color> {
for attr in &self.attributes {
if let ThemeAttr::TitleColor(col) = attr {
return Some(col.clone());
}
}
None
}
}
impl Default for Config {
fn default() -> Self {
let resize_step = 0.1;
let mod_key = Key::Mod4Super;
let font_bold = String::from("-*-fixed-*-*-*-*-13-*-*-*-*-*-*-*");
let active_color = Color::from_hex("#800080").expect("default active color");
let normal_color = Color::from_hex("#330033").expect("default normal color");
let urgent_color = Color::from_hex("#7811A1").expect("default urgent color");
let text_color = Color::from_hex("#898989").expect("default text color");
Self {
use_panel: true,
mod_key,
font: String::from("-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*"),
font_bold: font_bold.clone(),
keybinds: vec![
Keybind::new(
[mod_key, Key::Shift, Key::Char('r')].into_iter(),
HlwmCommand::Reload,
),
Keybind::new(
[mod_key, Key::Shift, Key::Char('c')].into_iter(),
HlwmCommand::Close { window: None },
),
Keybind::new(
[mod_key, Key::Char('s')].into_iter(),
HlwmCommand::Spawn {
executable: String::from("flameshot"),
args: vec![String::from("gui")],
},
),
Keybind::new(
[mod_key, Key::Return].into_iter(),
HlwmCommand::Spawn {
executable: String::from("dmenu_run"),
args: vec![],
},
),
Keybind::new(
[mod_key, Key::Left].into_iter(),
HlwmCommand::Focus(Direction::Left),
),
Keybind::new(
[mod_key, Key::Right].into_iter(),
HlwmCommand::Focus(Direction::Right),
),
Keybind::new(
[mod_key, Key::Up].into_iter(),
HlwmCommand::Focus(Direction::Up),
),
Keybind::new(
[mod_key, Key::Down].into_iter(),
HlwmCommand::Focus(Direction::Down),
),
Keybind::new(
[mod_key, Key::Shift, Key::Left].into_iter(),
HlwmCommand::Shift(Direction::Left),
),
Keybind::new(
[mod_key, Key::Shift, Key::Right].into_iter(),
HlwmCommand::Shift(Direction::Right),
),
Keybind::new(
[mod_key, Key::Shift, Key::Up].into_iter(),
HlwmCommand::Shift(Direction::Up),
),
Keybind::new(
[mod_key, Key::Shift, Key::Down].into_iter(),
HlwmCommand::Shift(Direction::Down),
),
Keybind::new(
[mod_key, Key::Char('u')].into_iter(),
HlwmCommand::Split(Align::Bottom(Some(0.5))),
),
Keybind::new(
[mod_key, Key::Char('o')].into_iter(),
HlwmCommand::Split(Align::Right(Some(0.5))),
),
Keybind::new([mod_key, Key::Char('r')].into_iter(), HlwmCommand::Remove),
Keybind::new(
[mod_key, Key::Char('f')].into_iter(),
HlwmCommand::Fullscreen(ToggleBool::Toggle),
),
Keybind::new(
[mod_key, Key::Space].into_iter(),
HlwmCommand::Or {
separator: Separator::Comma,
commands: vec![
HlwmCommand::And {
separator: Separator::Period,
commands: vec![
HlwmCommand::Compare {
attribute: String::from("tags.focus.curframe_wcount"),
operator: Operator::Equal,
value: String::from("2"),
},
HlwmCommand::CycleLayout {
delta: Some(Index::Relative(1)),
layouts: vec![
FrameLayout::Vertical,
FrameLayout::Horizontal,
FrameLayout::Max,
FrameLayout::Vertical,
FrameLayout::Grid,
],
},
],
},
HlwmCommand::CycleLayout {
delta: Some(Index::Relative(1)),
layouts: vec![],
},
],
},
),
Keybind::new(
[mod_key, Key::Control, Key::Left].into_iter(),
HlwmCommand::Resize {
direction: Direction::Left,
fraction_delta: Some(Index::Relative(resize_step)),
},
),
Keybind::new(
[mod_key, Key::Control, Key::Down].into_iter(),
HlwmCommand::Resize {
direction: Direction::Down,
fraction_delta: Some(Index::Relative(resize_step)),
},
),
Keybind::new(
[mod_key, Key::Control, Key::Up].into_iter(),
HlwmCommand::Resize {
direction: Direction::Up,
fraction_delta: Some(Index::Relative(resize_step)),
},
),
Keybind::new(
[mod_key, Key::Control, Key::Right].into_iter(),
HlwmCommand::Resize {
direction: Direction::Right,
fraction_delta: Some(Index::Relative(resize_step)),
},
),
Keybind::new([mod_key, Key::Tab].into_iter(), HlwmCommand::Cycle),
Keybind::new(
[mod_key, Key::Char('i')].into_iter(),
HlwmCommand::JumpTo(Window::Urgent),
),
],
services: vec![Service {
name: String::from("fcitx5"),
arguments: vec![],
}],
tags: [
Tag::standard(".1", Key::Char('1')),
Tag::standard(".2", Key::Char('2')),
Tag::standard(".3", Key::Char('3')),
Tag::standard(".4", Key::Char('4')),
Tag::standard(".5", Key::Char('5')),
Tag::standard("F1", Key::F1),
Tag::standard("F2", Key::F2),
Tag::standard("F3", Key::F3),
Tag::standard("F4", Key::F4),
Tag::standard("F5", Key::F5),
]
.into(),
mousebinds: vec![
Mousebind::new(
mod_key,
[Key::Mouse(MouseButton::Button1)].into_iter(),
MousebindAction::Move,
),
Mousebind::new(
mod_key,
[Key::Mouse(MouseButton::Button2)].into_iter(),
MousebindAction::Zoom,
),
Mousebind::new(
mod_key,
[Key::Mouse(MouseButton::Button3)].into_iter(),
MousebindAction::Resize,
),
],
theme: Theme {
attributes: (|| -> Vec<ThemeAttr> {
ThemeAttr::iter()
.map(|attr| {
let mut attr = attr;
match attr.borrow_mut() {
ThemeAttr::TitleWhen(when) => *when = "multiple_tabs".into(),
ThemeAttr::TitleFont(font) => *font = font_bold.clone(),
ThemeAttr::TitleDepth(depth) => *depth = 3,
ThemeAttr::InnerWidth(inw) => *inw = 0,
ThemeAttr::TitleHeight(h) => *h = 15,
ThemeAttr::BorderWidth(w) => *w = 1,
ThemeAttr::TilingOuterWidth(w) => *w = 1,
ThemeAttr::FloatingOuterWidth(w) => *w = 1,
ThemeAttr::FloatingBorderWidth(w) => *w = 4,
ThemeAttr::InnerColor(col) => *col = Color::BLACK,
ThemeAttr::TitleColor(col) => *col = text_color.clone(),
ThemeAttr::ActiveColor(col) => *col = active_color.clone(),
ThemeAttr::NormalColor(col) => *col = normal_color.clone(),
ThemeAttr::UrgentColor(col) => *col = urgent_color.clone(),
ThemeAttr::BackgroundColor(col) => *col = Color::BLACK,
ThemeAttr::ActiveInnerColor(col) => *col = active_color.clone(),
ThemeAttr::ActiveOuterColor(col) => *col = active_color.clone(),
ThemeAttr::NormalInnerColor(col) => *col = normal_color.clone(),
ThemeAttr::NormalOuterColor(col) => *col = normal_color.clone(),
ThemeAttr::NormalTitleColor(col) => *col = normal_color.clone(),
ThemeAttr::UrgentInnerColor(col) => *col = urgent_color.clone(),
ThemeAttr::UrgentOuterColor(col) => *col = urgent_color.clone(),
}
attr
})
.collect()
})(),
},
rules: vec![
rule!(
window_types!(Dialog, Utility, Splash),
Consequence::Floating(true)
),
rule!(Consequence::Focus(true)),
rule!(Consequence::FloatPlacement(FloatPlacement::Smart)),
rule!(window_types!(Dialog), Consequence::Floating(true)),
rule!(
window_types!(Notification, Dock, Desktop),
Consequence::Manage(false)
),
rule!(Condition::FixedSize, Consequence::Floating(true)),
],
attributes: vec![],
settings: vec![
Setting::FrameBorderWidth(2),
Setting::ShowFrameDecorations(ShowFrameDecoration::FocusedIfMultiple),
Setting::TreeStyle(String::from("╾│ ├└╼─┐")),
Setting::FrameBgTransparent(true.into()),
Setting::SmartWindowSurroundings(false.into()),
Setting::SmartFrameSurroundings(SmartFrameSurroundings::HideAll),
Setting::FrameTransparentWidth(5),
Setting::FrameGap(3),
Setting::WindowGap(0),
Setting::FramePadding(0),
Setting::MouseRecenterGap(0),
Setting::FrameBorderActiveColor(active_color.clone()),
Setting::FrameBgActiveColor(active_color.clone()),
],
font_pango: String::from("Monospace 9"),
font_pango_bold: String::from("Monospace Bold 9"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Service {
pub name: String,
pub arguments: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Tag {
Standard {
name: String,
key: Key,
},
Other {
name: String,
use_keys: Option<Vec<Key>>,
move_keys: Option<Vec<Key>>,
},
}
impl Tag {
pub fn standard<S: Into<String>>(name: S, key: Key) -> Self {
Self::Standard {
key,
name: name.into(),
}
}
pub fn other<S: Into<String>, K: IntoIterator<Item = Key>>(
name: S,
use_keys: Option<K>,
move_keys: Option<K>,
) -> Self {
Self::Other {
name: name.into(),
use_keys: use_keys.map(|k| k.into_iter().collect()),
move_keys: move_keys.map(|k| k.into_iter().collect()),
}
}
pub fn name(&self) -> &str {
match self {
Tag::Standard { name, key: _ } => name.as_str(),
Tag::Other {
name,
use_keys: _,
move_keys: _,
} => name.as_str(),
}
}
fn use_keybind(&self, mod_key: Key) -> Option<Keybind> {
match self {
Tag::Standard { name, key } => Some(Keybind {
keys: vec![mod_key, *key],
command: Box::new(HlwmCommand::UseTag(name.clone())),
}),
Tag::Other {
name,
use_keys,
move_keys: _,
} => use_keys.clone().map(|use_keys| Keybind {
keys: use_keys,
command: Box::new(HlwmCommand::UseTag(name.clone())),
}),
}
}
fn move_keybind(&self, mod_key: Key) -> Option<Keybind> {
match self {
Tag::Standard { name, key } => Some(Keybind {
keys: vec![mod_key, Key::Shift, *key],
command: Box::new(HlwmCommand::MoveTag(name.clone())),
}),
Tag::Other {
name,
use_keys: _,
move_keys,
} => move_keys.clone().map(|move_keys| Keybind {
keys: move_keys,
command: Box::new(HlwmCommand::MoveTag(name.clone())),
}),
}
}
pub fn to_command_set(&self, mod_key: Key) -> Vec<HlwmCommand> {
let mut commands = Vec::with_capacity(3);
commands.push(HlwmCommand::AddTag(self.name().to_string()));
if let Some(keybind) = self.use_keybind(mod_key) {
commands.push(HlwmCommand::Keybind(keybind));
}
if let Some(keybind) = self.move_keybind(mod_key) {
commands.push(HlwmCommand::Keybind(keybind));
}
commands
}
}
impl<S> From<(S, Key)> for Tag
where
S: Into<String>,
{
fn from((name, key): (S, Key)) -> Self {
Self::Standard {
name: name.into(),
key,
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
use crate::hlwm::{
attribute::Attribute,
color::{Color, X11Color},
};
use super::{Config, SetAttribute};
#[test]
fn config_serialize_deserialize() {
let mut cfg = Config::default();
// Add extra attrs for testing
cfg.attributes.push(SetAttribute {
path: "my.attr.path".into(),
value: Attribute::Color(Color::X11(X11Color::NavajoWhite3)),
});
let cfg_serialized = cfg.serialize().unwrap();
println!("--> Config <--");
println!("{cfg_serialized}");
println!("--> Config <--");
let parsed_cfg = Config::deserialize(&cfg_serialized).unwrap();
assert_eq!(cfg, parsed_cfg,)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AttrWrapper {
attr: SetAttribute,
}
#[test]
fn set_attribute_serialize_deserialize() {
let attr = AttrWrapper {
attr: SetAttribute {
path: "MyCool.Path".into(),
value: Attribute::Color(Color::X11(X11Color::LemonChiffon2)),
},
};
let serialized = toml::to_string_pretty(&attr).expect("serialize");
let parsed: AttrWrapper = toml::from_str(&serialized).expect("unserailize");
assert_eq!(attr.attr, parsed.attr,)
}
}