tag rework: tags now can either be standard or other, supports all types better

This commit is contained in:
emilis 2024-03-02 21:18:21 +00:00
parent e4bcc52cbe
commit f0930ab2eb
14 changed files with 432 additions and 405 deletions

View File

@ -1,7 +1,6 @@
use std::{ use std::{
borrow::BorrowMut, borrow::BorrowMut,
cmp::Ordering, collections::HashMap,
convert::Infallible,
env, env,
fmt::{Debug, Display}, fmt::{Debug, Display},
fs::{self}, fs::{self},
@ -13,21 +12,19 @@ use std::{
}; };
use log::info; use log::info;
use serde::{ use serde::{Deserialize, Serialize};
de::{Expected, Unexpected},
Deserialize, Serialize,
};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
environ::{self, ActiveKeybinds, EnvironError},
hlwm::{ hlwm::{
self, self,
attribute::Attribute, attribute::Attribute,
color::Color, color::Color,
command::{CommandError, HlwmCommand}, command::{CommandError, HlwmCommand},
hook::Hook, hook::Hook,
key::{Key, KeyParseError, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction}, key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction},
rule::{Condition, Consequence, FloatPlacement, Rule, Unrule}, rule::{Condition, Consequence, FloatPlacement, Rule, Unrule},
setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings}, setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings},
theme::ThemeAttr, theme::ThemeAttr,
@ -54,10 +51,10 @@ pub enum ConfigError {
CommandError(#[from] CommandError), CommandError(#[from] CommandError),
#[error("non-utf8 string error: {0}")] #[error("non-utf8 string error: {0}")]
Utf8StringError(#[from] FromUtf8Error), Utf8StringError(#[from] FromUtf8Error),
#[error("failed parsing keybind [{0}]: [{1}]")]
KeyParseError(String, KeyParseError),
#[error("failed parsing value from string: {0}")] #[error("failed parsing value from string: {0}")]
StringParseError(#[from] StringParseError), StringParseError(#[from] StringParseError),
#[error("getting from environment error: [{0}]")]
EnvironError(#[from] EnvironError),
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@ -342,23 +339,7 @@ impl Config {
info!("loading tag command set"); info!("loading tag command set");
(&self.tags) (&self.tags)
.into_iter() .into_iter()
.map(|tag| (tag, tag.key())) .map(|tag| tag.to_command_set(self.mod_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() .flatten()
.chain([HlwmCommand::And { .chain([HlwmCommand::And {
separator: Separator::Comma, separator: Separator::Comma,
@ -378,7 +359,9 @@ impl Config {
}, },
HlwmCommand::MergeTag { HlwmCommand::MergeTag {
tag: "default".to_string(), tag: "default".to_string(),
target: Some(hlwm::Tag::Name(self.tags.first().unwrap().to_string())), target: Some(hlwm::TagSelect::Name(
self.tags.first().cloned().unwrap().name().to_string(),
)),
}, },
], ],
} }
@ -387,7 +370,7 @@ impl Config {
} }
/// Create a config gathered from the herbstluftwm configs, /// Create a config gathered from the herbstluftwm configs,
/// using default mouse binds/tags /// using default mouse binds/attributes set
pub fn from_herbstluft() -> Self { pub fn from_herbstluft() -> Self {
fn setting<T, E: std::error::Error, F: FnOnce(&str) -> Result<T, E>>( fn setting<T, E: std::error::Error, F: FnOnce(&str) -> Result<T, E>>(
name: &str, name: &str,
@ -404,6 +387,7 @@ impl Config {
} }
let client = Client::new(); let client = Client::new();
let default = Config::default(); let default = Config::default();
let mod_key = setting(Self::MOD_KEY, Key::from_str, default.mod_key);
Config { Config {
font: client font: client
.get_str_attr(Self::FONT.to_string()) .get_str_attr(Self::FONT.to_string())
@ -417,7 +401,7 @@ impl Config {
font_pango_bold: client font_pango_bold: client
.get_str_attr(Self::FONT_PANGO_BOLD.to_string()) .get_str_attr(Self::FONT_PANGO_BOLD.to_string())
.unwrap_or_log(default.font_pango_bold), .unwrap_or_log(default.font_pango_bold),
mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key), mod_key,
services: setting( services: setting(
Self::SERVICES, Self::SERVICES,
|v| serde_json::de::from_str(v), |v| serde_json::de::from_str(v),
@ -444,26 +428,9 @@ impl Config {
.collect() .collect()
})(), })(),
}, },
keybinds: Self::active_keybinds(true).unwrap_or_log(default.keybinds), keybinds: environ::active_keybinds(ActiveKeybinds::OmitNamedTagBinds)
tags: (|| -> Result<Vec<Tag>, _> { .unwrap_or_log(default.keybinds),
Result::<_, ConfigError>::Ok({ tags: Self::active_tags(mod_key).unwrap_or_log(default.tags),
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_log(default.tags),
rules: (|| -> Result<Vec<Rule>, ConfigError> { rules: (|| -> Result<Vec<Rule>, ConfigError> {
Ok( Ok(
String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)? String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)?
@ -488,36 +455,75 @@ impl Config {
} }
} }
fn active_keybinds(omit_tag_binds: bool) -> Result<Vec<Keybind>, ConfigError> { fn active_tags(mod_key: Key) -> Result<Vec<Tag>, ConfigError> {
String::from_utf8(Client::new().execute(HlwmCommand::ListKeybinds)?.stdout)? let mut by_tag: HashMap<String, Vec<Keybind>> = HashMap::new();
.split("\n") environ::active_keybinds(ActiveKeybinds::OnlyNamedTagBinds)?
.filter(|i| { .into_iter()
!omit_tag_binds .filter(|bind| match bind.command.deref() {
|| match i.split("\t").skip(1).next() { HlwmCommand::UseTag(_) | HlwmCommand::MoveTag(_) => true,
Some(command) => { _ => false,
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| { .for_each(|bind| match bind.command.deref() {
Keybind::from_str(row) HlwmCommand::UseTag(tag) | HlwmCommand::MoveTag(tag) => match by_tag.get_mut(tag) {
.map_err(|err| ConfigError::KeyParseError(row.to_string(), err)) 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::<Result<Vec<_>, ConfigError>>() .collect())
} }
} }
@ -565,198 +571,6 @@ impl Theme {
} }
} }
#[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 {
write!(f, "{}", self.0.to_string())
}
}
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 { impl Default for Config {
fn default() -> Self { fn default() -> Self {
let resize_step = 0.1; let resize_step = 0.1;
@ -915,17 +729,19 @@ impl Default for Config {
arguments: vec![String::from("panel")], arguments: vec![String::from("panel")],
}, },
], ],
tags: (1..=5) tags: [
.into_iter() Tag::standard(".1", Key::Char('1')),
.map(|idx| Tag::FrontRow(idx.try_into().unwrap())) Tag::standard(".2", Key::Char('2')),
.chain(vec![ Tag::standard(".3", Key::Char('3')),
Tag::FunctionRow(1.try_into().unwrap()), Tag::standard(".4", Key::Char('4')),
Tag::FunctionRow(2.try_into().unwrap()), Tag::standard(".5", Key::Char('5')),
Tag::FunctionRow(3.try_into().unwrap()), Tag::standard("F1", Key::F1),
Tag::FunctionRow(4.try_into().unwrap()), Tag::standard("F2", Key::F2),
Tag::FunctionRow(5.try_into().unwrap()), Tag::standard("F3", Key::F3),
]) Tag::standard("F4", Key::F4),
.collect(), Tag::standard("F5", Key::F5),
]
.into(),
mousebinds: vec![ mousebinds: vec![
Mousebind::new( Mousebind::new(
mod_key, mod_key,
@ -1019,6 +835,110 @@ pub struct Service {
pub arguments: Vec<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)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

67
src/environ.rs Normal file
View File

@ -0,0 +1,67 @@
use std::{str::FromStr, string::FromUtf8Error};
use log::debug;
use thiserror::Error;
use crate::{
hlwm::{
command::{CommandError, HlwmCommand},
key::{KeyParseError, Keybind},
Client,
},
split,
};
#[derive(Clone, Debug, Error)]
pub enum EnvironError {
#[error("keybind parsing error: [{0}]")]
KeyParseError(#[from] KeyParseError),
#[error("command execution error: [{0}]")]
CommandError(#[from] CommandError),
}
impl From<FromUtf8Error> for EnvironError {
fn from(value: FromUtf8Error) -> Self {
CommandError::UtfError(value).into()
}
}
pub enum ActiveKeybinds {
All,
OmitNamedTagBinds,
OnlyNamedTagBinds,
}
pub fn active_keybinds(ty: ActiveKeybinds) -> Result<Vec<Keybind>, EnvironError> {
let (use_tag, move_tag) = (
HlwmCommand::UseTag(String::new()).to_string(),
HlwmCommand::MoveTag(String::new()).to_string(),
);
String::from_utf8(Client::new().execute(HlwmCommand::ListKeybinds)?.stdout)?
.split("\n")
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.filter(|i| {
let parts = split::tab_or_space(*i);
if parts.len() < 2 {
debug!(
"active_keybinds: parts.len() for [{i}] was [{}]; expected >= 2",
parts.len()
);
return false;
}
let command = parts[1].as_str();
match ty {
ActiveKeybinds::All => true,
ActiveKeybinds::OmitNamedTagBinds => command != use_tag && command != move_tag,
ActiveKeybinds::OnlyNamedTagBinds => command == use_tag || command == move_tag,
}
})
.map(|row: &str| {
Ok(Keybind::from_str(row).map_err(|err| {
debug!("row: [{row}], error: [{err}]");
err
})?)
})
.collect::<Result<Vec<_>, EnvironError>>()
}

View File

@ -4,7 +4,7 @@ use strum::IntoEnumIterator;
use serde::{de::Expected, Deserialize, Serialize}; use serde::{de::Expected, Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{gen_parse, hlwm::Client}; use crate::{gen_parse, hlwm::Client, split};
use super::{ use super::{
attribute::{Attribute, AttributeError, AttributeOption}, attribute::{Attribute, AttributeError, AttributeOption},
@ -14,9 +14,9 @@ use super::{
pad::Pad, pad::Pad,
rule::{Rule, Unrule}, rule::{Rule, Unrule},
setting::{FrameLayout, Setting, SettingName}, setting::{FrameLayout, Setting, SettingName},
split,
window::Window, window::Window,
Align, Direction, Index, Monitor, Operator, Separator, StringParseError, Tag, ToCommandString, Align, Direction, Index, Monitor, Operator, Separator, StringParseError, TagSelect,
ToCommandString,
}; };
#[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)] #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)]
@ -131,7 +131,7 @@ pub enum HlwmCommand {
/// If `target` is None, the focused tag will be used /// If `target` is None, the focused tag will be used
MergeTag { MergeTag {
tag: String, tag: String,
target: Option<Tag>, target: Option<TagSelect>,
}, },
Cycle, Cycle,
Focus(Direction), Focus(Direction),
@ -848,7 +848,7 @@ mod test {
rule::{Condition, Consequence, Rule, RuleOperator}, rule::{Condition, Consequence, Rule, RuleOperator},
setting::{Setting, SettingName}, setting::{Setting, SettingName},
window::Window, window::Window,
Align, Direction, Tag, ToCommandString, Align, Direction, TagSelect, ToCommandString,
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@ -984,7 +984,7 @@ mod test {
HlwmCommand::MergeTag { tag: _, target: _ } => ( HlwmCommand::MergeTag { tag: _, target: _ } => (
HlwmCommand::MergeTag { HlwmCommand::MergeTag {
tag: "my_tag".into(), tag: "my_tag".into(),
target: Some(Tag::Name("other_tag".into())), target: Some(TagSelect::Name("other_tag".into())),
}, },
"merge_tag\tmy_tag\tother_tag".into(), "merge_tag\tmy_tag\tother_tag".into(),
), ),

View File

@ -5,7 +5,7 @@ use strum::IntoEnumIterator;
use crate::gen_parse; use crate::gen_parse;
use super::{command::CommandParseError, window::Window, Monitor, Tag, ToCommandString}; use super::{command::CommandParseError, window::Window, Monitor, TagSelect, ToCommandString};
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
@ -38,7 +38,7 @@ pub enum Hook {
}, },
/// The flags (i.e. urgent or filled state) have been changed. /// The flags (i.e. urgent or filled state) have been changed.
TagFlags, TagFlags,
TagAdded(Tag), TagAdded(TagSelect),
TagRenamed { TagRenamed {
old: String, old: String,
new: String, new: String,

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use thiserror::Error; use thiserror::Error;
use crate::hlwm::split; use crate::split;
use super::{ use super::{
command::{CommandParseError, HlwmCommand}, command::{CommandParseError, HlwmCommand},
@ -43,6 +43,39 @@ pub enum Key {
Mouse(MouseButton), Mouse(MouseButton),
} }
impl Key {
pub fn is_standard(&self) -> bool {
match self {
Key::Char(_)
| Key::F1
| Key::F2
| Key::F3
| Key::F4
| Key::F5
| Key::F6
| Key::F7
| Key::F8
| Key::F9
| Key::F10
| Key::F11
| Key::F12 => true,
_ => false,
}
}
}
impl From<MouseButton> for Key {
fn from(value: MouseButton) -> Self {
Key::Mouse(value)
}
}
impl From<char> for Key {
fn from(value: char) -> Self {
Key::Char(value)
}
}
impl Serialize for Key { impl Serialize for Key {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where

View File

@ -18,7 +18,7 @@ use self::{
command::{CommandError, HlwmCommand}, command::{CommandError, HlwmCommand},
key::KeyParseError, key::KeyParseError,
setting::{Setting, SettingName}, setting::{Setting, SettingName},
window::TagStatus, tag::TagStatus,
}; };
pub mod attribute; pub mod attribute;
@ -32,7 +32,7 @@ mod octal;
pub mod pad; pub mod pad;
pub mod rule; pub mod rule;
pub mod setting; pub mod setting;
mod split; pub mod tag;
pub mod theme; pub mod theme;
pub mod window; pub mod window;
#[macro_use] #[macro_use]
@ -128,6 +128,7 @@ impl Client {
Ok(lines) Ok(lines)
} }
#[allow(unused)]
pub fn tag_status(&self) -> Result<Vec<TagStatus>, CommandError> { pub fn tag_status(&self) -> Result<Vec<TagStatus>, CommandError> {
Ok(self Ok(self
.query(HlwmCommand::TagStatus { monitor: None })? .query(HlwmCommand::TagStatus { monitor: None })?
@ -167,12 +168,12 @@ pub enum StringParseError {
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Tag { pub enum TagSelect {
Index(i32), Index(i32),
Name(String), Name(String),
} }
impl FromStr for Tag { impl FromStr for TagSelect {
type Err = Infallible; type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -184,17 +185,17 @@ impl FromStr for Tag {
} }
} }
impl Default for Tag { impl Default for TagSelect {
fn default() -> Self { fn default() -> Self {
Self::Index(Default::default()) Self::Index(Default::default())
} }
} }
impl std::fmt::Display for Tag { impl std::fmt::Display for TagSelect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Tag::Index(i) => write!(f, "{i}"), TagSelect::Index(i) => write!(f, "{i}"),
Tag::Name(name) => f.write_str(name), TagSelect::Name(name) => f.write_str(name),
} }
} }
} }

View File

@ -1,6 +1,6 @@
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use crate::hlwm::split; use crate::split;
use super::StringParseError; use super::StringParseError;

View File

@ -8,7 +8,9 @@ use std::{
use serde::{de::Expected, Deserialize, Serialize}; use serde::{de::Expected, Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use super::{hlwmbool, hook::Hook, split, StringParseError, ToCommandString}; use crate::split;
use super::{hlwmbool, hook::Hook, StringParseError, ToCommandString};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Rule { pub struct Rule {

View File

@ -3,15 +3,10 @@ use std::str::FromStr;
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{gen_parse, hlwm::command::CommandParseError}; use crate::{gen_parse, hlwm::command::CommandParseError, split};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use super::{ use super::{color::Color, hlwmbool::ToggleBool, StringParseError, ToCommandString};
color::Color,
hlwmbool::ToggleBool,
split::{self},
StringParseError, ToCommandString,
};
#[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq, strum::EnumDiscriminants)] #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq, strum::EnumDiscriminants)]
#[strum_discriminants( #[strum_discriminants(

98
src/hlwm/tag.rs Normal file
View File

@ -0,0 +1,98 @@
use std::{
fmt::{Display, Write},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use super::StringParseError;
pub struct TagStatus {
name: String,
state: TagState,
}
#[allow(unused)]
impl TagStatus {
pub fn name(&self) -> &str {
&self.name
}
pub fn state(&self) -> TagState {
self.state
}
}
impl FromStr for TagStatus {
type Err = StringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.chars();
let state = parts
.next()
.ok_or(StringParseError::UnknownValue)?
.try_into()?;
let name = parts.collect();
Ok(Self { name, state })
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
pub enum TagState {
Empty,
NotEmpty,
/// The tag contains an urgent window
Urgent,
/// The tag is viewed on the specified MONITOR and it is focused
SameMonitorFocused,
/// The tag is viewed on the specified MONITOR, but this monitor is not focused
SameMonitor,
/// The tag is viewed on a different MONITOR and it is focused
DifferentMonitorFocused,
/// The tag is viewed on a different MONITOR, but this monitor is not focused
DifferentMonitor,
}
impl TryFrom<char> for TagState {
type Error = StringParseError;
fn try_from(value: char) -> Result<Self, Self::Error> {
Self::iter()
.into_iter()
.find(|i| char::from(i) == value)
.ok_or(StringParseError::UnknownValue)
}
}
impl FromStr for TagState {
type Err = StringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::iter()
.into_iter()
.find(|i| i.to_string() == s)
.ok_or(StringParseError::UnknownValue)
}
}
impl From<&TagState> for char {
fn from(value: &TagState) -> Self {
match value {
TagState::SameMonitorFocused => '#',
TagState::SameMonitor => '+',
TagState::DifferentMonitorFocused => '%',
TagState::DifferentMonitor => '-',
TagState::Empty => '.',
TagState::NotEmpty => ':',
TagState::Urgent => '!',
}
}
}
impl Display for TagState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char(self.into())
}
}

View File

@ -4,7 +4,7 @@ use serde::{de::Expected, Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use thiserror::Error; use thiserror::Error;
use crate::hlwm::split; use crate::split;
use super::{ use super::{
attribute::Attribute, attribute::Attribute,

View File

@ -1,7 +1,4 @@
use std::{ use std::str::FromStr;
fmt::{Display, Write},
str::FromStr,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@ -60,95 +57,6 @@ impl std::fmt::Display for Window {
} }
} }
pub struct TagStatus {
name: String,
state: TagState,
}
#[allow(unused)]
impl TagStatus {
pub fn name(&self) -> &str {
&self.name
}
pub fn state(&self) -> TagState {
self.state
}
}
impl FromStr for TagStatus {
type Err = StringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.chars();
let state = parts
.next()
.ok_or(StringParseError::UnknownValue)?
.try_into()?;
let name = parts.collect();
Ok(Self { name, state })
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
pub enum TagState {
Empty,
NotEmpty,
/// The tag contains an urgent window
Urgent,
/// The tag is viewed on the specified MONITOR and it is focused
SameMonitorFocused,
/// The tag is viewed on the specified MONITOR, but this monitor is not focused
SameMonitor,
/// The tag is viewed on a different MONITOR and it is focused
DifferentMonitorFocused,
/// The tag is viewed on a different MONITOR, but this monitor is not focused
DifferentMonitor,
}
impl TryFrom<char> for TagState {
type Error = StringParseError;
fn try_from(value: char) -> Result<Self, Self::Error> {
Self::iter()
.into_iter()
.find(|i| char::from(i) == value)
.ok_or(StringParseError::UnknownValue)
}
}
impl FromStr for TagState {
type Err = StringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::iter()
.into_iter()
.find(|i| i.to_string() == s)
.ok_or(StringParseError::UnknownValue)
}
}
impl From<&TagState> for char {
fn from(value: &TagState) -> Self {
match value {
TagState::SameMonitorFocused => '#',
TagState::SameMonitor => '+',
TagState::DifferentMonitorFocused => '%',
TagState::DifferentMonitor => '-',
TagState::Empty => '.',
TagState::NotEmpty => ':',
TagState::Urgent => '!',
}
}
}
impl Display for TagState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char(self.into())
}
}
#[derive(Clone, Copy, strum::Display)] #[derive(Clone, Copy, strum::Display)]
#[strum(serialize_all = "UPPERCASE")] #[strum(serialize_all = "UPPERCASE")]
pub enum WindowType { pub enum WindowType {

View File

@ -4,13 +4,14 @@ use std::path::{Path, PathBuf};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use config::Config; use config::Config;
use log::{error, info}; use log::{error, info};
use logerr::UnwrapLog;
pub mod cmd; pub mod cmd;
mod config; mod config;
pub mod environ;
mod hlwm; mod hlwm;
pub mod logerr; pub mod logerr;
mod panel; mod panel;
pub mod split;
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[command(name = "hlctl")] #[command(name = "hlctl")]
@ -26,7 +27,8 @@ enum HlctlCommand {
/// Save the currently loaded configuration to file /// Save the currently loaded configuration to file
#[command(long_about = r#" #[command(long_about = r#"
`save` Tries to find an existing config file. If not present, the default config is used. `save` Tries to find an existing config file. If not present, the default config is used.
Whichever one is loaded, its tags are used. All other values are collected from the environment Whichever one is loaded, its mousebinds and attribute set are used.
All other values are collected from the environment
(or default values) and this new config is saved. (or default values) and this new config is saved.
The configuration file located at $HOME/.config/herbstluftwm/hlctl.toml"#)] The configuration file located at $HOME/.config/herbstluftwm/hlctl.toml"#)]
@ -59,8 +61,8 @@ fn load_config() -> Config {
Err(err) => { Err(err) => {
error!("Could not load config. Error: {err}"); error!("Could not load config. Error: {err}");
error!(""); error!("");
error!("Hint: try calling `hlctl save` to save a default or collected config"); error!("Loading default config");
std::process::exit(1); Config::default()
} }
} }
} }
@ -97,7 +99,8 @@ fn init() {
fn merged_config() -> Config { fn merged_config() -> Config {
let default = load_config(); let default = load_config();
let mut collected = Config::from_herbstluft(); let mut collected = Config::from_herbstluft();
collected.tags = default.tags; collected.mousebinds = default.mousebinds;
collected.attributes = default.attributes;
collected collected
} }