987 lines
35 KiB
Rust
987 lines
35 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, EnvironError},
|
|
hlwm::{
|
|
self,
|
|
attribute::Attribute,
|
|
color::Color,
|
|
command::{CommandError, HlwmCommand},
|
|
hook::Hook,
|
|
key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction},
|
|
rule::{Condition, Consequence, FloatPlacement, Rule, Unrule},
|
|
setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings},
|
|
theme::ThemeAttr,
|
|
window::Window,
|
|
Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool,
|
|
},
|
|
logerr::UnwrapLog,
|
|
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 value from string: {0}")]
|
|
StringParseError(#[from] StringParseError),
|
|
#[error("getting from environment error: [{0}]")]
|
|
EnvironError(#[from] EnvironError),
|
|
}
|
|
|
|
#[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 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::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,
|
|
}
|
|
.to_try(),
|
|
]
|
|
})
|
|
.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 {
|
|
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_raw_parts(
|
|
&attr_path,
|
|
&client
|
|
.get_attr(attr_path.clone())
|
|
.map(|t| t.to_string())
|
|
.unwrap_or_log(default.to_string()),
|
|
)
|
|
.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(
|
|
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_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 {
|
|
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![],
|
|
},
|
|
Service {
|
|
name: String::from("hlctl"),
|
|
arguments: vec![String::from("panel")],
|
|
},
|
|
],
|
|
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();
|
|
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,)
|
|
}
|
|
}
|