use std::{borrow::BorrowMut, str::FromStr}; use log::trace; use serde::{de::Expected, Deserialize, Serialize}; use crate::{ hlwm::{command::CommandParseError, parser::ArgParser}, split, }; use strum::IntoEnumIterator; use super::{ color::Color, hlwmbool::ToggleBool, parser::{FromCommandArgs, ParseError}, ToCommandString, }; #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq, strum::EnumDiscriminants)] #[strum_discriminants( name(SettingName), derive(strum::Display, strum::EnumIter), strum(serialize_all = "snake_case") )] #[strum(serialize_all = "snake_case")] pub enum Setting { /// If set, detect_monitors is automatically executed every time a monitor is connected, disconnected or resized. AutoDetectMonitors(ToggleBool), /// If set, EWMH panels are automatically detected and reserve space at the side of the monitors /// they are on (via pad attributes of each monitor). /// This setting is activated per default. AutoDetectPanels(ToggleBool), /// This setting controls the behaviour of focus and shift if no -e or -i argument is given. /// If set, then focus and shift changes the focused frame even if there are other clients /// in this frame in the specified direction. /// Else, a client within current frame is selected if it is in the specified direction. DefaultDirectionExternalOnly(ToggleBool), /// Name of the layout algorithm, which is used if a new frame is created (on a new tag or by a non-trivial split). DefaultFrameLayout(FrameLayout), /// String to append when window or tab titles are shortened to fit in the available space Ellipsis(String), /// If set, commands [HlwmCommand::Focus] and [HlwmCommand::Shift] cross monitor boundaries. /// If there is no client in the direction given to focus, then the monitor in the specified /// direction is focused. /// Similarly, if shift cannot move a window within a tag, the window is moved to the /// neighbour monitor in the desired direction. FocusCrossesMonitorBoundaries(ToggleBool), /// If set and a window is focused by mouse cursor, this window is focused /// (this feature is also known as sloppy focus). /// If unset, you need to click to change the window focus by mouse. /// /// If another window is hidden by the focus change /// (e.g. when having pseudotiled windows in the max layout) /// then an extra click is required to change the focus. FocusFollowsMouse(ToggleBool), /// If set, only pagers and taskbars are allowed to change the focus. If unset, all applications can request a /// focus change FocusStealingPrevention(ToggleBool), /// Focused frame opacity in percent. Requires a running compositing manager to take actual effect. FrameActiveOpacity(u8), /// The fill color of a focused frame FrameBgActiveColor(Color), /// The fill color of an unfocused frame /// It is only visible if non-focused frames are configured to be visible, see [ShowFrameDecoration]. FrameBgNormalColor(Color), /// If set, the background of frames are transparent. /// That means a rectangle is cut out from the inner such that only the frame border /// and a stripe of width [Setting::FrameTransparentWidth] can be seen. /// Use [Setting::FrameActiveOpacity] and [Setting::FrameNormalOpacity] for real transparency. FrameBgTransparent(ToggleBool), /// The border color of a focused frame FrameBorderActiveColor(Color), /// The color of the inner border of a frame FrameBorderInnerColor(Color), /// The width of the inner border of a frame. /// Must be less than [Setting::FrameBorderWidth], since it does not add to the frame border width but is a /// part of it. FrameBorderInnerWidth(u32), /// The border color of an unfocused frame FrameBorderNormalColor(Color), /// Border width of a frame FrameBorderWidth(u32), /// The gap between frames in the tiling mode FrameGap(u32), /// Unfocused frame opacity in percent. /// Requires a running compositing manager to take actual effect FrameNormalOpacity(u8), /// The padding within a frame in the tiling mode /// i.e. the space between the border of a frame and the windows within it FramePadding(u32), /// Specifies the width of the remaining frame colored with [Setting::FrameBgActiveColor] /// if [Setting::FrameBgTransparent] is set FrameTransparentWidth(u32), /// This setting affects the size of the last client in a frame that is arranged by grid layout. /// If set, then the last client always fills the gap within this frame. /// If unset, then the last client has the same size as all other clients in this frame GaplessGrid(ToggleBool), /// If activated, windows are explicitly hidden when they are covered by another window in a frame with max layout. /// This only has a visible effect if a compositor is used. /// If activated, shadows do not stack up and transparent windows show the wallpaper behind them instead of /// the other clients in the max layout. HideCoveredWindows(ToggleBool), /// If greater than 0, then the clients on all monitors aren’t moved or resized anymore. /// If it is set to 0, then the arranging of monitors is enabled again, and all monitors are rearranged /// if their content has changed in the meantime. /// You should not change this setting manually due to concurrency issues; /// use the commands [HlwmCommand::Lock] and [HlwmCommand::Unlock] instead. MonitorsLocked(u8), /// Specifies the gap around a monitor. /// If the monitor is selected and the mouse position would be restored into this gap, /// it is set to the center of the monitor. /// This is useful, when the monitor was left via mouse movement, but is reselected by keyboard. /// If the gap is 0 (default), the mouse is never recentered MouseRecenterGap(u32), /// If greater than 0, it specifies the least distance between a centered pseudotile window /// and the border of the frame or tile it is assigned to. /// If this distance is lower than pseudotile_center_threshold, it is aligned to the top left /// of the client’s tile. PseudotileCenterThreshold(u32), /// If set, a window is raised if it is clicked. The value of this setting is only noticed in floating mode RaiseOnClick(ToggleBool), /// If set, a window is raised if it is focused. The value of this setting is only used in floating mode RaiseOnFocus(ToggleBool), /// If set, a window is raised temporarily if it is focused on its tag. /// Temporarily in this case means that the window will return to its previous stacking position /// if another window is focused RaiseOnFocusTemporarily(ToggleBool), /// This controls, which frame decorations are shown (or none at all) ShowFrameDecorations(ShowFrameDecoration), /// See the docs for members of [SmartFrameSurroundings] SmartFrameSurroundings(SmartFrameSurroundings), /// If set, window borders and gaps will be removed and minimal when there’s no /// ambiguity regarding the focused window. /// This minimal window decoration can be configured by the `theme.minimal` object. SmartWindowSurroundings(ToggleBool), /// If a client is dragged in floating mode, then it snaps to neighbour clients /// if the distance between them is smaller than SnapDistance SnapDistance(u32), /// Specifies the remaining gap if a dragged client snaps to an edge in floating mode. /// If SnapGap is set to 0, no gap will remain SnapGap(u32), /// If set: If you want to view a tag, that already is viewed on another monitor, /// then the monitor contents will be swapped and you see the wanted tag on the focused monitor. /// If not set, the other monitor is focused if it shows the desired tag. SwapMonitorsToGetTag(ToggleBool), /// If activated, multiple windows in a frame with the max layout algorithm are drawn as tabs TabbedMax(ToggleBool), /// It contains the chars that are used to print a nice ascii tree. /// It must contain at least 8 characters. e.g. `X|:#+*-.` produces a tree like: /// ``` /// X-. /// #-. child 0 /// | #-* child 00 /// | +-* child 01 /// +-. child 1 /// : #-* child 10 /// : +-* child 11 /// ``` /// Useful values for tree_style are: `╾│ ├└╼─┐` or `-| |'--.` or `╾│ ├╰╼─╮.` TreeStyle(String), /// If set, a client's window content is resized immediately during resizing it with the mouse. /// If unset, the client's content is resized after the mouse button is released. UpdateDraggedClients(ToggleBool), /// If set, verbose output is logged to herbstluftwm’s stderr. /// The default value is controlled by the `--verbose` command line flag Verbose(ToggleBool), /// Border color of a focused window /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.active.color` WindowBorderActiveColor(Color), /// Color of the inner border of a window. /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.inner_color` WindowBorderInnerColor(Color), /// The width of the inner border of a window. /// Must be less than window_border_width, since it does not add to the window border width but is a part of it. /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.inner_width` WindowBorderInnerWidth(u32), /// Border color of an unfocused window /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.normal.color` WindowBorderNormalColor(Color), /// Border color of an unfocused but urgent window. /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.urgent.color` WindowBorderUrgentColor(Color), /// Border width of a window /// **Warning:** /// This only exists for compatibility reasons; it is only an alias for the attribute `theme.border_width` WindowBorderWidth(u32), /// The gap between windows within one frame in the tiling mode. WindowGap(u32), /// It controls the value of the `_NET_WM_NAME` property on the root window, /// which specifies the name of the running window manager. /// The value of this setting is not updated if the actual `_NET_WM_NAME` property /// on the root window is changed externally. Example usage: /// /// ``` /// cycle_value wmname herbstluftwm LG3D /// ``` Wmname(String), } impl Default for SettingName { fn default() -> Self { SettingName::AutoDetectMonitors } } impl FromStr for SettingName { type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .find(|i| i.to_string() == s) .ok_or(ParseError::InvalidCommand(s.to_string())) } } impl Serialize for Setting { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_command_string().replace("\t", " = ")) } } impl<'de> Deserialize<'de> for Setting { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { pub enum Expect { Command(String), } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Expect::Command(err) => { write!(f, "a valid [command][space/tab][value] string: {err}") } } } } let str_val: String = Deserialize::deserialize(deserializer)?; let ((command, arg), _) = split::on_first_match(&str_val, &['=']) .ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into())) .map_err(|err| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err) })?; Ok( Self::from_command_args(&command.trim(), [arg.trim()].into_iter()).map_err(|err| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&str_val), &Expect::Command(err.to_string()), ) })?, ) } } impl FromStr for Setting { type Err = ParseError; fn from_str(s: &str) -> Result { trace!("[from_str] beginning to parse [{s}] as setting"); let ((command, arg), _) = split::on_first_match(s, &['\t', ' ']).ok_or(ParseError::InvalidValue { value: s.to_string(), expected: "[setting] [value]", })?; Self::from_command_args(&command.trim(), [arg].into_iter()) } } impl FromCommandArgs for Setting { fn from_command_args, I: Iterator>( cmd: &str, args: I, ) -> Result { let mut command = Self::iter() .find(|s| s.to_string() == cmd) .ok_or(ParseError::InvalidCommand(cmd.to_string()))?; let mut parser = ArgParser::from_strings(args.map(|i| i.into())); match command.borrow_mut() { Setting::Verbose(arg) | Setting::TabbedMax(arg) | Setting::GaplessGrid(arg) | Setting::RaiseOnClick(arg) | Setting::RaiseOnFocus(arg) | Setting::AutoDetectPanels(arg) | Setting::FocusFollowsMouse(arg) | Setting::HideCoveredWindows(arg) | Setting::AutoDetectMonitors(arg) | Setting::FrameBgTransparent(arg) | Setting::SwapMonitorsToGetTag(arg) | Setting::UpdateDraggedClients(arg) | Setting::FocusStealingPrevention(arg) | Setting::RaiseOnFocusTemporarily(arg) | Setting::SmartWindowSurroundings(arg) | Setting::DefaultDirectionExternalOnly(arg) | Setting::FocusCrossesMonitorBoundaries(arg) => *arg = parser.next_from_str(cmd)?, Setting::FrameBgActiveColor(arg) | Setting::FrameBgNormalColor(arg) | Setting::FrameBorderInnerColor(arg) | Setting::FrameBorderActiveColor(arg) | Setting::FrameBorderNormalColor(arg) | Setting::WindowBorderInnerColor(arg) | Setting::WindowBorderActiveColor(arg) | Setting::WindowBorderNormalColor(arg) | Setting::WindowBorderUrgentColor(arg) => *arg = parser.next_from_str(cmd)?, Setting::DefaultFrameLayout(arg) => *arg = parser.next_from_str(cmd)?, Setting::ShowFrameDecorations(arg) => *arg = parser.next_from_str(cmd)?, Setting::SmartFrameSurroundings(arg) => *arg = parser.next_from_str(cmd)?, Setting::MonitorsLocked(arg) | Setting::FrameActiveOpacity(arg) | Setting::FrameNormalOpacity(arg) => *arg = parser.next_from_str(cmd)?, Setting::SnapGap(arg) | Setting::FrameGap(arg) | Setting::WindowGap(arg) | Setting::SnapDistance(arg) | Setting::FramePadding(arg) | Setting::MouseRecenterGap(arg) | Setting::FrameBorderWidth(arg) | Setting::WindowBorderWidth(arg) | Setting::FrameTransparentWidth(arg) | Setting::FrameBorderInnerWidth(arg) | Setting::WindowBorderInnerWidth(arg) | Setting::PseudotileCenterThreshold(arg) => *arg = parser.next_from_str(cmd)?, Setting::Wmname(arg) | Setting::Ellipsis(arg) | Setting::TreeStyle(arg) => { *arg = parser.must_string(cmd)? } } Ok(command) } } impl Default for Setting { fn default() -> Self { Self::AutoDetectMonitors(ToggleBool::Bool(true)) } } impl ToCommandString for Setting { fn to_command_string(&self) -> String { match self { Setting::AutoDetectMonitors(value) | Setting::AutoDetectPanels(value) | Setting::DefaultDirectionExternalOnly(value) | Setting::FocusCrossesMonitorBoundaries(value) | Setting::FocusFollowsMouse(value) | Setting::FocusStealingPrevention(value) | Setting::FrameBgTransparent(value) | Setting::GaplessGrid(value) | Setting::HideCoveredWindows(value) | Setting::RaiseOnClick(value) | Setting::RaiseOnFocus(value) | Setting::RaiseOnFocusTemporarily(value) | Setting::SmartWindowSurroundings(value) | Setting::TabbedMax(value) | Setting::UpdateDraggedClients(value) | Setting::Verbose(value) | Setting::SwapMonitorsToGetTag(value) => vec![self.to_string(), value.to_string()], Setting::TreeStyle(value) | Setting::Wmname(value) | Setting::Ellipsis(value) => { vec![self.to_string(), value.to_string()] } Setting::FrameActiveOpacity(value) | Setting::FrameNormalOpacity(value) | Setting::MonitorsLocked(value) => vec![self.to_string(), value.to_string()], Setting::FrameBorderInnerWidth(value) | Setting::FrameBorderWidth(value) | Setting::FrameGap(value) | Setting::FramePadding(value) | Setting::FrameTransparentWidth(value) | Setting::MouseRecenterGap(value) | Setting::PseudotileCenterThreshold(value) | Setting::SnapDistance(value) | Setting::SnapGap(value) | Setting::WindowBorderInnerWidth(value) | Setting::WindowBorderWidth(value) | Setting::WindowGap(value) => vec![self.to_string(), value.to_string()], Setting::FrameBgActiveColor(value) | Setting::FrameBgNormalColor(value) | Setting::FrameBorderActiveColor(value) | Setting::FrameBorderInnerColor(value) | Setting::FrameBorderNormalColor(value) | Setting::WindowBorderActiveColor(value) | Setting::WindowBorderInnerColor(value) | Setting::WindowBorderNormalColor(value) | Setting::WindowBorderUrgentColor(value) => vec![self.to_string(), value.to_string()], Setting::DefaultFrameLayout(value) => vec![self.to_string(), value.to_string()], Setting::ShowFrameDecorations(value) => vec![self.to_string(), value.to_string()], Setting::SmartFrameSurroundings(value) => vec![self.to_string(), value.to_string()], } .join("\t") } } #[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)] #[strum(serialize_all = "snake_case")] pub enum ShowFrameDecoration { /// Show no frame decorations at all None, /// Show decorations of frames that have client windows, Nonempty, /// Show decorations on the tags with at least two frames, IfMultiple, /// Show decorations of frames that have no client windows, IfEmpty, /// Show the decoration of focused and nonempty frames, Focused, /// Show decorations of focused and non-empty frames on tags with at least two frames. FocusedIfMultiple, /// Show all frame decorations. All, } impl Default for ShowFrameDecoration { fn default() -> Self { Self::FocusedIfMultiple } } #[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)] #[strum(serialize_all = "snake_case")] pub enum SmartFrameSurroundings { /// Frame borders and gaps will be removed when there is no ambiguity regarding the focused frame HideAll, /// Only frame gaps will be removed when there is no ambiguity regarding the focused frame HideGaps, /// Always show frame borders and gaps Off, } impl Default for SmartFrameSurroundings { fn default() -> Self { Self::Off } } #[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)] #[strum(serialize_all = "snake_case")] pub enum FrameLayout { /// clients are placed below each other Vertical, /// clients are placed next to each other Horizontal, /// all clients are maximized in this frame Max, /// clients are arranged in an almost quadratic grid Grid, } impl Default for FrameLayout { fn default() -> Self { Self::Vertical } } #[cfg(test)] mod test { use serde::{Deserialize, Serialize}; use crate::hlwm::{setting::SettingName, ToggleBool}; use super::Setting; #[derive(Debug, Serialize, Deserialize)] pub struct SettingWrapper { setting: Setting, } #[test] fn setting_serialize_deserialize() { let setting = SettingWrapper { setting: Setting::AutoDetectMonitors(ToggleBool::Toggle), }; let serialized = toml::to_string_pretty(&setting).expect("serializing"); let deserialized: SettingWrapper = toml::from_str(&serialized).expect("deserializing"); assert_eq!(setting.setting, deserialized.setting); } #[test] fn setting_name() { assert_eq!( "auto_detect_monitors", SettingName::AutoDetectMonitors.to_string() ) } }