hlctl/src/config.rs

1046 lines
36 KiB
Rust

use std::{
borrow::BorrowMut,
cmp::Ordering,
convert::Infallible,
env,
fmt::Display,
fs::{self},
io,
ops::Deref,
path::Path,
str::FromStr,
string::FromUtf8Error,
};
use log::info;
use serde::{
de::{Expected, Unexpected},
Deserialize, Serialize,
};
use strum::IntoEnumIterator;
use thiserror::Error;
use crate::{
hlwm::{
attribute::Attribute,
color::Color,
command::{CommandError, HlwmCommand},
hook::Hook,
key::{Key, KeyParseError, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction},
rule::{Condition, Consequence, FloatPlacement, Rule},
setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings},
theme::ThemeAttr,
window::Window,
Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool,
},
rule, 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 keybind: {0}")]
KeyParseError(#[from] KeyParseError),
#[error("failed parsing value from string: {0}")]
StringParseError(#[from] StringParseError),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Config {
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 FromStr for SetAttribute {
type Err = StringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(')');
let type_string = parts.next().map(|p| p.strip_prefix('(')).flatten().ok_or(
StringParseError::RequiredArgMissing("attribute type".into()),
)?;
let mut parts = parts
.next()
.ok_or(StringParseError::RequiredArgMissing(
"attribute path/value".into(),
))?
.split('=')
.map(|v| v.trim());
let path = parts
.next()
.ok_or(StringParseError::RequiredArgMissing(
"attribute path".into(),
))?
.to_string();
let value_string = parts.collect::<Vec<_>>().join("=");
if value_string.is_empty() {
return Err(StringParseError::RequiredArgMissing(
"attribute value".into(),
));
}
let value = Attribute::new(type_string, &value_string)?;
Ok(Self { path, 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_str(&str_val).map_err(|_| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey)
})?)
}
}
impl Config {
const FONT: &'static str = "theme.my_font";
const FONT_BOLD: &'static str = "theme.my_font_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("<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("<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::FONT, Some(self.font.clone())),
(Self::MOD_KEY, Some(self.mod_key.to_string())),
(Self::FONT_BOLD, Some(self.font_bold.clone())),
(
Self::SERVICES,
if self.services.len() == 0 {
None
} else {
Some(serde_json::ser::to_string(&self.services)?)
},
),
]
.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,
}
.to_try(),
]
})
.flatten()
.collect()
}
fn rule_command_set(&self) -> Vec<HlwmCommand> {
info!("loading rule command set");
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.attrs_set()?)
.chain(self.theme_command_set())
.chain(self.tag_command_set())
.chain(self.settings_command_set())
.chain(self.rule_command_set())
.chain(self.service_command_set())
.collect())
}
fn tag_command_set(&self) -> Vec<HlwmCommand> {
info!("loading tag command set");
(&self.tags)
.into_iter()
.map(|tag| (tag, tag.key()))
.filter(|(_, key)| key.is_some())
.map(|(tag, key)| {
let tag_name = tag.to_string();
let key = key.unwrap();
[
HlwmCommand::AddTag(tag_name.clone()),
HlwmCommand::Keybind(Keybind::new(
[self.mod_key, key],
HlwmCommand::UseTag(tag_name.clone()),
)),
HlwmCommand::Keybind(Keybind::new(
[self.mod_key, Key::Shift, key],
HlwmCommand::MoveTag(tag_name),
)),
]
})
.flatten()
.chain([
HlwmCommand::And {
separator: Separator::Comma,
commands: vec![
HlwmCommand::Compare {
attribute: "tags.by-name.default.index".into(),
operator: Operator::Equal,
value: "tags.focus.index".into(),
},
HlwmCommand::UseIndex {
index: Index::Relative(1),
skip_visible: false,
},
],
}
.to_try(),
HlwmCommand::MergeTag {
tag: "default".to_string(),
target: None,
}
.to_try(),
])
.collect()
}
/// Create a config gathered from the herbstluftwm configs,
/// using default mouse binds/tags
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 {
match Client::new().get_attr(name.to_string()) {
Ok(setting) => f(&setting.to_string()).unwrap_or(default),
Err(_) => default,
}
}
let client = Client::new();
let default = Config::default();
Config {
font: setting(
Self::FONT,
|f| -> Result<_, Infallible> { Ok(f.to_string()) },
default.font,
),
mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key),
font_bold: client
.get_attr(ThemeAttr::TitleFont(String::new()).attr_path())
.map(|a| a.to_string())
.unwrap_or(default.font_bold),
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_raw_parts(
&attr_path,
&client
.get_attr(attr_path.clone())
.map(|t| t.to_string())
.unwrap_or(default.to_string()),
)
.unwrap_or(default.clone())
})
.collect()
})(),
},
keybinds: Self::active_keybinds(true).unwrap_or(default.keybinds),
tags: (|| -> Result<Vec<Tag>, _> {
Result::<_, ConfigError>::Ok({
let mut tags = client
.tag_status()?
.into_iter()
.map(|tag| {
let tag_result: Result<_, Infallible> = tag.name().parse();
tag_result.unwrap()
})
.collect::<Vec<_>>();
tags.sort_by(|lhs: &Tag, rhs| match lhs.partial_cmp(rhs) {
Some(ord) => ord,
None => Ordering::Less,
});
tags
})
})()
.unwrap_or(default.tags),
rules: (|| -> Result<Vec<Rule>, ConfigError> {
Ok(
String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)?
.split('\n')
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.map(|line| Rule::from_str(line))
.collect::<Result<_, _>>()?,
)
})()
.unwrap_or(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(default.settings),
..default
}
}
fn active_keybinds(omit_tag_binds: bool) -> Result<Vec<Keybind>, ConfigError> {
String::from_utf8(Client::new().execute(HlwmCommand::ListKeybinds)?.stdout)?
.split("\n")
.filter(|i| {
!omit_tag_binds
|| match i.split("\t").skip(1).next() {
Some(command) => {
command
!= HlwmCommand::UseIndex {
index: Index::Absolute(0),
skip_visible: false,
}
.to_string()
&& command
!= HlwmCommand::MoveIndex {
index: Index::Absolute(0),
skip_visible: false,
}
.to_string()
&& command != HlwmCommand::UseTag(String::new()).to_string()
&& command != HlwmCommand::MoveTag(String::new()).to_string()
}
None => false,
}
})
.map(|row: &str| Keybind::from_str(row).map_err(|err| err.into()))
.collect::<Result<Vec<_>, ConfigError>>()
}
}
#[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
}
}
#[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum InclusiveError {
#[error("out of range")]
OutOfRange,
}
impl Expected for InclusiveError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
InclusiveError::OutOfRange => write!(f, "out of range"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Inclusive<const MAX: u8>(u8);
impl Inclusive<10> {
fn char(&self) -> char {
match self.0 {
1 => '1',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
6 => '6',
7 => '7',
8 => '8',
9 => '9',
0 => '0',
_ => unreachable!(),
}
}
fn f_key(&self) -> Option<Key> {
match self.0 {
1 => Some(Key::F1),
2 => Some(Key::F2),
3 => Some(Key::F3),
4 => Some(Key::F4),
5 => Some(Key::F5),
6 => Some(Key::F6),
7 => Some(Key::F7),
8 => Some(Key::F8),
9 => Some(Key::F9),
_ => None,
}
}
}
impl<const MAX: u8> Display for Inclusive<MAX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<const MAX: u8> Deref for Inclusive<MAX> {
type Target = u8;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<const MAX: u8> TryFrom<u8> for Inclusive<MAX> {
type Error = InclusiveError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value > MAX {
return Err(InclusiveError::OutOfRange);
}
Ok(Inclusive(value))
}
}
impl<const MAX: u8> From<Inclusive<MAX>> for u8 {
fn from(value: Inclusive<MAX>) -> Self {
value.0
}
}
impl<const MAX: u8> Serialize for Inclusive<MAX> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
u8::serialize(&self.0, serializer)
}
}
impl<'de, const MAX: u8> Deserialize<'de> for Inclusive<MAX> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let val = u8::deserialize(deserializer)?;
if val > MAX {
return Err(serde::de::Error::invalid_value(
Unexpected::Unsigned(val as u64),
&InclusiveError::OutOfRange,
));
}
Ok(Inclusive(val))
}
}
impl<const MAX: u8> Inclusive<MAX> {}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Tag {
FrontRow(Inclusive<10>),
FunctionRow(Inclusive<10>),
Other(String),
}
impl Tag {
pub fn key(&self) -> Option<Key> {
match self {
Tag::FrontRow(idx) => Some(Key::Char(idx.char())),
Tag::FunctionRow(idx) => Some(match idx.f_key() {
Some(f) => f,
None => return None,
}),
Tag::Other(_) => None,
}
}
}
impl PartialEq for Tag {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::FrontRow(l0), Self::FrontRow(r0)) => l0 == r0,
(Self::FunctionRow(l0), Self::FunctionRow(r0)) => l0 == r0,
(Self::Other(l0), Self::Other(r0)) => l0 == r0,
_ => false,
}
}
}
impl PartialOrd for Tag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Tag::FrontRow(lhs), Tag::FrontRow(rhs)) => Some(lhs.0.cmp(&rhs.0)),
(Tag::FrontRow(_), Tag::FunctionRow(_)) => Some(Ordering::Greater),
(Tag::FrontRow(_), Tag::Other(_)) => Some(Ordering::Greater),
(Tag::FunctionRow(_), Tag::FrontRow(_)) => Some(Ordering::Less),
(Tag::FunctionRow(lhs), Tag::FunctionRow(rhs)) => Some(lhs.0.cmp(&rhs.0)),
(Tag::FunctionRow(_), Tag::Other(_)) => Some(Ordering::Greater),
(Tag::Other(_), Tag::FrontRow(_)) | (Tag::Other(_), Tag::FunctionRow(_)) => {
Some(Ordering::Less)
}
(Tag::Other(_), Tag::Other(_)) => None,
}
}
}
impl FromStr for Tag {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let indicator = match chars.next() {
Some(i) => i,
None => return Ok(Self::Other(s.to_string())),
};
let number = u8::from_str(&chars.next().unwrap_or_default().to_string()).ok();
if number.is_none() {
return Ok(Self::Other(s.to_string()));
}
let number = match number.unwrap().try_into() {
Ok(number) => number,
Err(_) => return Ok(Self::Other(s.to_string())),
};
match indicator {
'.' => Ok(Self::FrontRow(number)),
'F' => Ok(Self::FunctionRow(number)),
_ => unreachable!(),
}
}
}
impl Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Tag::FrontRow(tag) => write!(f, ".{tag}"),
Tag::FunctionRow(tag) => write!(f, "F{tag}"),
Tag::Other(tag) => f.write_str(tag),
}
}
}
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 {
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: (1..=5)
.into_iter()
.map(|idx| Tag::FrontRow(idx.try_into().unwrap()))
.chain(vec![
Tag::FunctionRow(1.try_into().unwrap()),
Tag::FunctionRow(2.try_into().unwrap()),
Tag::FunctionRow(3.try_into().unwrap()),
Tag::FunctionRow(4.try_into().unwrap()),
Tag::FunctionRow(5.try_into().unwrap()),
])
.collect(),
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>,
}
#[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();
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,)
}
}