├── .gitignore ├── assets ├── preview.gif └── example_config.toml ├── tests ├── setup.sh └── integration_test.rs ├── Containerfile ├── .github └── workflows │ ├── test.yaml │ └── release.yaml ├── Cargo.toml ├── LICENSE ├── src ├── regex.rs ├── config.rs ├── main.rs └── lib.rs ├── CHANGELOG.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | *.log 4 | .vagrant 5 | -------------------------------------------------------------------------------- /assets/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roosta/i3wsr/HEAD/assets/preview.gif -------------------------------------------------------------------------------- /tests/setup.sh: -------------------------------------------------------------------------------- 1 | export DISPLAY=:99.0 2 | Xvfb :99.0 & 3 | sleep 3 4 | i3 -c /dev/null & 5 | sleep 3 6 | gpick & 7 | sleep 3 8 | xterm & 9 | sleep 3 10 | DISPLAY=:99.0 i3-msg [class="XTerm"] floating enable 11 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS deps 2 | RUN apk update && apk add rust i3wm gpick xterm cargo xvfb bash 3 | 4 | FROM alpine:latest 5 | COPY --from=deps /var/cache/apk /var/cache/apk 6 | COPY . ./app 7 | WORKDIR /app 8 | RUN apk add --no-cache rust i3wm gpick xterm cargo xvfb bash 9 | CMD /app/tests/setup.sh && cargo test 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'test' 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | 12 | - name: Install test dependencies 13 | run: sudo apt-get install -y i3-wm gpick xterm 14 | 15 | - name: Setup test environment 16 | run: tests/setup.sh 17 | 18 | - name: Run tests 19 | run: cargo test 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Crates.io package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | container: 11 | image: rust:latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install toml-cli 16 | run: cargo install toml-cli 17 | 18 | - name: Check version 19 | run: test "v$(toml get -r Cargo.toml package.version)" = "${{ github.ref_name }}" 20 | 21 | - name: Publish 22 | run: cargo publish 23 | env: 24 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 25 | -------------------------------------------------------------------------------- /assets/example_config.toml: -------------------------------------------------------------------------------- 1 | [icons] 2 | # font awesome 3 | TelegramDesktop = "" 4 | firefox = "" 5 | Alacritty = "" 6 | Termite = "" 7 | Thunderbird = "" 8 | Gpick = "" 9 | Nautilus = "📘" 10 | # smile emoji 11 | MyNiceProgram = "😛" 12 | 13 | # i3 / Xwayland 14 | [aliases.class] 15 | TelegramDesktop = "Telegram" 16 | "Org\\.gnome\\.Nautilus" = "Nautilus" 17 | 18 | # Sway only 19 | [aliases.app_id] 20 | "^firefox$" = "Firefox" 21 | 22 | # i3 only 23 | [aliases.instance] 24 | "open.spotify.com" = "Spotify" 25 | 26 | # Both i3 and sway 27 | [aliases.name] 28 | 29 | [general] 30 | separator = "  " 31 | split_at = ":" 32 | empty_label = "🌕" 33 | display_property = "instance" # class, instance, name 34 | default_icon = "" 35 | 36 | [options] 37 | remove_duplicates = false 38 | no_names = false 39 | no_icon_names = false 40 | focus_fix = false 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "i3wsr" 4 | version = "3.1.2" 5 | description = "A dynamic workspace renamer for i3 and Sway that updates names to reflect their active applications." 6 | authors = ["Daniel Berg "] 7 | repository = "https://github.com/roosta/i3wsr" 8 | readme = "README.md" 9 | keywords = ["i3", "workspaces", "linux", "sway"] 10 | categories = ["gui", "command-line-utilities", "config"] 11 | license = "MIT" 12 | exclude = ["/script", "/assets/*", "Vagrantfile"] 13 | 14 | [lib] 15 | name = "i3wsr_core" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "i3wsr" 20 | path = "src/main.rs" 21 | 22 | [dependencies] 23 | clap = { version = "4.5", features = ["derive"] } 24 | toml = "0.7" 25 | serde = { version = "1.0", features = ["derive"] } 26 | itertools = "0.13" 27 | regex = "1.11" 28 | dirs = "5.0" 29 | thiserror = "1.0" 30 | swayipc = "3.0" 31 | colored = "2" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/regex.rs: -------------------------------------------------------------------------------- 1 | use crate::Config; 2 | pub use regex::Regex; 3 | use std::collections::HashMap; 4 | use std::error::Error; 5 | use std::fmt; 6 | 7 | #[derive(Debug)] 8 | pub enum RegexError { 9 | Compilation(regex::Error), 10 | Pattern(String), 11 | } 12 | 13 | impl fmt::Display for RegexError { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self { 16 | RegexError::Compilation(e) => write!(f, "Regex compilation error: {}", e), 17 | RegexError::Pattern(e) => write!(f, "{}", e), 18 | } 19 | } 20 | } 21 | 22 | impl Error for RegexError {} 23 | 24 | impl From for RegexError { 25 | fn from(err: regex::Error) -> Self { 26 | RegexError::Compilation(err) 27 | } 28 | } 29 | 30 | /// A compiled regex pattern and its corresponding replacement string 31 | pub type Pattern = (Regex, String); 32 | 33 | /// Holds compiled regex patterns for different window properties 34 | #[derive(Debug)] 35 | pub struct Compiled { 36 | pub class: Vec, 37 | pub instance: Vec, 38 | pub name: Vec, 39 | pub app_id: Vec, 40 | } 41 | 42 | /// Compiles a single regex pattern from a key-value pair 43 | fn compile_pattern((pattern, replacement): (&String, &String)) -> Result { 44 | Ok(( 45 | Regex::new(pattern).map_err(|e| { 46 | RegexError::Pattern(format!("Invalid regex pattern '{}': {}", pattern, e)) 47 | })?, 48 | replacement.to_owned(), 49 | )) 50 | } 51 | 52 | /// Compiles a collection of patterns from a HashMap 53 | fn compile_patterns(patterns: &HashMap) -> Result, RegexError> { 54 | patterns 55 | .iter() 56 | .map(|(k, v)| compile_pattern((k, v))) 57 | .collect() 58 | } 59 | 60 | /// Parses the configuration into compiled regex patterns 61 | pub fn parse_config(config: &Config) -> Result { 62 | Ok(Compiled { 63 | class: compile_patterns(&config.aliases.class)?, 64 | instance: compile_patterns(&config.aliases.instance)?, 65 | name: compile_patterns(&config.aliases.name)?, 66 | app_id: compile_patterns(&config.aliases.app_id)?, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use swayipc::{Connection, Node}; 4 | use i3wsr_core::{Config, update_tree}; 5 | 6 | #[test] 7 | fn connection_tree() -> Result<(), Box> { 8 | env::set_var("DISPLAY", ":99.0"); 9 | let mut conn = Connection::new()?; 10 | let config = Config::default(); 11 | let res = i3wsr_core::regex::parse_config(&config)?; 12 | assert!(update_tree(&mut conn, &config, &res, false).is_ok()); 13 | 14 | let tree = conn.get_tree()?; 15 | let workspaces = i3wsr_core::get_workspaces(tree); 16 | 17 | let name = workspaces.first() 18 | .and_then(|ws| ws.name.as_ref()) 19 | .map(|name| name.to_string()) 20 | .unwrap_or_default(); 21 | 22 | assert_eq!(name, String::from("1 Gpick | XTerm")); 23 | Ok(()) 24 | } 25 | 26 | #[test] 27 | fn get_title() -> Result<(), Box> { 28 | env::set_var("DISPLAY", ":99.0"); 29 | let mut conn = swayipc::Connection::new()?; 30 | 31 | let tree = conn.get_tree()?; 32 | let mut ws_nodes: Vec = Vec::new(); 33 | let workspaces = i3wsr_core::get_workspaces(tree); 34 | for workspace in &workspaces { 35 | let nodes = workspace.nodes.iter() 36 | .chain( 37 | workspace.floating_nodes.iter().flat_map(|fnode| { 38 | if !fnode.nodes.is_empty() { 39 | fnode.nodes.iter() 40 | } else { 41 | std::slice::from_ref(fnode).iter() 42 | } 43 | }) 44 | ) 45 | .cloned() 46 | .collect::>(); 47 | ws_nodes.extend(nodes); 48 | } 49 | let config = i3wsr_core::Config::default(); 50 | let res = i3wsr_core::regex::parse_config(&config)?; 51 | let result: Result, _> = ws_nodes 52 | .iter() 53 | .map(|node| i3wsr_core::get_title(node, &config, &res)) 54 | .collect(); 55 | assert_eq!(result?, vec!["Gpick", "XTerm"]); 56 | Ok(()) 57 | } 58 | 59 | #[test] 60 | fn collect_titles() -> Result<(), Box> { 61 | env::set_var("DISPLAY", ":99.0"); 62 | let mut conn = swayipc::Connection::new()?; 63 | let tree = conn.get_tree()?; 64 | let workspaces = i3wsr_core::get_workspaces(tree); 65 | let mut result: Vec> = Vec::new(); 66 | let config = i3wsr_core::Config::default(); 67 | let res = i3wsr_core::regex::parse_config(&config)?; 68 | for workspace in workspaces { 69 | result.push(i3wsr_core::collect_titles(&workspace, &config, &res)); 70 | } 71 | let expected = vec![vec!["Gpick", "XTerm"]]; 72 | assert_eq!(result, expected); 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::io::{self, Read}; 5 | use std::path::Path; 6 | use thiserror::Error; 7 | 8 | type StringMap = HashMap; 9 | type IconMap = HashMap; 10 | type OptionMap = HashMap; 11 | 12 | #[derive(Error, Debug)] 13 | pub enum ConfigError { 14 | #[error("Failed to read config file: {0}")] 15 | IoError(#[from] io::Error), 16 | #[error("Failed to parse TOML: {0}")] 17 | TomlError(#[from] toml::de::Error), 18 | } 19 | 20 | /// Represents aliases for different categories 21 | #[derive(Deserialize, Debug, Clone)] 22 | #[serde(default)] 23 | pub struct Aliases { 24 | pub class: StringMap, 25 | pub instance: StringMap, 26 | pub name: StringMap, 27 | pub app_id: StringMap, 28 | } 29 | 30 | impl Aliases { 31 | /// Creates a new empty Aliases instance 32 | pub fn new() -> Self { 33 | Self::default() 34 | } 35 | 36 | /// Gets an alias by category and key 37 | pub fn get_alias(&self, category: &str, key: &str) -> Option<&String> { 38 | match category { 39 | "app_id" => self.app_id.get(key), 40 | "class" => self.class.get(key), 41 | "instance" => self.instance.get(key), 42 | "name" => self.name.get(key), 43 | _ => None, 44 | } 45 | } 46 | } 47 | 48 | impl Default for Aliases { 49 | fn default() -> Self { 50 | Self { 51 | class: StringMap::new(), 52 | instance: StringMap::new(), 53 | name: StringMap::new(), 54 | app_id: StringMap::new(), 55 | } 56 | } 57 | } 58 | 59 | /// Main configuration structure 60 | #[derive(Deserialize, Debug, Clone)] 61 | #[serde(default)] 62 | pub struct Config { 63 | pub icons: IconMap, 64 | pub aliases: Aliases, 65 | pub general: StringMap, 66 | pub options: OptionMap, 67 | } 68 | 69 | impl Config { 70 | /// Creates a new Config instance from a file 71 | pub fn new(filename: &Path) -> Result { 72 | let config = Self::from_file(filename)?; 73 | Ok(config) 74 | } 75 | 76 | /// Loads configuration from a TOML file 77 | pub fn from_file(filename: &Path) -> Result { 78 | let mut file = File::open(filename)?; 79 | let mut buffer = String::new(); 80 | file.read_to_string(&mut buffer)?; 81 | let config: Config = toml::from_str(&buffer)?; 82 | Ok(config) 83 | } 84 | 85 | /// Gets a general configuration value 86 | pub fn get_general(&self, key: &str) -> Option { 87 | self.general.get(key).map(|s| s.to_string()) 88 | } 89 | 90 | /// Gets an option value 91 | pub fn get_option(&self, key: &str) -> Option { 92 | self.options.get(key).copied() 93 | } 94 | 95 | /// Gets an icon by key 96 | pub fn get_icon(&self, key: &str) -> Option { 97 | self.icons.get(key).map(|s| s.to_string()) 98 | } 99 | 100 | /// Sets a general configuration value 101 | pub fn set_general(&mut self, key: String, value: String) { 102 | self.general.insert(key, value); 103 | } 104 | 105 | /// Sets a an option configuration value 106 | pub fn set_option(&mut self, key: String, value: bool) { 107 | self.options.insert(key, value); 108 | } 109 | } 110 | 111 | impl Default for Config { 112 | fn default() -> Self { 113 | Self { 114 | icons: IconMap::new(), 115 | aliases: Aliases::default(), 116 | general: StringMap::new(), 117 | options: OptionMap::new(), 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [Unreleased] - 2025-07-21 6 | 7 | ## [v3.1.2] - 2025-07-21 8 | ### Bug fixes 9 | 10 | - Fix issue with unwanted workspace focus change on rename dispatches. Add 11 | feature flag for the focus fix, now needs to be enabled manually via either 12 | config or cmdline flags 13 | `--focus-fix`. 14 | ```toml 15 | [options] 16 | focus_fix = true 17 | ``` 18 | 19 | Ref: https://github.com/roosta/i3wsr/issues/42 20 | 21 | ## [v3.1.1] - 2025-01-25 22 | 23 | ### Bug Fixes 24 | 25 | - Fix an issue where `i3wsr` would not collect `i3` titles correctly. This also 26 | more cleanly handles the differing tree structures between sway and i3 27 | without the need for a conditional. 28 | 29 | ## [v3.1.0] - 2025-01-22 30 | 31 | ### Bug Fixes 32 | 33 | - Fix [multi-monitor window dragging issue specific to i3](https://github.com/roosta/i3wsr/issues/34) 34 | - Sway doesn't trigger any events on window drag 35 | 36 | ### Features 37 | 38 | - Add sway support 39 | - Add `--verbose` cmdline flag for easier debugging in case of issues 40 | 41 | #### Sway 42 | 43 | Support for [Sway](https://github.com/swaywm/sway) is added, new config key 44 | addition `app_id` in place of `class` when running native Wayland applications: 45 | 46 | ``` 47 | [aliases.app_id] 48 | firefox-developer-edition = "Firefox Developer" 49 | ``` 50 | `i3wsr` will still check for `name`, `instance`, and `class` for `Xwayland` 51 | windows, where applicable. So some rules can be preserved. To migrate replace 52 | `[aliases.class]` with `[aliases.app_id]`, keep in mind that `app_id` and 53 | `class` aren't always interchangeable , so some additional modifications is 54 | usually needed. 55 | 56 | > A useful script figuring out `app_id` can be found [here](https://gist.github.com/crispyricepc/f313386043395ff06570e02af2d9a8e0#file-wlprop-sh), it works like `xprop` but for Wayland. 57 | 58 | ### Deprecations 59 | 60 | I've flagged `--icons` as deprecated, it will not exit the application but it 61 | no longer works. I'd be surprised if anyone actually used that preset, as it 62 | was only ever for demonstration purposes, and kept around as a holdover from 63 | previous versions. 64 | 65 | ## [v3.0.0] - 2024-02-19 66 | 67 | **BREAKING**: Config syntax changes, see readme for new syntax but in short 68 | `wm_property` is no longer, and have been replaced by scoped aliases that are 69 | checked in this order: 70 | ```toml 71 | [aliases.name] # 1 72 | ".*mutt$" = "Mutt" 73 | 74 | [aliases.instance] # 2 75 | "open.spotify.com" = "Spotify" 76 | 77 | [aliases.class] # 3 78 | "^firefoxdeveloperedition$" = "Firefox-dev" 79 | ``` 80 | 81 | If there are no alias defined, `i3wsr` will default class, but this can be 82 | configured with 83 | ``` 84 | --display-property=[class|instance|name]` 85 | ``` 86 | or config file: 87 | 88 | ```toml 89 | [general] 90 | display_property = "instance" # class, instance, name 91 | ``` 92 | 93 | ### Bug Fixes 94 | 95 | - Missing instance in class string 96 | - Remove old file from package exclude 97 | - Tests, update connection namespace 98 | - Clean cache on vagrant machine 99 | - Format source files using rustfmt 100 | - Refresh lock file 101 | - License year to current 102 | - Ignore scratch buffer 103 | - Tests 104 | - Handle no alias by adding display_prop conf 105 | - Add display property as a cmd opt 106 | 107 | ### Documentation 108 | 109 | - Fix readme url (after branch rename) 110 | - Update instance explanation 111 | - Update toc 112 | - Document aliases usage 113 | - Update readme and example config 114 | - Fix badge, update toc, fix section placement 115 | - Fix typo 116 | 117 | ### Features 118 | 119 | - Update casing etc for error msg 120 | - Add split_at option 121 | - [**breaking**] Enable wm_property scoped aliases 122 | - Add empty_label option 123 | 124 | ### Miscellaneous Tasks 125 | 126 | - Add test workflow, update scripts 127 | - Update test branch 128 | - Fix job name 129 | - Remove old travis conf 130 | - Remove leftover cmd opt 131 | 132 | ### Refactor 133 | 134 | - Rewrite failure logic 135 | - Remove lazy_static 136 | - Move i3ipc to dependency section 137 | - Remove unneeded extern declarations 138 | - Update clap, rewrite args parsing 139 | - Move cmd arg parsing to new setup fn 140 | - Replace xcb with i3ipc window_properties 141 | 142 | ### Styling 143 | 144 | - Rustfmt 145 | 146 | ### Testing 147 | 148 | - Update ubuntu version 149 | - Fix tests after failure refactor 150 | 151 | ### Deps 152 | 153 | - Update to latest version of xcb 154 | - Update toml (0.7.6) 155 | - Update serde (1.0.171) 156 | - Update itertools (0.11.0) 157 | - Pin regex to 1.9.1 158 | - Pin endoing to 0.2.33 159 | 160 | ## [v2.1.1] - 2022-03-15 161 | 162 | ### Bug Fixes 163 | 164 | - Use with_context() instead of context() 165 | 166 | ## [v2.1.0] - 2022-03-14 167 | 168 | ### Bug Fixes 169 | 170 | - Build warnings 171 | 172 | ### Documentation 173 | 174 | - Add examples of workspace assignment 175 | - Document about the default config file 176 | 177 | 178 | [Unreleased]: https://github.com/roosta/i3wsr/compare/v3.1.2...HEAD 179 | [v3.1.2]: https://github.com/roosta/i3wsr/compare/v3.1.1...v3.1.2 180 | [v3.1.1]: https://github.com/roosta/i3wsr/compare/v3.1.0...v3.1.1 181 | [v3.1.0]: https://github.com/roosta/i3wsr/compare/v3.0.0...v3.1.0 182 | [v3.0.0]: https://github.com/roosta/i3wsr/compare/v2.1.1...v3.0.0 183 | [v2.1.1]: https://github.com/roosta/i3wsr/compare/v2.1.0...v2.1.1 184 | [v2.1.0]: https://github.com/roosta/i3wsr/compare/v2.1.1...v3.0.0 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | i3wsr - i3/Sway workspace renamer 2 | ====== 3 | 4 | [![Test Status](https://github.com/roosta/i3wsr/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/roosta/i3wsr/actions) 5 | [![Crates.io](https://img.shields.io/crates/v/i3wsr)](https://crates.io/crates/i3wsr) 6 | 7 | A dynamic workspace renamer for i3 and Sway that updates names to reflect their 8 | active applications. 9 | 10 | `i3wsr` can be configured through command-line flags or a `TOML` config file, 11 | offering extensive customization of workspace names, icons, aliases, and 12 | display options. 13 | 14 | ## Preview 15 | 16 | ![preview](https://raw.githubusercontent.com/roosta/i3wsr/main/assets/preview.gif) 17 | 18 | ## Requirements 19 | 20 | i3wsr requires [i3](https://i3wm.org/) or [sway](https://swaywm.org/), and 21 | [numbered 22 | workspaces](https://i3wm.org/docs/userguide.html#_changing_named_workspaces_moving_to_workspaces), 23 | see [Configuration](#configuration) 24 | 25 | ## Installation 26 | 27 | [Rust](https://www.rust-lang.org/en-US/), and [Cargo](http://doc.crates.io/) is 28 | required, and `i3wsr` can be installed using cargo like so: 29 | 30 | ```sh 31 | cargo install i3wsr 32 | ``` 33 | 34 | Or alternatively, you can build a release binary, 35 | 36 | ```sh 37 | cargo build --release 38 | ``` 39 | 40 | Then place the built binary, located at `target/release/i3wsr`, somewhere on your `$path`. 41 | 42 | ### Arch linux 43 | 44 | If you're running Arch you can install either [stable](https://aur.archlinux.org/packages/i3wsr/), or [latest](https://aur.archlinux.org/packages/i3wsr-git/) from AUR thanks to reddit user [u/OniTux](https://www.reddit.com/user/OniTux). 45 | 46 | ## Usage 47 | 48 | Just launch the program and it'll listen for events if you are running I3 or 49 | Sway. Another option is to put something like this in your i3 or Sway config: 50 | 51 | ``` 52 | # i3 53 | exec_always --no-startup-id i3wsr 54 | 55 | # Sway 56 | exec_always i3wsr 57 | ``` 58 | 59 | > `exec_always` ensures a new instance of `i3wsr` is started when config is reloaded or wm/compositor is restarted. 60 | 61 | ## Configuration 62 | 63 | This program depends on numbered workspaces, since we're constantly changing the 64 | workspace name. So your I3 or Sway configuration need to reflect this: 65 | 66 | ``` 67 | bindsym $mod+1 workspace number 1 68 | assign [class="(?i)firefox"] number 1 69 | ``` 70 | 71 | ### Keeping part of the workspace name 72 | 73 | If you're like me and don't necessarily bind your workspaces to only numbers, 74 | or you want to keep a part of the name constant you can do like this: 75 | 76 | ``` 77 | set $myws "1:[Q]" # my sticky part 78 | bindsym $mod+q workspace number $myws 79 | assign [class="(?i)firefox"] number $myws 80 | ``` 81 | 82 | This way the workspace would look something like this when it gets changed: 83 | 84 | ``` 85 | 1:[Q] Emacs|Firefox 86 | ``` 87 | You can take this a bit further by using a bar that trims the workspace number and be left with only 88 | ``` 89 | [Q] Emacs|Firefox 90 | ``` 91 | 92 | ## Configuration / options 93 | 94 | Configuration for i3wsr can be done using cmd flags, or a config file. A config 95 | file allows for more nuanced settings, and is required to configure icons and 96 | aliases. By default i3wsr looks for the config file at 97 | `$XDG_HOME/.config/i3wsr/config.toml` or `$XDG_CONFIG_HOME/i3wsr/config.toml`. 98 | To specify another path, pass it to the `--config` option on invocation: 99 | ```bash 100 | i3wsr --config ~/my_config.toml 101 | ``` 102 | Example config can be found in 103 | [assets/example\_config.toml](https://github.com/roosta/i3wsr/blob/main/assets/example_config.toml). 104 | 105 | 106 | ### Aliases 107 | 108 | 109 | Sometimes a class, instance or name can be overly verbose, use aliases that 110 | match to window properties to create simpler names instead of showing the full 111 | property 112 | 113 | 114 | ```toml 115 | # For Sway 116 | [aliases.app_id] 117 | 118 | # for i3 119 | [aliases.class] 120 | 121 | # Exact match 122 | "^Google-chrome-unstable$" = "Chrome-dev" 123 | 124 | # Substring match 125 | firefox = "Firefox" 126 | 127 | # Escape if you want to match literal periods 128 | "Org\\.gnome\\.Nautilus" = "Nautilus" 129 | ``` 130 | Alias keys uses regex for matching, so it's possible to get creative: 131 | 132 | ```toml 133 | # This will match gimp regardless of version number reported in class 134 | "Gimp-\\d\\.\\d\\d" = "Gimp" 135 | 136 | # Capture group substitutions 137 | "^(.*) - Google Chrome$" = "🌐 $1" 138 | ``` 139 | 140 | Remember to quote anything but `[a-zA-Z]`, and to escape your slashes. Due to 141 | rust string escapes if you want a literal backslash use two slashes `\\d`. 142 | 143 | ### Aliases based on property 144 | 145 | i3wsr supports 4 window properties currently: 146 | 147 | ```toml 148 | [aliases.name] # 1 i3 / wayland / sway 149 | [aliases.instance] # 2 i3 / xwayland 150 | [aliases.class] # 3 i3 / xwayland 151 | [aliases.app_id] # 3 wayland / sway only 152 | ``` 153 | These are checked in descending order, so if i3wsr finds a name alias, it'll 154 | use that and if not, then check instance, then finally use class 155 | 156 | #### Class 157 | 158 | > Only for Xwayland / i3 159 | 160 | This is the default for `i3`, and the most succinct. 161 | 162 | #### App id 163 | 164 | > Only for Wayland / Sway 165 | 166 | This is the default for wayland apps, and the most and works largely like class. 167 | 168 | #### Instance 169 | 170 | > Only for Xwayland / i3 171 | 172 | Use `instance` instead of `class` when assigning workspace names, 173 | instance is usually more specific. i3wsr will try to get the instance but if it 174 | isn't defined will fall back to class. 175 | 176 | A use case for this option could be launching `chromium 177 | --app="https://web.whatsapp.com"`, and then assign a different icon to whatsapp 178 | in your config file, while chrome retains its own alias: 179 | ```toml 180 | 181 | [icons] 182 | "WhatsApp" = "🗩" 183 | 184 | [aliases.class] 185 | Google-chrome = "Chrome" 186 | 187 | [aliases.instance] 188 | "web\\.whatsapp\\.com" = "Whatsapp" 189 | ``` 190 | 191 | #### Name 192 | 193 | > Sway and i3 194 | 195 | Uses `name` instead of `instance` and `class|app_id`, this option is very 196 | verbose and relies on regex matching of aliases to be of any use. 197 | 198 | A use-case is running some terminal application, and as default i3wsr will only 199 | display class regardless of whats running in the terminal. 200 | 201 | So you could do something like this: 202 | 203 | ```toml 204 | [aliases.name] 205 | ".*mutt$" = "Mutt" 206 | ``` 207 | 208 | ### Display property 209 | 210 | Which property to display if no aliases is found: 211 | 212 | ```toml 213 | [general] 214 | display_property = "instance" 215 | ``` 216 | 217 | Possible options are `class`, `app_id`, `instance`, and `name`, and will default 218 | to `class` or `app_id` depending on display server if not present. 219 | 220 | You can alternatively supply cmd argument: 221 | ```sh 222 | i3wsr --display-property name 223 | ``` 224 | ### Icons 225 | 226 | You can config icons for your WM property, these are defined in your config file. 227 | 228 | ```toml 229 | [icons] 230 | Firefox = "🌍" 231 | 232 | # Use quote when matching anything other than [a-zA-Z] 233 | "Org.gnome.Nautilus" = "📘" 234 | ``` 235 | i3wsr tries to match an icon with an alias first, if none are found it then 236 | checks your `display_property`, and tries to match an icon with a non aliased 237 | `display_property`, lastly it will try to match on class. 238 | 239 | ```toml 240 | [aliases.class] 241 | "Gimp-\\d\\.\\d\\d" = "Gimp" 242 | 243 | [icons] 244 | Gimp = "📄" 245 | ``` 246 | 247 | A font that provides icons is of course recommended, like 248 | [font-awesome](https://fontawesome.com/). Make sure your bar has that font 249 | configured. 250 | 251 | ### Separator 252 | 253 | Normally i3wsr uses the pipe character `|` between class names in a workspace, 254 | but a custom separator can be configured in the config file: 255 | ```toml 256 | [general] 257 | separator = "  " 258 | ``` 259 | 260 | ### Default icon 261 | To use a default icon when no other is defined use: 262 | ```toml 263 | [general] 264 | default_icon = "💀" 265 | ``` 266 | ### Empty label 267 | 268 | Set a label for empty workspaces. 269 | 270 | ```toml 271 | [general] 272 | empty_label = "🌕" 273 | ``` 274 | ### No icon names 275 | To display names only if icon is not available, you can use the 276 | `--no-icon-names` flag, or enable it in your config file like so: 277 | ```toml 278 | [options] 279 | no_icon_names = true 280 | ``` 281 | ### No names 282 | If you don't want i3wsr to display names at all, you can use the 283 | `--no-names` flag, or enable it in your config file like so: 284 | ```toml 285 | [options] 286 | no_names = true 287 | ``` 288 | 289 | ### Remove duplicates 290 | If you want duplicates removed from workspaces use either the flag 291 | `--remove-duplicates`, or configure it in the `options` section of the config 292 | file: 293 | ```toml 294 | [options] 295 | remove_duplicates = true 296 | ``` 297 | 298 | ### Split at character 299 | 300 | By default i3wsr will keep everything until the first `space` character is found, 301 | then replace the remainder with titles. 302 | 303 | If you want to define a different character that is used to split the 304 | numbered/constant part of the workspace and the dynamic content, you can use 305 | the option `--split-at [CHAR]` 306 | 307 | ```toml 308 | [general] 309 | split_at = ":" 310 | ``` 311 | 312 | Here we define colon as the split character, which results in i3wsr only 313 | keeping the numbered part of a workspace name when renaming. 314 | 315 | This can give a cleaner config, but I've kept the old behavior as default. 316 | 317 | ## Testing 318 | 319 | To run unit tests use `cargo test --lib`, to run the full test suite locally 320 | use the [Containerfile](./Containerfile) with [Podman](https://podman.io/) for 321 | example: 322 | 323 | ```sh 324 | # cd project root 325 | podman build -t i3wsr-test . 326 | podman run -t --name i3wsr-test i3wsr-test:latest 327 | ``` 328 | 329 | ## License 330 | 331 | [MIT](./LICENSE) 332 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! # i3wsr - i3/Sway Workspace Renamer 2 | //! 3 | //! 4 | //! A dynamic workspace renamer for i3 and Sway that updates names to reflect their 5 | //! active applications. 6 | //! 7 | //! ## Usage 8 | //! 9 | //! 1. Install using cargo: 10 | //! ```bash 11 | //! cargo install i3wsr 12 | //! ``` 13 | //! 14 | //! 2. Add to your i3/Sway config: 15 | //! ``` 16 | //! exec_always --no-startup-id i3wsr 17 | //! ``` 18 | //! 19 | //! 3. Ensure numbered workspaces in i3/Sway config: 20 | //! ``` 21 | //! bindsym $mod+1 workspace number 1 22 | //! assign [class="(?i)firefox"] number 1 23 | //! ``` 24 | //! 25 | //! ## Configuration 26 | //! 27 | //! Configuration can be done via: 28 | //! - Command line arguments 29 | //! - TOML configuration file (default: `$XDG_CONFIG_HOME/i3wsr/config.toml`) 30 | //! 31 | //! ### Config File Sections: 32 | //! 33 | //! ```toml 34 | //! [icons] 35 | //! # Map window classes to icons 36 | //! Firefox = "🌍" 37 | //! default_icon = "💻" 38 | //! 39 | //! [aliases.app_id] 40 | //! "^firefox$" = "Firefox" 41 | //! 42 | //! [aliases.class] 43 | //! # Map window classes to friendly names 44 | //! "Google-chrome" = "Chrome" 45 | //! 46 | //! [aliases.instance] 47 | //! # Map window instances to friendly names 48 | //! "web.whatsapp.com" = "WhatsApp" 49 | //! 50 | //! [aliases.name] 51 | //! # Map window names using regex 52 | //! ".*mutt$" = "Mail" 53 | //! 54 | //! [general] 55 | //! separator = " | " # Separator between window names 56 | //! split_at = ":" # Character to split workspace number 57 | //! empty_label = "🌕" # Label for empty workspaces 58 | //! display_property = "class" # Default property to display (class/app_id/instance/name) 59 | //! 60 | //! [options] 61 | //! remove_duplicates = false # Remove duplicate window names 62 | //! no_names = false # Show only icons 63 | //! no_icon_names = false # Show names only if no icon available 64 | //! focus_fix = false # Enable experimental focus fix, see #34 for more. Ignore if you don't know you need this. 65 | //! ``` 66 | //! 67 | //! ### Command Line Options: 68 | //! 69 | //! - `--verbose`: Enable detailed logging 70 | //! - `--config `: Use alternative config file 71 | //! - `--no-icon-names`: Show only icons when available 72 | //! - `--no-names`: Never show window names 73 | //! - `--remove-duplicates`: Remove duplicate entries 74 | //! - `--display-property `: Window property to use (class/app_id/instance/name) 75 | //! - `--split-at `: Character to split workspace names 76 | //! 77 | //! ### Window Properties: 78 | //! 79 | //! Three window properties can be used for naming: 80 | //! - `class`: Default, most stable (WM_CLASS) 81 | //! - `app_id`: In place of class only for sway/wayland 82 | //! - `instance`: More specific than class (WM_INSTANCE) 83 | //! - `name`: Most detailed but volatile (WM_NAME) 84 | //! 85 | //! Properties are checked in order: name -> instance -> class/app_id 86 | //! 87 | //! ### Special Features: 88 | //! 89 | //! - Regex support in aliases 90 | //! - Custom icons per window 91 | //! - Default icons 92 | //! - Empty workspace labels 93 | //! - Duplicate removal 94 | //! - Custom separators 95 | //! 96 | //! For more details, see the [README](https://github.com/roosta/i3wsr) 97 | 98 | use clap::{Parser, ValueEnum}; 99 | use dirs::config_dir; 100 | use i3wsr_core::config::{Config, ConfigError}; 101 | use std::io; 102 | use std::path::Path; 103 | use swayipc::{Connection, Event, EventType, Fallible, WorkspaceChange}; 104 | use std::env; 105 | 106 | use i3wsr_core::AppError; 107 | 108 | /// Window property types that can be used for workspace naming. 109 | /// 110 | /// These properties determine which window attribute is used when displaying 111 | /// window names in workspaces: 112 | /// - `Class`: Uses WM_CLASS (default, most stable) 113 | /// - `Instance`: Uses WM_INSTANCE (more specific than class) 114 | /// - `Name`: Uses WM_NAME (most detailed but volatile) 115 | /// - `AppId`: In place of class only for sway/wayland 116 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 117 | enum Properties { 118 | Class, 119 | Instance, 120 | Name, 121 | AppId 122 | } 123 | 124 | impl Properties { 125 | fn as_str(&self) -> &'static str { 126 | match self { 127 | Properties::Class => "class", 128 | Properties::Instance => "instance", 129 | Properties::Name => "name", 130 | Properties::AppId => "app_id", 131 | } 132 | } 133 | } 134 | 135 | /// Command line arguments for i3wsr 136 | /// 137 | /// Configuration can be provided either through command line arguments 138 | /// or through a TOML configuration file. Command line arguments take 139 | /// precedence over configuration file settings. 140 | #[derive(Parser, Debug)] 141 | #[command( 142 | author, 143 | version, 144 | about = "Dynamic workspace renamer for i3 and Sway window managers" 145 | )] 146 | #[command( 147 | long_about = "Automatically renames workspaces based on their window contents. \ 148 | Supports custom icons, aliases, and various display options. \ 149 | Can be configured via command line flags or a TOML configuration file." 150 | )] 151 | struct Args { 152 | /// Enable verbose logging of events and operations 153 | #[arg( 154 | short, 155 | long, 156 | help = "Print detailed information about events and operations" 157 | )] 158 | verbose: bool, 159 | 160 | #[arg( 161 | long, 162 | help = "Enable experimental focus fix, see #34 for more. Ignore if you don't know you need this." 163 | )] 164 | focus_fix: bool, 165 | 166 | /// Deprecated: Icon set option (maintained for backwards compatibility) 167 | #[arg( 168 | long, 169 | value_name = "SET", 170 | help = "[DEPRECATED] Icon set selection - will be removed in future versions" 171 | )] 172 | icons: Option, 173 | /// Path to TOML configuration file 174 | #[arg( 175 | short, 176 | long, 177 | help = "Path to TOML config file (default: $XDG_CONFIG_HOME/i3wsr/config.toml)", 178 | value_name = "FILE" 179 | )] 180 | config: Option, 181 | 182 | /// Display only icon (if available) otherwise display name 183 | #[arg( 184 | short = 'm', 185 | long, 186 | help = "Show only icons when available, fallback to names otherwise" 187 | )] 188 | no_icon_names: bool, 189 | 190 | /// Do not display window names, only show icons 191 | #[arg(short, long, help = "Show only icons, never display window names")] 192 | no_names: bool, 193 | 194 | /// Remove duplicate entries in workspace names 195 | #[arg( 196 | short, 197 | long, 198 | help = "Remove duplicate window names from workspace labels" 199 | )] 200 | remove_duplicates: bool, 201 | 202 | /// Which window property to use when no alias is found 203 | #[arg( 204 | short = 'p', 205 | long, 206 | value_enum, 207 | help = "Window property to use for naming (class/instance/name)", 208 | value_name = "PROPERTY" 209 | )] 210 | display_property: Option, 211 | 212 | /// Character used to split the workspace title string 213 | #[arg( 214 | short = 'a', 215 | long, 216 | help = "Character that separates workspace number from window names", 217 | value_name = "CHAR" 218 | )] 219 | split_at: Option, 220 | } 221 | 222 | /// Loads configuration from a TOML file or creates default configuration 223 | fn load_config(config_path: Option<&str>) -> Result { 224 | let xdg_config = config_dir() 225 | .ok_or_else(|| { 226 | ConfigError::IoError(io::Error::new( 227 | io::ErrorKind::NotFound, 228 | "Could not determine config directory", 229 | )) 230 | })? 231 | .join("i3wsr/config.toml"); 232 | 233 | match config_path { 234 | Some(path) => { 235 | println!("Loading config from: {path}"); 236 | Config::new(Path::new(path)) 237 | } 238 | None => { 239 | if xdg_config.exists() { 240 | Config::new(&xdg_config) 241 | } else { 242 | Ok(Config { 243 | ..Default::default() 244 | }) 245 | } 246 | } 247 | } 248 | } 249 | 250 | /// Applies command line arguments to configuration 251 | fn apply_args_to_config(config: &mut Config, args: &Args) { 252 | // Apply boolean options 253 | let options = [ 254 | ("no_icon_names", args.no_icon_names), 255 | ("no_names", args.no_names), 256 | ("remove_duplicates", args.remove_duplicates), 257 | ("focus_fix", args.focus_fix), 258 | ]; 259 | 260 | for (key, value) in options { 261 | if value { 262 | config.options.insert(key.to_string(), value); 263 | } 264 | } 265 | 266 | // Apply general settings 267 | if let Some(split_char) = &args.split_at { 268 | config 269 | .general 270 | .insert("split_at".to_string(), split_char.clone()); 271 | } 272 | 273 | if let Some(display_property) = &args.display_property { 274 | config 275 | .general 276 | .insert("display_property".to_string(), display_property.as_str().to_string()); 277 | } 278 | } 279 | 280 | /// Sets up the program by processing arguments and initializing configuration 281 | /// Command line arguments take precedence over configuration file settings. 282 | fn setup() -> Result { 283 | let args = Args::parse(); 284 | 285 | // Handle deprecated --icons option 286 | if let Some(icon_set) = &args.icons { 287 | if icon_set == "awesome" { 288 | eprintln!("Warning: The --icons option is deprecated and will be removed in a future version."); 289 | eprintln!("Icons are now configured via the config file in the [icons] section."); 290 | } else { 291 | eprintln!("Warning: Invalid --icons value '{}'. Only 'awesome' is supported for backwards compatibility.", icon_set); 292 | } 293 | } 294 | 295 | // Set verbose mode if requested 296 | i3wsr_core::VERBOSE.store(args.verbose, std::sync::atomic::Ordering::Relaxed); 297 | 298 | let mut config = load_config(args.config.as_deref())?; 299 | apply_args_to_config(&mut config, &args); 300 | 301 | Ok(config) 302 | } 303 | 304 | /// Processes window manager events and updates workspace names accordingly 305 | fn handle_event( 306 | event: Fallible, 307 | conn: &mut Connection, 308 | config: &Config, 309 | res: &i3wsr_core::regex::Compiled, 310 | ) -> Result<(), AppError> { 311 | match event { 312 | Ok(Event::Window(e)) => { 313 | i3wsr_core::handle_window_event(&e, conn, config, res) 314 | .map_err(|e| AppError::Event(format!("Window event error: {}", e)))?; 315 | } 316 | Ok(Event::Workspace(e)) => { 317 | if e.change == WorkspaceChange::Reload && env::var("SWAYSOCK").is_ok() { 318 | return Err(AppError::Abort(format!("Config reloaded"))); 319 | } 320 | i3wsr_core::handle_ws_event(&e, conn, config, res) 321 | .map_err(|e| AppError::Event(format!("Workspace event error: {}", e)))?; 322 | } 323 | Ok(_) => {} 324 | Err(e) => { 325 | // Check if it's an UnexpectedEof error (common when i3/sway restarts) 326 | if let swayipc::Error::Io(io_err) = &e { 327 | if io_err.kind() == std::io::ErrorKind::UnexpectedEof { 328 | return Err(AppError::Abort("Window manager connection lost (EOF), shutting down...".to_string())); 329 | } 330 | } 331 | return Err(AppError::Event(format!("IPC event error: {}", e))); 332 | } 333 | } 334 | Ok(()) 335 | } 336 | 337 | /// Main event loop that monitors window manager events 338 | /// The program will continue running and handling events until 339 | /// interrupted or an unrecoverable error occurs. 340 | fn run() -> Result<(), AppError> { 341 | let config = setup()?; 342 | let res = i3wsr_core::regex::parse_config(&config)?; 343 | 344 | let mut conn = Connection::new()?; 345 | let subscriptions = [EventType::Window, EventType::Workspace]; 346 | 347 | i3wsr_core::update_tree(&mut conn, &config, &res, false) 348 | .map_err(|e| AppError::Event(format!("Initial tree update failed: {}", e)))?; 349 | 350 | let event_connection = Connection::new()?; 351 | let events = event_connection.subscribe(&subscriptions)?; 352 | 353 | println!("Started successfully. Listening for events..."); 354 | 355 | for event in events { 356 | if let Err(e) = handle_event(event, &mut conn, &config, &res) { 357 | match &e { 358 | // Exit program on abort, this is because when config gets reloaded, we want the 359 | // old process to exit, letting sway start a new one. 360 | AppError::Abort(_) => { 361 | return Err(e); 362 | } 363 | // Continue running despite errors 364 | _ => eprintln!("Error handling event: {}", e), 365 | } 366 | } 367 | } 368 | 369 | Ok(()) 370 | } 371 | 372 | fn main() { 373 | if let Err(e) = run() { 374 | eprintln!("Fatal error: {}", e); 375 | std::process::exit(1); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # i3wsr - i3/Sway Workspace Renamer 2 | //! 3 | //! Internal library functionality for the i3wsr binary. This crate provides the core functionality 4 | //! for renaming i3/Sway workspaces based on their content. 5 | //! 6 | //! ## Note 7 | //! 8 | //! This is primarily a binary crate. The public functions and types are mainly exposed for: 9 | //! - Use by the binary executable 10 | //! - Testing purposes 11 | //! - Internal organization 12 | //! 13 | //! While you could technically use this as a library, it's not designed or maintained for that purpose. 14 | use itertools::Itertools; 15 | use swayipc::{ 16 | Connection, Node, NodeType, WindowChange, WindowEvent, WorkspaceChange, WorkspaceEvent, 17 | }; 18 | extern crate colored; 19 | use colored::Colorize; 20 | 21 | pub mod config; 22 | pub mod regex; 23 | 24 | pub use config::Config; 25 | use std::error::Error; 26 | use std::fmt; 27 | use std::io; 28 | use std::sync::atomic::{AtomicBool, Ordering}; 29 | 30 | /// Global flag to control debug output verbosity. 31 | /// 32 | /// This flag is atomic to allow safe concurrent access without requiring mutex locks. 33 | /// It's primarily used by the binary to enable/disable detailed logging of events 34 | /// and commands. 35 | /// 36 | /// # Usage 37 | /// 38 | /// ```rust 39 | /// use std::sync::atomic::Ordering; 40 | /// 41 | /// // Enable verbose output 42 | /// i3wsr_core::VERBOSE.store(true, Ordering::Relaxed); 43 | /// 44 | /// // Check if verbose is enabled 45 | /// if i3wsr_core::VERBOSE.load(Ordering::Relaxed) { 46 | /// println!("Verbose output enabled"); 47 | /// } 48 | /// ``` 49 | pub static VERBOSE: AtomicBool = AtomicBool::new(false); 50 | 51 | #[derive(Debug)] 52 | pub enum AppError { 53 | Config(config::ConfigError), 54 | Connection(swayipc::Error), 55 | Regex(regex::RegexError), 56 | Event(String), 57 | IoError(io::Error), 58 | Abort(String), 59 | } 60 | 61 | impl fmt::Display for AppError { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | match self { 64 | AppError::Config(e) => write!(f, "Configuration error: {}", e), 65 | AppError::Connection(e) => write!(f, "IPC connection error: {}", e), 66 | AppError::Regex(e) => write!(f, "Regex compilation error: {}", e), 67 | AppError::Event(e) => write!(f, "Event handling error: {}", e), 68 | AppError::IoError(e) => write!(f, "IO error: {}", e), 69 | AppError::Abort(e) => write!(f, "Abort signal, stopping program: {}", e), 70 | } 71 | } 72 | } 73 | 74 | impl Error for AppError {} 75 | 76 | impl From for AppError { 77 | fn from(err: config::ConfigError) -> Self { 78 | AppError::Config(err) 79 | } 80 | } 81 | 82 | impl From for AppError { 83 | fn from(err: swayipc::Error) -> Self { 84 | AppError::Connection(err) 85 | } 86 | } 87 | 88 | impl From for AppError { 89 | fn from(err: regex::RegexError) -> Self { 90 | AppError::Regex(err) 91 | } 92 | } 93 | 94 | impl From for AppError { 95 | fn from(err: io::Error) -> Self { 96 | AppError::IoError(err) 97 | } 98 | } 99 | 100 | /// Helper fn to get options via config 101 | fn get_option(config: &Config, key: &str) -> bool { 102 | config.get_option(key).unwrap_or(false) 103 | } 104 | 105 | fn resolve_alias(value: Option<&String>, patterns: &[(regex::Regex, String)]) -> Option { 106 | value.and_then(|val| { 107 | patterns 108 | .iter() 109 | .find(|(re, _)| re.is_match(val)) 110 | .map(|(re, alias_format)| { 111 | re.replace(val, alias_format).into_owned() 112 | }) 113 | }) 114 | } 115 | 116 | fn format_with_icon(icon: &str, title: &str, no_names: bool, no_icon_names: bool) -> String { 117 | if no_icon_names || no_names { 118 | icon.to_string() 119 | } else { 120 | format!("{} {}", icon, title) 121 | } 122 | } 123 | 124 | /// Gets a window title by trying to find an alias for the window, eventually falling back on 125 | /// class, or app_id, depending on platform. 126 | pub fn get_title( 127 | node: &Node, 128 | config: &Config, 129 | res: ®ex::Compiled, 130 | ) -> Result> { 131 | let display_prop = config 132 | .get_general("display_property") 133 | .unwrap_or_else(|| "class".to_string()); 134 | 135 | let title = match &node.window_properties { 136 | // Xwayland / Xorg 137 | Some(props) => { 138 | // First try to find an alias using the window properties 139 | let alias = resolve_alias(props.title.as_ref(), &res.name) 140 | .or_else(|| resolve_alias(props.instance.as_ref(), &res.instance)) 141 | .or_else(|| resolve_alias(props.class.as_ref(), &res.class)); 142 | 143 | // If no alias found, use the configured display property 144 | let title = alias.or_else(|| { 145 | let prop_value = match display_prop.as_str() { 146 | "name" => props.title.clone(), 147 | "instance" => props.instance.clone(), 148 | _ => props.class.clone(), 149 | }; 150 | prop_value 151 | }); 152 | 153 | title.ok_or_else(|| { 154 | format!( 155 | "No title found: tried aliases and display_prop '{}'", 156 | display_prop 157 | ) 158 | })? 159 | } 160 | // Wayland 161 | None => { 162 | let alias = resolve_alias(node.name.as_ref(), &res.name) 163 | .or_else(|| resolve_alias(node.app_id.as_ref(), &res.app_id)); 164 | 165 | let title = alias.or_else(|| { 166 | let prop_value = match display_prop.as_str() { 167 | "name" => node.name.clone(), 168 | _ => node.app_id.clone(), 169 | }; 170 | prop_value 171 | }); 172 | title.ok_or_else(|| { 173 | format!( 174 | "No title found: tried aliases and display_prop '{}'", 175 | display_prop 176 | ) 177 | })? 178 | } 179 | }; 180 | 181 | // Try to find an alias first 182 | let no_names = get_option(config, "no_names"); 183 | let no_icon_names = get_option(config, "no_icon_names"); 184 | 185 | Ok(if let Some(icon) = config.get_icon(&title) { 186 | format_with_icon(&icon, &title, no_names, no_icon_names) 187 | } else if let Some(default_icon) = config.get_general("default_icon") { 188 | format_with_icon(&default_icon, &title, no_names, no_icon_names) 189 | } else if no_names { 190 | String::new() 191 | } else { 192 | title 193 | }) 194 | } 195 | 196 | /// Filters out special workspaces (like scratchpad) and collects regular workspaces 197 | /// from the window manager tree structure. 198 | pub fn get_workspaces(tree: Node) -> Vec { 199 | let excludes = ["__i3_scratch", "__sway_scratch"]; 200 | 201 | // Helper function to recursively find workspaces in a node 202 | fn find_workspaces(node: Node, excludes: &[&str]) -> Vec { 203 | let mut workspaces = Vec::new(); 204 | 205 | // If this is a workspace node that's not excluded, add it 206 | if matches!(node.node_type, NodeType::Workspace) { 207 | if let Some(name) = &node.name { 208 | if !excludes.contains(&name.as_str()) { 209 | workspaces.push(node.clone()); 210 | } 211 | } 212 | } 213 | 214 | // Recursively check child nodes 215 | for child in node.nodes { 216 | workspaces.extend(find_workspaces(child, excludes)); 217 | } 218 | 219 | workspaces 220 | } 221 | 222 | // Start the recursive search from the root 223 | find_workspaces(tree, &excludes) 224 | } 225 | 226 | /// Collect a vector of workspace titles, recursively traversing all nested nodes 227 | pub fn collect_titles(workspace: &Node, config: &Config, res: ®ex::Compiled) -> Vec { 228 | fn collect_nodes<'a>(node: &'a Node, nodes: &mut Vec<&'a Node>) { 229 | // Add the current node if it has window properties or app_id 230 | if node.window_properties.is_some() || node.app_id.is_some() { 231 | nodes.push(node); 232 | } 233 | 234 | // Recursively collect from regular nodes 235 | for child in &node.nodes { 236 | collect_nodes(child, nodes); 237 | } 238 | 239 | // Recursively collect from floating nodes 240 | for child in &node.floating_nodes { 241 | collect_nodes(child, nodes); 242 | } 243 | } 244 | 245 | let mut all_nodes = Vec::new(); 246 | collect_nodes(workspace, &mut all_nodes); 247 | 248 | let mut titles = Vec::new(); 249 | for node in all_nodes { 250 | let title = match get_title(node, config, res) { 251 | Ok(title) => title, 252 | Err(e) => { 253 | eprintln!("get_title error: \"{}\" for workspace {:#?}", e, workspace); 254 | continue; 255 | } 256 | }; 257 | titles.push(title); 258 | } 259 | 260 | titles 261 | } 262 | 263 | /// Applies options on titles, like remove duplicates 264 | fn apply_options(titles: Vec, config: &Config) -> Vec { 265 | let mut processed = titles; 266 | 267 | if get_option(config, "remove_duplicates") { 268 | processed = processed.into_iter().unique().collect(); 269 | } 270 | 271 | if get_option(config, "no_names") { 272 | processed = processed.into_iter().filter(|s| !s.is_empty()).collect(); 273 | } 274 | 275 | processed 276 | } 277 | 278 | fn get_split_char(config: &Config) -> char { 279 | config 280 | .get_general("split_at") 281 | .and_then(|s| if s.is_empty() { None } else { s.chars().next() }) 282 | .unwrap_or(' ') 283 | } 284 | 285 | fn escape_string(s: &str) -> String { 286 | s.replace('\\', "\\\\").replace('"', "\\\"") 287 | } 288 | 289 | fn format_workspace_name(initial: &str, titles: &str, split_at: char, config: &Config) -> String { 290 | let mut new = String::from(initial); 291 | 292 | // Add colon if needed 293 | if split_at == ':' && !initial.is_empty() && !titles.is_empty() { 294 | new.push(':'); 295 | } 296 | 297 | // Add titles if present 298 | if !titles.is_empty() { 299 | new.push_str(titles); 300 | } else if let Some(empty_label) = config.get_general("empty_label") { 301 | new.push(' '); 302 | new.push_str(&empty_label); 303 | } 304 | 305 | new 306 | } 307 | 308 | /// Internal function to update all workspace names based on their current content. 309 | /// This function is public for testing purposes and binary use only. 310 | /// 311 | /// Update all workspace names in tree 312 | pub fn update_tree( 313 | conn: &mut Connection, 314 | config: &Config, 315 | res: ®ex::Compiled, 316 | focus: bool, 317 | ) -> Result<(), Box> { 318 | let tree = conn.get_tree()?; 319 | let separator = config 320 | .get_general("separator") 321 | .unwrap_or_else(|| " | ".to_string()); 322 | let split_at = get_split_char(config); 323 | 324 | for workspace in get_workspaces(tree) { 325 | // Get the old workspace name 326 | let old = workspace.name.as_ref().ok_or_else(|| { 327 | format!( 328 | "Failed to get workspace name for workspace: {:#?}", 329 | workspace 330 | ) 331 | })?; 332 | 333 | // Process titles 334 | let titles = collect_titles(&workspace, config, res); 335 | let titles = apply_options(titles, config); 336 | let titles = if !titles.is_empty() { 337 | format!(" {}", titles.join(&separator)) 338 | } else { 339 | String::new() 340 | }; 341 | 342 | // Get initial part of workspace name 343 | let initial = old.split(split_at).next().unwrap_or(""); 344 | 345 | // Format new workspace name 346 | let new = format_workspace_name(initial, &titles, split_at, config); 347 | 348 | // Only send command if name changed 349 | if old != &new { 350 | let command = format!( 351 | "rename workspace \"{}\" to \"{}\"", 352 | escape_string(old), 353 | escape_string(&new) 354 | ); 355 | if VERBOSE.load(Ordering::Relaxed) { 356 | println!("{} {}", "[COMMAND]".blue(), command); 357 | if let Some(output) = &workspace.output { 358 | println!("{} Workspace on output: {}", "[INFO]".cyan(), output); 359 | } 360 | } 361 | 362 | // Focus on flag, fix for moving floating windows across multiple monitors 363 | if focus { 364 | let focus_cmd = format!("workspace \"{}\"", old); 365 | conn.run_command(&focus_cmd)?; 366 | } 367 | 368 | // Then rename it 369 | conn.run_command(&command)?; 370 | } 371 | } 372 | Ok(()) 373 | } 374 | 375 | /// Processes various window events (new, close, move, title changes) and updates 376 | /// workspace names accordingly. This is a core part of the event loop in the main binary. 377 | pub fn handle_window_event( 378 | e: &WindowEvent, 379 | conn: &mut Connection, 380 | config: &Config, 381 | res: ®ex::Compiled, 382 | ) -> Result<(), AppError> { 383 | if VERBOSE.load(Ordering::Relaxed) { 384 | println!( 385 | "{} Change: {:?}, Container: {:?}", 386 | "[WINDOW EVENT]".yellow(), 387 | e.change, 388 | e.container 389 | ); 390 | } 391 | match e.change { 392 | WindowChange::New 393 | | WindowChange::Close 394 | | WindowChange::Move 395 | | WindowChange::Title 396 | | WindowChange::Floating => { 397 | update_tree(conn, config, res, false) 398 | .map_err(|e| AppError::Event(format!("Tree update failed: {}", e)))?; 399 | } 400 | _ => (), 401 | } 402 | Ok(()) 403 | } 404 | 405 | /// Processes workspace events (empty, focus changes) and updates workspace names 406 | /// as needed. This is a core part of the event loop in the main binary. 407 | pub fn handle_ws_event( 408 | e: &WorkspaceEvent, 409 | conn: &mut Connection, 410 | config: &Config, 411 | res: ®ex::Compiled, 412 | ) -> Result<(), AppError> { 413 | if VERBOSE.load(Ordering::Relaxed) { 414 | println!( 415 | "{} Change: {:?}, Current: {:?}, Old: {:?}", 416 | "[WORKSPACE EVENT]".green(), 417 | e.change, 418 | e.current, 419 | e.old 420 | ); 421 | } 422 | 423 | let focus_fix = get_option(config, "focus_fix"); 424 | 425 | match e.change { 426 | WorkspaceChange::Empty | WorkspaceChange::Focus => { 427 | update_tree(conn, config, res, e.change == WorkspaceChange::Focus && focus_fix) 428 | .map_err(|e| AppError::Event(format!("Tree update failed: {}", e)))?; 429 | } 430 | _ => (), 431 | } 432 | Ok(()) 433 | } 434 | 435 | #[cfg(test)] 436 | mod tests { 437 | use regex::Regex; 438 | 439 | #[test] 440 | fn test_resolve_alias() { 441 | let patterns = vec![ 442 | (Regex::new(r"Firefox").unwrap(), "firefox".to_string()), 443 | (Regex::new(r"Chrome").unwrap(), "chrome".to_string()), 444 | ]; 445 | 446 | // Test matching case 447 | let binding = "Firefox".to_string(); 448 | let value = Some(&binding); 449 | assert_eq!( 450 | super::resolve_alias(value, &patterns), 451 | Some("firefox".to_string()) 452 | ); 453 | 454 | // Test non-matching case 455 | let binding = "Safari".to_string(); 456 | let value = Some(&binding); 457 | assert_eq!(super::resolve_alias(value, &patterns), None); 458 | 459 | // Test None case 460 | let value: Option<&String> = None; 461 | assert_eq!(super::resolve_alias(value, &patterns), None); 462 | } 463 | 464 | #[test] 465 | fn test_format_with_icon() { 466 | let icon = "🦊"; 467 | let title = "Firefox"; 468 | 469 | // Test normal case 470 | assert_eq!( 471 | super::format_with_icon(&icon, title, false, false), 472 | "🦊 Firefox" 473 | ); 474 | 475 | // Test no_names = true 476 | assert_eq!(super::format_with_icon(&icon, title, true, false), "🦊"); 477 | 478 | // Test no_icon_names = true 479 | assert_eq!(super::format_with_icon(&icon, title, false, true), "🦊"); 480 | 481 | // Test both flags true 482 | assert_eq!(super::format_with_icon(&icon, title, true, true), "🦊"); 483 | } 484 | 485 | #[test] 486 | fn test_get_split_char() { 487 | let mut config = super::Config::default(); 488 | 489 | // Test default (space) 490 | assert_eq!(super::get_split_char(&config), ' '); 491 | 492 | // Test with custom split char 493 | config.set_general("split_at".to_string(), ":".to_string()); 494 | assert_eq!(super::get_split_char(&config), ':'); 495 | 496 | // Test with empty string 497 | config.set_general("split_at".to_string(), "".to_string()); 498 | assert_eq!(super::get_split_char(&config), ' '); 499 | } 500 | 501 | #[test] 502 | fn test_escape_string() { 503 | assert_eq!(super::escape_string("normal"), "normal"); 504 | assert_eq!(super::escape_string("quote \""), "quote \\\""); 505 | assert_eq!(super::escape_string("backslash \\"), "backslash \\\\"); 506 | assert_eq!(super::escape_string("both \" \\"), "both \\\" \\\\"); 507 | } 508 | 509 | #[test] 510 | fn test_format_workspace_name() { 511 | let mut config = super::Config::default(); 512 | 513 | // Test normal case with space 514 | assert_eq!( 515 | super::format_workspace_name("1", " Firefox Chrome", ' ', &config), 516 | "1 Firefox Chrome" 517 | ); 518 | 519 | // Test with colon separator 520 | assert_eq!( 521 | super::format_workspace_name("1", " Firefox Chrome", ':', &config), 522 | "1: Firefox Chrome" 523 | ); 524 | 525 | // Test empty titles with no empty_label 526 | assert_eq!(super::format_workspace_name("1", "", ':', &config), "1"); 527 | 528 | // Test empty titles with empty_label 529 | config.set_general("empty_label".to_string(), "Empty".to_string()); 530 | assert_eq!( 531 | super::format_workspace_name("1", "", ':', &config), 532 | "1 Empty" 533 | ); 534 | 535 | // Test empty initial 536 | assert_eq!( 537 | super::format_workspace_name("", " Firefox Chrome", ':', &config), 538 | " Firefox Chrome" 539 | ); 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.20" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.11" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.7" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 49 | dependencies = [ 50 | "windows-sys 0.60.2", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys 0.60.2", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.9.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 75 | 76 | [[package]] 77 | name = "clap" 78 | version = "4.5.43" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" 81 | dependencies = [ 82 | "clap_builder", 83 | "clap_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_builder" 88 | version = "4.5.43" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" 91 | dependencies = [ 92 | "anstream", 93 | "anstyle", 94 | "clap_lex", 95 | "strsim", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_derive" 100 | version = "4.5.41" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" 103 | dependencies = [ 104 | "heck", 105 | "proc-macro2", 106 | "quote", 107 | "syn", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_lex" 112 | version = "0.7.5" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 115 | 116 | [[package]] 117 | name = "colorchoice" 118 | version = "1.0.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 121 | 122 | [[package]] 123 | name = "colored" 124 | version = "2.2.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 127 | dependencies = [ 128 | "lazy_static", 129 | "windows-sys 0.59.0", 130 | ] 131 | 132 | [[package]] 133 | name = "dirs" 134 | version = "5.0.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 137 | dependencies = [ 138 | "dirs-sys", 139 | ] 140 | 141 | [[package]] 142 | name = "dirs-sys" 143 | version = "0.4.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 146 | dependencies = [ 147 | "libc", 148 | "option-ext", 149 | "redox_users", 150 | "windows-sys 0.48.0", 151 | ] 152 | 153 | [[package]] 154 | name = "either" 155 | version = "1.15.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 158 | 159 | [[package]] 160 | name = "equivalent" 161 | version = "1.0.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 164 | 165 | [[package]] 166 | name = "getrandom" 167 | version = "0.2.16" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 170 | dependencies = [ 171 | "cfg-if", 172 | "libc", 173 | "wasi", 174 | ] 175 | 176 | [[package]] 177 | name = "hashbrown" 178 | version = "0.15.5" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 181 | 182 | [[package]] 183 | name = "heck" 184 | version = "0.5.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 187 | 188 | [[package]] 189 | name = "i3wsr" 190 | version = "3.1.2" 191 | dependencies = [ 192 | "clap", 193 | "colored", 194 | "dirs", 195 | "itertools", 196 | "regex", 197 | "serde", 198 | "swayipc", 199 | "thiserror", 200 | "toml", 201 | ] 202 | 203 | [[package]] 204 | name = "indexmap" 205 | version = "2.10.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 208 | dependencies = [ 209 | "equivalent", 210 | "hashbrown", 211 | ] 212 | 213 | [[package]] 214 | name = "is_terminal_polyfill" 215 | version = "1.70.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 218 | 219 | [[package]] 220 | name = "itertools" 221 | version = "0.13.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 224 | dependencies = [ 225 | "either", 226 | ] 227 | 228 | [[package]] 229 | name = "itoa" 230 | version = "1.0.15" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 233 | 234 | [[package]] 235 | name = "lazy_static" 236 | version = "1.5.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 239 | 240 | [[package]] 241 | name = "libc" 242 | version = "0.2.174" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 245 | 246 | [[package]] 247 | name = "libredox" 248 | version = "0.1.9" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 251 | dependencies = [ 252 | "bitflags", 253 | "libc", 254 | ] 255 | 256 | [[package]] 257 | name = "memchr" 258 | version = "2.7.5" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 261 | 262 | [[package]] 263 | name = "once_cell_polyfill" 264 | version = "1.70.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 267 | 268 | [[package]] 269 | name = "option-ext" 270 | version = "0.2.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 273 | 274 | [[package]] 275 | name = "proc-macro2" 276 | version = "1.0.95" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 279 | dependencies = [ 280 | "unicode-ident", 281 | ] 282 | 283 | [[package]] 284 | name = "quote" 285 | version = "1.0.40" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 288 | dependencies = [ 289 | "proc-macro2", 290 | ] 291 | 292 | [[package]] 293 | name = "redox_users" 294 | version = "0.4.6" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 297 | dependencies = [ 298 | "getrandom", 299 | "libredox", 300 | "thiserror", 301 | ] 302 | 303 | [[package]] 304 | name = "regex" 305 | version = "1.11.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 308 | dependencies = [ 309 | "aho-corasick", 310 | "memchr", 311 | "regex-automata", 312 | "regex-syntax", 313 | ] 314 | 315 | [[package]] 316 | name = "regex-automata" 317 | version = "0.4.9" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 320 | dependencies = [ 321 | "aho-corasick", 322 | "memchr", 323 | "regex-syntax", 324 | ] 325 | 326 | [[package]] 327 | name = "regex-syntax" 328 | version = "0.8.5" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 331 | 332 | [[package]] 333 | name = "ryu" 334 | version = "1.0.20" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 337 | 338 | [[package]] 339 | name = "serde" 340 | version = "1.0.219" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 343 | dependencies = [ 344 | "serde_derive", 345 | ] 346 | 347 | [[package]] 348 | name = "serde_derive" 349 | version = "1.0.219" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 352 | dependencies = [ 353 | "proc-macro2", 354 | "quote", 355 | "syn", 356 | ] 357 | 358 | [[package]] 359 | name = "serde_json" 360 | version = "1.0.142" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 363 | dependencies = [ 364 | "itoa", 365 | "memchr", 366 | "ryu", 367 | "serde", 368 | ] 369 | 370 | [[package]] 371 | name = "serde_spanned" 372 | version = "0.6.9" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 375 | dependencies = [ 376 | "serde", 377 | ] 378 | 379 | [[package]] 380 | name = "strsim" 381 | version = "0.11.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 384 | 385 | [[package]] 386 | name = "swayipc" 387 | version = "3.0.3" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "2b8c50cb2e98e88b52066a35ef791fffd8f6fa631c3a4983de18ba41f718c736" 390 | dependencies = [ 391 | "serde", 392 | "serde_json", 393 | "swayipc-types", 394 | ] 395 | 396 | [[package]] 397 | name = "swayipc-types" 398 | version = "1.4.2" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "c4f6205b8f8ea7cd6244d76adce0a0f842525a13c47376feecf04280bda57231" 401 | dependencies = [ 402 | "serde", 403 | "serde_json", 404 | "thiserror", 405 | ] 406 | 407 | [[package]] 408 | name = "syn" 409 | version = "2.0.104" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 412 | dependencies = [ 413 | "proc-macro2", 414 | "quote", 415 | "unicode-ident", 416 | ] 417 | 418 | [[package]] 419 | name = "thiserror" 420 | version = "1.0.69" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 423 | dependencies = [ 424 | "thiserror-impl", 425 | ] 426 | 427 | [[package]] 428 | name = "thiserror-impl" 429 | version = "1.0.69" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "syn", 436 | ] 437 | 438 | [[package]] 439 | name = "toml" 440 | version = "0.7.8" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" 443 | dependencies = [ 444 | "serde", 445 | "serde_spanned", 446 | "toml_datetime", 447 | "toml_edit", 448 | ] 449 | 450 | [[package]] 451 | name = "toml_datetime" 452 | version = "0.6.11" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 455 | dependencies = [ 456 | "serde", 457 | ] 458 | 459 | [[package]] 460 | name = "toml_edit" 461 | version = "0.19.15" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 464 | dependencies = [ 465 | "indexmap", 466 | "serde", 467 | "serde_spanned", 468 | "toml_datetime", 469 | "winnow", 470 | ] 471 | 472 | [[package]] 473 | name = "unicode-ident" 474 | version = "1.0.18" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 477 | 478 | [[package]] 479 | name = "utf8parse" 480 | version = "0.2.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 483 | 484 | [[package]] 485 | name = "wasi" 486 | version = "0.11.1+wasi-snapshot-preview1" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 489 | 490 | [[package]] 491 | name = "windows-link" 492 | version = "0.1.3" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 495 | 496 | [[package]] 497 | name = "windows-sys" 498 | version = "0.48.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 501 | dependencies = [ 502 | "windows-targets 0.48.5", 503 | ] 504 | 505 | [[package]] 506 | name = "windows-sys" 507 | version = "0.59.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 510 | dependencies = [ 511 | "windows-targets 0.52.6", 512 | ] 513 | 514 | [[package]] 515 | name = "windows-sys" 516 | version = "0.60.2" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 519 | dependencies = [ 520 | "windows-targets 0.53.3", 521 | ] 522 | 523 | [[package]] 524 | name = "windows-targets" 525 | version = "0.48.5" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 528 | dependencies = [ 529 | "windows_aarch64_gnullvm 0.48.5", 530 | "windows_aarch64_msvc 0.48.5", 531 | "windows_i686_gnu 0.48.5", 532 | "windows_i686_msvc 0.48.5", 533 | "windows_x86_64_gnu 0.48.5", 534 | "windows_x86_64_gnullvm 0.48.5", 535 | "windows_x86_64_msvc 0.48.5", 536 | ] 537 | 538 | [[package]] 539 | name = "windows-targets" 540 | version = "0.52.6" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 543 | dependencies = [ 544 | "windows_aarch64_gnullvm 0.52.6", 545 | "windows_aarch64_msvc 0.52.6", 546 | "windows_i686_gnu 0.52.6", 547 | "windows_i686_gnullvm 0.52.6", 548 | "windows_i686_msvc 0.52.6", 549 | "windows_x86_64_gnu 0.52.6", 550 | "windows_x86_64_gnullvm 0.52.6", 551 | "windows_x86_64_msvc 0.52.6", 552 | ] 553 | 554 | [[package]] 555 | name = "windows-targets" 556 | version = "0.53.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 559 | dependencies = [ 560 | "windows-link", 561 | "windows_aarch64_gnullvm 0.53.0", 562 | "windows_aarch64_msvc 0.53.0", 563 | "windows_i686_gnu 0.53.0", 564 | "windows_i686_gnullvm 0.53.0", 565 | "windows_i686_msvc 0.53.0", 566 | "windows_x86_64_gnu 0.53.0", 567 | "windows_x86_64_gnullvm 0.53.0", 568 | "windows_x86_64_msvc 0.53.0", 569 | ] 570 | 571 | [[package]] 572 | name = "windows_aarch64_gnullvm" 573 | version = "0.48.5" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 576 | 577 | [[package]] 578 | name = "windows_aarch64_gnullvm" 579 | version = "0.52.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 582 | 583 | [[package]] 584 | name = "windows_aarch64_gnullvm" 585 | version = "0.53.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 588 | 589 | [[package]] 590 | name = "windows_aarch64_msvc" 591 | version = "0.48.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 594 | 595 | [[package]] 596 | name = "windows_aarch64_msvc" 597 | version = "0.52.6" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 600 | 601 | [[package]] 602 | name = "windows_aarch64_msvc" 603 | version = "0.53.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 606 | 607 | [[package]] 608 | name = "windows_i686_gnu" 609 | version = "0.48.5" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 612 | 613 | [[package]] 614 | name = "windows_i686_gnu" 615 | version = "0.52.6" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 618 | 619 | [[package]] 620 | name = "windows_i686_gnu" 621 | version = "0.53.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 624 | 625 | [[package]] 626 | name = "windows_i686_gnullvm" 627 | version = "0.52.6" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 630 | 631 | [[package]] 632 | name = "windows_i686_gnullvm" 633 | version = "0.53.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 636 | 637 | [[package]] 638 | name = "windows_i686_msvc" 639 | version = "0.48.5" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 642 | 643 | [[package]] 644 | name = "windows_i686_msvc" 645 | version = "0.52.6" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 648 | 649 | [[package]] 650 | name = "windows_i686_msvc" 651 | version = "0.53.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 654 | 655 | [[package]] 656 | name = "windows_x86_64_gnu" 657 | version = "0.48.5" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 660 | 661 | [[package]] 662 | name = "windows_x86_64_gnu" 663 | version = "0.52.6" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 666 | 667 | [[package]] 668 | name = "windows_x86_64_gnu" 669 | version = "0.53.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 672 | 673 | [[package]] 674 | name = "windows_x86_64_gnullvm" 675 | version = "0.48.5" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 678 | 679 | [[package]] 680 | name = "windows_x86_64_gnullvm" 681 | version = "0.52.6" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 684 | 685 | [[package]] 686 | name = "windows_x86_64_gnullvm" 687 | version = "0.53.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 690 | 691 | [[package]] 692 | name = "windows_x86_64_msvc" 693 | version = "0.48.5" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 696 | 697 | [[package]] 698 | name = "windows_x86_64_msvc" 699 | version = "0.52.6" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 702 | 703 | [[package]] 704 | name = "windows_x86_64_msvc" 705 | version = "0.53.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 708 | 709 | [[package]] 710 | name = "winnow" 711 | version = "0.5.40" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 714 | dependencies = [ 715 | "memchr", 716 | ] 717 | --------------------------------------------------------------------------------