1021 lines
35 KiB
Rust
1021 lines
35 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: Option<String>,
|
|
pub font_bold: Option<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 SetAttribute {
|
|
pub fn new<S: Into<String>>(path: S, value: Attribute) -> Self {
|
|
Self {
|
|
path: path.into(),
|
|
value,
|
|
}
|
|
}
|
|
}
|
|
|
|
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, self.font.clone()),
|
|
(Self::MOD_KEY, Some(self.mod_key.to_string())),
|
|
(Self::FONT_BOLD, 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(Some(f.to_string())) },
|
|
default.font,
|
|
),
|
|
mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key),
|
|
font_bold: Some(
|
|
client
|
|
.get_attr(ThemeAttr::TitleFont(String::new()).attr_path())
|
|
.map(|a| a.to_string())
|
|
.unwrap_or(default.font_bold.unwrap()),
|
|
),
|
|
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>,
|
|
}
|
|
|
|
#[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: Some(String::from("-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*")),
|
|
font_bold: Some(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()),
|
|
],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
pub struct Service {
|
|
pub name: String,
|
|
pub arguments: Vec<String>,
|
|
}
|
|
|
|
impl Service {
|
|
pub fn spawn(&self) -> Result<(), CommandError> {
|
|
Client::new().execute(HlwmCommand::Spawn {
|
|
executable: self.name.clone(),
|
|
args: self.arguments.clone(),
|
|
})?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[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,)
|
|
}
|
|
}
|