├── .direnv └── bin │ └── nix-direnv-reload ├── .envrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── cargo-doc-check.yml │ ├── clippy-lint.yml │ ├── coverage.yml │ ├── publish.yml │ ├── release.yml │ └── test.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── Makefile ├── README.md ├── config.toml.example ├── contrib └── generate_icons.py ├── flake.lock ├── flake.nix ├── hyprland-autoname-workspaces.service ├── pull_request_template.md └── src ├── config └── mod.rs ├── main.rs ├── params └── mod.rs └── renamer ├── formatter.rs ├── icon.rs ├── macros.rs └── mod.rs /.direnv/bin/nix-direnv-reload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | if [[ ! -d "/home/cyril/personal/hyprland-autoname-workspaces" ]]; then 4 | echo "Cannot find source directory; Did you move it?" 5 | echo "(Looking for "/home/cyril/personal/hyprland-autoname-workspaces")" 6 | echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' 7 | exit 1 8 | fi 9 | 10 | # rebuild the cache forcefully 11 | _nix_direnv_force_reload=1 direnv exec "/home/cyril/personal/hyprland-autoname-workspaces" true 12 | 13 | # Update the mtime for .envrc. 14 | # This will cause direnv to reload again - but without re-building. 15 | touch "/home/cyril/personal/hyprland-autoname-workspaces/.envrc" 16 | 17 | # Also update the timestamp of whatever profile_rc we have. 18 | # This makes sure that we know we are up to date. 19 | touch -r "/home/cyril/personal/hyprland-autoname-workspaces/.envrc" "/home/cyril/personal/hyprland-autoname-workspaces/.direnv"/*.rc 20 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export NIX_CONFIG="extra-experimental-features = nix-command flakes" 2 | use flake 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cyrinux, maximbaz] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Program version?** 13 | 14 | - Run `hyprland-autoname-workspaces --version` 15 | 16 | **Program configuration dump? (ideally)** 17 | 18 | - Run `hyprland-autoname-workspaces --dump` 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Linux Distro (please complete the following information):** 32 | 33 | - [e.g. Arch] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Rust Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions-rs/toolchain@v1 20 | name: Install rust toolchain 21 | with: 22 | toolchain: stable 23 | override: true 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | name: Add caching 27 | 28 | - uses: actions-rs/cargo@v1 29 | name: Build crate 30 | with: 31 | command: build 32 | args: --verbose --all-features 33 | -------------------------------------------------------------------------------- /.github/workflows/cargo-doc-check.yml: -------------------------------------------------------------------------------- 1 | name: Check Docs 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | rustdoc: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions-rs/toolchain@v1 20 | name: Install rust toolchain 21 | with: 22 | toolchain: stable 23 | override: true 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | name: Add caching 27 | 28 | - uses: actions-rs/cargo@v1 29 | name: Check Documentation with Rustdoc 30 | with: 31 | command: doc 32 | args: --verbose --no-deps 33 | -------------------------------------------------------------------------------- /.github/workflows/clippy-lint.yml: -------------------------------------------------------------------------------- 1 | name: Clippy Lint and Check 2 | on: [pull_request] 3 | env: 4 | CARGO_TERM_COLOR: always 5 | jobs: 6 | clippy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: actions-rs/toolchain@v1 12 | name: Install Rust Toolchain 13 | with: 14 | toolchain: stable 15 | components: clippy 16 | override: true 17 | 18 | - uses: Swatinem/rust-cache@v2 19 | name: Add caching 20 | 21 | - uses: actions-rs/clippy-check@v1 22 | name: Lint and Check codebase with Clippy 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | args: --all-features --verbose 26 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: [push] 4 | jobs: 5 | test: 6 | name: coverage 7 | runs-on: ubuntu-latest 8 | container: 9 | image: xd009642/tarpaulin:develop-nightly 10 | options: --security-opt seccomp=unconfined 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Generate code coverage 16 | run: | 17 | cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out xml 18 | 19 | - name: Upload to codecov.io 20 | uses: codecov/codecov-action@v3 21 | with: 22 | fail_ci_if_error: true 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Pattern matched against refs/tags 4 | tags: 5 | - "*" # Push events to every tag not containing / 6 | workflow_dispatch: 7 | 8 | name: Publish 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | - uses: katyo/publish-crates@v2 21 | with: 22 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 23 | ignore-unpublished-changes: true 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # CI that: 2 | # 3 | # * checks for a Git Tag that looks like a release 4 | # * creates a Github Release™ and fills in its text 5 | # * builds artifacts with cargo-dist (executable-zips, installers) 6 | # * uploads those artifacts to the Github Release™ 7 | # 8 | # Note that the Github Release™ will be created before the artifacts, 9 | # so there will be a few minutes where the release has no artifacts 10 | # and then they will slowly trickle in, possibly failing. To make 11 | # this more pleasant we mark the release as a "draft" until all 12 | # artifacts have been successfully uploaded. This allows you to 13 | # choose what to do with partial successes and avoids spamming 14 | # anyone with notifications before the release is actually ready. 15 | name: Release 16 | 17 | permissions: 18 | contents: write 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc. 22 | # The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version. 25 | # 26 | # If PACKAGE_NAME is specified, then we will create a Github Release™ for that 27 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all 30 | # (cargo-dist-able) packages in the workspace with that version (this is mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent Github Release™ for each one. 36 | # 37 | # If there's a prerelease-style suffix to the version then the Github Release™ 38 | # will be marked as a prerelease. 39 | on: 40 | push: 41 | tags: 42 | - "*-?v[0-9]+*" 43 | 44 | jobs: 45 | # Create the Github Release™ so the packages have something to be uploaded to 46 | create-release: 47 | runs-on: ubuntu-latest 48 | outputs: 49 | has-releases: ${{ steps.create-release.outputs.has-releases }} 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Install Rust 55 | run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0 56 | - name: Install cargo-dist 57 | run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 58 | - id: create-release 59 | run: | 60 | cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json 61 | echo "dist plan ran successfully" 62 | cat dist-manifest.json 63 | 64 | # Create the Github Release™ based on what cargo-dist thinks it should be 65 | ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json) 66 | IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json) 67 | jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md 68 | gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md 69 | echo "created announcement!" 70 | 71 | # Upload the manifest to the Github Release™ 72 | gh release upload ${{ github.ref_name }} dist-manifest.json 73 | echo "uploaded manifest!" 74 | 75 | # Disable all the upload-artifacts tasks if we have no actual releases 76 | HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json) 77 | echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT" 78 | 79 | # Build and packages all the things 80 | upload-artifacts: 81 | # Let the initial task tell us to not run (currently very blunt) 82 | needs: create-release 83 | if: ${{ needs.create-release.outputs.has-releases == 'true' }} 84 | strategy: 85 | matrix: 86 | # For these target platforms 87 | include: 88 | - os: ubuntu-latest 89 | dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu 90 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 91 | - os: ubuntu-latest 92 | dist-args: --artifacts=local --target=aarch64-unknown-linux-gnu 93 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 94 | 95 | runs-on: ${{ matrix.os }} 96 | env: 97 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | steps: 99 | - uses: actions/checkout@v3 100 | - name: Install Rust 101 | run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0 102 | - name: Install cargo-dist 103 | run: ${{ matrix.install-dist }} 104 | - name: Run cargo-dist 105 | # This logic is a bit janky because it's trying to be a polyglot between 106 | # powershell and bash since this will run on windows, macos, and linux! 107 | # The two platforms don't agree on how to talk about env vars but they 108 | # do agree on 'cat' and '$()' so we use that to marshal values between commands. 109 | run: | 110 | # Actually do builds and make zips and whatnot 111 | cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json 112 | echo "dist ran successfully" 113 | cat dist-manifest.json 114 | 115 | # Parse out what we just built and upload it to the Github Release™ 116 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt 117 | echo "uploading..." 118 | cat uploads.txt 119 | gh release upload ${{ github.ref_name }} $(cat uploads.txt) 120 | echo "uploaded!" 121 | 122 | # Mark the Github Release™ as a non-draft now that everything has succeeded! 123 | publish-release: 124 | # Only run after all the other tasks, but it's ok if upload-artifacts was skipped 125 | needs: [create-release, upload-artifacts] 126 | if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }} 127 | runs-on: ubuntu-latest 128 | env: 129 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | steps: 131 | - uses: actions/checkout@v3 132 | - name: mark release as non-draft 133 | run: | 134 | gh release edit ${{ github.ref_name }} --draft=false 135 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.html 3 | .direnv 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to hyprland-autoname-workspaces 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | All types of contributions are encouraged and valued. The community looks forward to your contributions. 🎉 6 | 7 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 8 | > 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "serde", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "anstream" 44 | version = "0.6.15" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 47 | dependencies = [ 48 | "anstyle", 49 | "anstyle-parse", 50 | "anstyle-query", 51 | "anstyle-wincon", 52 | "colorchoice", 53 | "is_terminal_polyfill", 54 | "utf8parse", 55 | ] 56 | 57 | [[package]] 58 | name = "anstyle" 59 | version = "1.0.8" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 62 | 63 | [[package]] 64 | name = "anstyle-parse" 65 | version = "0.2.5" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 68 | dependencies = [ 69 | "utf8parse", 70 | ] 71 | 72 | [[package]] 73 | name = "anstyle-query" 74 | version = "1.1.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 77 | dependencies = [ 78 | "windows-sys", 79 | ] 80 | 81 | [[package]] 82 | name = "anstyle-wincon" 83 | version = "3.0.4" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 86 | dependencies = [ 87 | "anstyle", 88 | "windows-sys", 89 | ] 90 | 91 | [[package]] 92 | name = "async-stream" 93 | version = "0.3.6" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 96 | dependencies = [ 97 | "async-stream-impl", 98 | "futures-core", 99 | "pin-project-lite", 100 | ] 101 | 102 | [[package]] 103 | name = "async-stream-impl" 104 | version = "0.3.6" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 107 | dependencies = [ 108 | "proc-macro2", 109 | "quote", 110 | "syn", 111 | ] 112 | 113 | [[package]] 114 | name = "autocfg" 115 | version = "1.3.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 118 | 119 | [[package]] 120 | name = "backtrace" 121 | version = "0.3.73" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 124 | dependencies = [ 125 | "addr2line", 126 | "cc", 127 | "cfg-if", 128 | "libc", 129 | "miniz_oxide", 130 | "object", 131 | "rustc-demangle", 132 | ] 133 | 134 | [[package]] 135 | name = "bitflags" 136 | version = "1.3.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 139 | 140 | [[package]] 141 | name = "bytes" 142 | version = "1.7.1" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 145 | 146 | [[package]] 147 | name = "cc" 148 | version = "1.1.14" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" 151 | dependencies = [ 152 | "shlex", 153 | ] 154 | 155 | [[package]] 156 | name = "cfg-if" 157 | version = "1.0.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 160 | 161 | [[package]] 162 | name = "clap" 163 | version = "4.5.16" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" 166 | dependencies = [ 167 | "clap_builder", 168 | "clap_derive", 169 | ] 170 | 171 | [[package]] 172 | name = "clap_builder" 173 | version = "4.5.15" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" 176 | dependencies = [ 177 | "anstream", 178 | "anstyle", 179 | "clap_lex", 180 | "strsim", 181 | ] 182 | 183 | [[package]] 184 | name = "clap_derive" 185 | version = "4.5.13" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 188 | dependencies = [ 189 | "heck", 190 | "proc-macro2", 191 | "quote", 192 | "syn", 193 | ] 194 | 195 | [[package]] 196 | name = "clap_lex" 197 | version = "0.7.2" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 200 | 201 | [[package]] 202 | name = "colorchoice" 203 | version = "1.0.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 206 | 207 | [[package]] 208 | name = "derive_more" 209 | version = "1.0.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 212 | dependencies = [ 213 | "derive_more-impl", 214 | ] 215 | 216 | [[package]] 217 | name = "derive_more-impl" 218 | version = "1.0.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 221 | dependencies = [ 222 | "proc-macro2", 223 | "quote", 224 | "syn", 225 | "unicode-xid", 226 | ] 227 | 228 | [[package]] 229 | name = "either" 230 | version = "1.13.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 233 | 234 | [[package]] 235 | name = "equivalent" 236 | version = "1.0.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 239 | 240 | [[package]] 241 | name = "futures-core" 242 | version = "0.3.30" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 245 | 246 | [[package]] 247 | name = "futures-lite" 248 | version = "2.6.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" 251 | dependencies = [ 252 | "futures-core", 253 | "pin-project-lite", 254 | ] 255 | 256 | [[package]] 257 | name = "gimli" 258 | version = "0.29.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 261 | 262 | [[package]] 263 | name = "hashbrown" 264 | version = "0.14.5" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 267 | 268 | [[package]] 269 | name = "heck" 270 | version = "0.5.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 273 | 274 | [[package]] 275 | name = "hermit-abi" 276 | version = "0.3.9" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 279 | 280 | [[package]] 281 | name = "hyprland" 282 | version = "0.4.0-beta.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "dc9c1413b6f0fd10b2e4463479490e30b2497ae4449f044da16053f5f2cb03b8" 285 | dependencies = [ 286 | "ahash", 287 | "async-stream", 288 | "derive_more", 289 | "either", 290 | "futures-lite", 291 | "hyprland-macros", 292 | "num-traits", 293 | "once_cell", 294 | "paste", 295 | "phf", 296 | "serde", 297 | "serde_json", 298 | "serde_repr", 299 | "tokio", 300 | ] 301 | 302 | [[package]] 303 | name = "hyprland-autoname-workspaces" 304 | version = "1.1.16" 305 | dependencies = [ 306 | "clap", 307 | "hyprland", 308 | "inotify", 309 | "regex", 310 | "semver", 311 | "serde", 312 | "serde_json", 313 | "signal-hook", 314 | "single-instance", 315 | "strfmt", 316 | "toml", 317 | "xdg", 318 | ] 319 | 320 | [[package]] 321 | name = "hyprland-macros" 322 | version = "0.4.0-beta.2" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" 325 | dependencies = [ 326 | "proc-macro2", 327 | "quote", 328 | "syn", 329 | ] 330 | 331 | [[package]] 332 | name = "indexmap" 333 | version = "2.4.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" 336 | dependencies = [ 337 | "equivalent", 338 | "hashbrown", 339 | ] 340 | 341 | [[package]] 342 | name = "inotify" 343 | version = "0.10.2" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" 346 | dependencies = [ 347 | "bitflags", 348 | "futures-core", 349 | "inotify-sys", 350 | "libc", 351 | "tokio", 352 | ] 353 | 354 | [[package]] 355 | name = "inotify-sys" 356 | version = "0.1.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 359 | dependencies = [ 360 | "libc", 361 | ] 362 | 363 | [[package]] 364 | name = "is_terminal_polyfill" 365 | version = "1.70.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 368 | 369 | [[package]] 370 | name = "itoa" 371 | version = "1.0.11" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 374 | 375 | [[package]] 376 | name = "libc" 377 | version = "0.2.158" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 380 | 381 | [[package]] 382 | name = "memchr" 383 | version = "2.7.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 386 | 387 | [[package]] 388 | name = "memoffset" 389 | version = "0.6.5" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 392 | dependencies = [ 393 | "autocfg", 394 | ] 395 | 396 | [[package]] 397 | name = "miniz_oxide" 398 | version = "0.7.4" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 401 | dependencies = [ 402 | "adler", 403 | ] 404 | 405 | [[package]] 406 | name = "mio" 407 | version = "1.0.2" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 410 | dependencies = [ 411 | "hermit-abi", 412 | "libc", 413 | "wasi", 414 | "windows-sys", 415 | ] 416 | 417 | [[package]] 418 | name = "nix" 419 | version = "0.23.2" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" 422 | dependencies = [ 423 | "bitflags", 424 | "cc", 425 | "cfg-if", 426 | "libc", 427 | "memoffset", 428 | ] 429 | 430 | [[package]] 431 | name = "num-traits" 432 | version = "0.2.19" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 435 | dependencies = [ 436 | "autocfg", 437 | ] 438 | 439 | [[package]] 440 | name = "object" 441 | version = "0.36.3" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" 444 | dependencies = [ 445 | "memchr", 446 | ] 447 | 448 | [[package]] 449 | name = "once_cell" 450 | version = "1.19.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 453 | 454 | [[package]] 455 | name = "paste" 456 | version = "1.0.15" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 459 | 460 | [[package]] 461 | name = "phf" 462 | version = "0.11.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 465 | dependencies = [ 466 | "phf_macros", 467 | "phf_shared", 468 | ] 469 | 470 | [[package]] 471 | name = "phf_generator" 472 | version = "0.11.3" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 475 | dependencies = [ 476 | "phf_shared", 477 | "rand", 478 | ] 479 | 480 | [[package]] 481 | name = "phf_macros" 482 | version = "0.11.3" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 485 | dependencies = [ 486 | "phf_generator", 487 | "phf_shared", 488 | "proc-macro2", 489 | "quote", 490 | "syn", 491 | ] 492 | 493 | [[package]] 494 | name = "phf_shared" 495 | version = "0.11.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 498 | dependencies = [ 499 | "siphasher", 500 | ] 501 | 502 | [[package]] 503 | name = "pin-project-lite" 504 | version = "0.2.14" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 507 | 508 | [[package]] 509 | name = "proc-macro2" 510 | version = "1.0.86" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 513 | dependencies = [ 514 | "unicode-ident", 515 | ] 516 | 517 | [[package]] 518 | name = "quote" 519 | version = "1.0.37" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 522 | dependencies = [ 523 | "proc-macro2", 524 | ] 525 | 526 | [[package]] 527 | name = "rand" 528 | version = "0.8.5" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 531 | dependencies = [ 532 | "rand_core", 533 | ] 534 | 535 | [[package]] 536 | name = "rand_core" 537 | version = "0.6.4" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 540 | 541 | [[package]] 542 | name = "regex" 543 | version = "1.10.6" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 546 | dependencies = [ 547 | "aho-corasick", 548 | "memchr", 549 | "regex-automata", 550 | "regex-syntax", 551 | ] 552 | 553 | [[package]] 554 | name = "regex-automata" 555 | version = "0.4.7" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 558 | dependencies = [ 559 | "aho-corasick", 560 | "memchr", 561 | "regex-syntax", 562 | ] 563 | 564 | [[package]] 565 | name = "regex-syntax" 566 | version = "0.8.4" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 569 | 570 | [[package]] 571 | name = "rustc-demangle" 572 | version = "0.1.24" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 575 | 576 | [[package]] 577 | name = "ryu" 578 | version = "1.0.18" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 581 | 582 | [[package]] 583 | name = "semver" 584 | version = "1.0.23" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 587 | 588 | [[package]] 589 | name = "serde" 590 | version = "1.0.209" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 593 | dependencies = [ 594 | "serde_derive", 595 | ] 596 | 597 | [[package]] 598 | name = "serde_derive" 599 | version = "1.0.209" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 602 | dependencies = [ 603 | "proc-macro2", 604 | "quote", 605 | "syn", 606 | ] 607 | 608 | [[package]] 609 | name = "serde_json" 610 | version = "1.0.127" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" 613 | dependencies = [ 614 | "itoa", 615 | "memchr", 616 | "ryu", 617 | "serde", 618 | ] 619 | 620 | [[package]] 621 | name = "serde_repr" 622 | version = "0.1.19" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 625 | dependencies = [ 626 | "proc-macro2", 627 | "quote", 628 | "syn", 629 | ] 630 | 631 | [[package]] 632 | name = "serde_spanned" 633 | version = "0.6.7" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 636 | dependencies = [ 637 | "serde", 638 | ] 639 | 640 | [[package]] 641 | name = "shlex" 642 | version = "1.3.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 645 | 646 | [[package]] 647 | name = "signal-hook" 648 | version = "0.3.17" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 651 | dependencies = [ 652 | "libc", 653 | "signal-hook-registry", 654 | ] 655 | 656 | [[package]] 657 | name = "signal-hook-registry" 658 | version = "1.4.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 661 | dependencies = [ 662 | "libc", 663 | ] 664 | 665 | [[package]] 666 | name = "single-instance" 667 | version = "0.3.3" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "4637485391f8545c9d3dbf60f9d9aab27a90c789a700999677583bcb17c8795d" 670 | dependencies = [ 671 | "libc", 672 | "nix", 673 | "thiserror", 674 | "widestring", 675 | "winapi", 676 | ] 677 | 678 | [[package]] 679 | name = "siphasher" 680 | version = "1.0.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 683 | 684 | [[package]] 685 | name = "socket2" 686 | version = "0.5.7" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 689 | dependencies = [ 690 | "libc", 691 | "windows-sys", 692 | ] 693 | 694 | [[package]] 695 | name = "strfmt" 696 | version = "0.2.4" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" 699 | 700 | [[package]] 701 | name = "strsim" 702 | version = "0.11.1" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 705 | 706 | [[package]] 707 | name = "syn" 708 | version = "2.0.76" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "unicode-ident", 715 | ] 716 | 717 | [[package]] 718 | name = "thiserror" 719 | version = "1.0.63" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 722 | dependencies = [ 723 | "thiserror-impl", 724 | ] 725 | 726 | [[package]] 727 | name = "thiserror-impl" 728 | version = "1.0.63" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 731 | dependencies = [ 732 | "proc-macro2", 733 | "quote", 734 | "syn", 735 | ] 736 | 737 | [[package]] 738 | name = "tokio" 739 | version = "1.39.3" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" 742 | dependencies = [ 743 | "backtrace", 744 | "bytes", 745 | "libc", 746 | "mio", 747 | "pin-project-lite", 748 | "socket2", 749 | "tokio-macros", 750 | "windows-sys", 751 | ] 752 | 753 | [[package]] 754 | name = "tokio-macros" 755 | version = "2.4.0" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 758 | dependencies = [ 759 | "proc-macro2", 760 | "quote", 761 | "syn", 762 | ] 763 | 764 | [[package]] 765 | name = "toml" 766 | version = "0.7.8" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" 769 | dependencies = [ 770 | "indexmap", 771 | "serde", 772 | "serde_spanned", 773 | "toml_datetime", 774 | "toml_edit", 775 | ] 776 | 777 | [[package]] 778 | name = "toml_datetime" 779 | version = "0.6.8" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 782 | dependencies = [ 783 | "serde", 784 | ] 785 | 786 | [[package]] 787 | name = "toml_edit" 788 | version = "0.19.15" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 791 | dependencies = [ 792 | "indexmap", 793 | "serde", 794 | "serde_spanned", 795 | "toml_datetime", 796 | "winnow", 797 | ] 798 | 799 | [[package]] 800 | name = "unicode-ident" 801 | version = "1.0.12" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 804 | 805 | [[package]] 806 | name = "unicode-xid" 807 | version = "0.2.6" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 810 | 811 | [[package]] 812 | name = "utf8parse" 813 | version = "0.2.2" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 816 | 817 | [[package]] 818 | name = "version_check" 819 | version = "0.9.5" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 822 | 823 | [[package]] 824 | name = "wasi" 825 | version = "0.11.0+wasi-snapshot-preview1" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 828 | 829 | [[package]] 830 | name = "widestring" 831 | version = "0.4.3" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" 834 | 835 | [[package]] 836 | name = "winapi" 837 | version = "0.3.9" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 840 | dependencies = [ 841 | "winapi-i686-pc-windows-gnu", 842 | "winapi-x86_64-pc-windows-gnu", 843 | ] 844 | 845 | [[package]] 846 | name = "winapi-i686-pc-windows-gnu" 847 | version = "0.4.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 850 | 851 | [[package]] 852 | name = "winapi-x86_64-pc-windows-gnu" 853 | version = "0.4.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 856 | 857 | [[package]] 858 | name = "windows-sys" 859 | version = "0.52.0" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 862 | dependencies = [ 863 | "windows-targets", 864 | ] 865 | 866 | [[package]] 867 | name = "windows-targets" 868 | version = "0.52.6" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 871 | dependencies = [ 872 | "windows_aarch64_gnullvm", 873 | "windows_aarch64_msvc", 874 | "windows_i686_gnu", 875 | "windows_i686_gnullvm", 876 | "windows_i686_msvc", 877 | "windows_x86_64_gnu", 878 | "windows_x86_64_gnullvm", 879 | "windows_x86_64_msvc", 880 | ] 881 | 882 | [[package]] 883 | name = "windows_aarch64_gnullvm" 884 | version = "0.52.6" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 887 | 888 | [[package]] 889 | name = "windows_aarch64_msvc" 890 | version = "0.52.6" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 893 | 894 | [[package]] 895 | name = "windows_i686_gnu" 896 | version = "0.52.6" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 899 | 900 | [[package]] 901 | name = "windows_i686_gnullvm" 902 | version = "0.52.6" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 905 | 906 | [[package]] 907 | name = "windows_i686_msvc" 908 | version = "0.52.6" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 911 | 912 | [[package]] 913 | name = "windows_x86_64_gnu" 914 | version = "0.52.6" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 917 | 918 | [[package]] 919 | name = "windows_x86_64_gnullvm" 920 | version = "0.52.6" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 923 | 924 | [[package]] 925 | name = "windows_x86_64_msvc" 926 | version = "0.52.6" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 929 | 930 | [[package]] 931 | name = "winnow" 932 | version = "0.5.40" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 935 | dependencies = [ 936 | "memchr", 937 | ] 938 | 939 | [[package]] 940 | name = "xdg" 941 | version = "2.5.2" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 944 | 945 | [[package]] 946 | name = "zerocopy" 947 | version = "0.7.35" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 950 | dependencies = [ 951 | "zerocopy-derive", 952 | ] 953 | 954 | [[package]] 955 | name = "zerocopy-derive" 956 | version = "0.7.35" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 959 | dependencies = [ 960 | "proc-macro2", 961 | "quote", 962 | "syn", 963 | ] 964 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyprland-autoname-workspaces" 3 | authors = ["Cyril Levis", "Maxim Baz"] 4 | version = "1.1.16" 5 | edition = "2021" 6 | categories = ["gui"] 7 | keywords = ["linux", "desktop-application", "hyprland", "waybar", "wayland"] 8 | description = "This app automatically rename workspaces with icons of started applications." 9 | readme = "README.md" 10 | license = "ISC" 11 | homepage = "https://github.com/hyprland-community/hyprland-autoname-workspaces" 12 | repository = "https://github.com/hyprland-community/hyprland-autoname-workspaces" 13 | 14 | [dependencies] 15 | regex = "1" 16 | clap = { version = "4.3.19", features = ["derive"] } 17 | hyprland = { version = "=0.4.0-beta.2" } 18 | signal-hook = "0.3.17" 19 | toml = { version = "0.7.6", features = ["indexmap", "preserve_order"] } 20 | xdg = "2.5.2" 21 | inotify = "0.10.2" 22 | serde = "1.0.181" 23 | strfmt = "0.2.4" 24 | serde_json = "1.0.104" 25 | single-instance = "0.3.3" 26 | semver = "1.0.18" 27 | 28 | [features] 29 | dev = ["hyprland/default"] 30 | 31 | # The profile that 'cargo dist' will build with 32 | [profile.dist] 33 | inherits = "release" 34 | lto = "thin" 35 | 36 | # Config for 'cargo dist' 37 | [workspace.metadata.dist] 38 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 39 | cargo-dist-version = "0.0.7" 40 | # The preferred Rust toolchain to use in CI (rustup toolchain syntax) 41 | rust-toolchain-version = "1.71.0" 42 | # CI backends to support (see 'cargo dist generate-ci') 43 | ci = ["github"] 44 | # The installers to generate for each app 45 | installers = [] 46 | # Target platforms to build apps for (Rust target-triple syntax) 47 | targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [ISC License](https://spdx.org/licenses/ISC) 2 | 3 | Copyright (c) 2023, Cyril Levis 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN := hyprland-autoname-workspaces 2 | VERSION := $$(git tag | tail -1) 3 | 4 | PREFIX ?= /usr 5 | LIB_DIR = $(DESTDIR)$(PREFIX)/lib 6 | BIN_DIR = $(DESTDIR)$(PREFIX)/bin 7 | SHARE_DIR = $(DESTDIR)$(PREFIX)/share 8 | 9 | .PHONY: build-dev 10 | build-dev: 11 | cargo update 12 | cargo build --features dev 13 | 14 | .PHONY: build 15 | build: 16 | cargo build --locked --release 17 | 18 | .PHONY: release 19 | release: 20 | cargo bump --git-tag 21 | git push origin --follow-tags --signed=yes 22 | 23 | .PHONY: test 24 | test: 25 | cargo test --locked 26 | 27 | .PHONY: lint 28 | lint: 29 | cargo fmt -- --check 30 | cargo clippy -- -Dwarnings 31 | 32 | .PHONY: coverage 33 | coverage: 34 | cargo install tarpaulin 35 | cargo tarpaulin --out html; xdg-open tarpaulin-report.html 36 | 37 | .PHONY: run 38 | run: 39 | cargo run 40 | 41 | .PHONY: clean 42 | clean: 43 | rm -rf dist 44 | 45 | .PHONY: install 46 | install: 47 | install -Dm755 -t "$(BIN_DIR)/" "target/release/$(BIN)" 48 | install -Dm644 -t "$(LIB_DIR)/systemd/user" "$(BIN).service" 49 | install -Dm644 -t "$(SHARE_DIR)/licenses/$(BIN)/" LICENSE.md 50 | install -Dm644 -t "$(SHARE_DIR)/$(BIN)/examples/" config.toml.example 51 | install -Dm755 -t "$(SHARE_DIR)/$(BIN)/examples/" contrib/generate_icons.py 52 | 53 | .PHONY: dist 54 | dist: clean build 55 | mkdir -p dist 56 | cp "target/release/$(BIN)" . 57 | tar -czvf "dist/$(BIN)-$(VERSION)-linux-x86_64.tar.gz" "$(BIN)" "$(BIN).service" config.toml.example LICENSE.md README.md Makefile contrib/generate_icons.py 58 | git archive -o "dist/$(BIN)-$(VERSION).tar.gz" --format tar.gz --prefix "$(BIN)-$(VERSION)/" "$(VERSION)" 59 | for f in dist/*.tar.gz; do gpg --detach-sign --armor "$$f"; done 60 | rm -f "dist/$(BIN)-$(VERSION).tar.gz" "$(BIN)" 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪟 hyprland-autoname-workspaces 2 | 3 | ![](https://img.shields.io/crates/d/hyprland-autoname-workspaces) 4 | ![](https://img.shields.io/crates/v/hyprland-autoname-workspaces) 5 | ![](https://img.shields.io/github/issues-raw/hyprland-community/hyprland-autoname-workspaces) 6 | ![](https://img.shields.io/github/stars/hyprland-community/hyprland-autoname-workspaces) 7 | ![](https://img.shields.io/aur/version/hyprland-autoname-workspaces-git) 8 | ![](https://img.shields.io/crates/v/hyprland-autoname-workspaces) 9 | [![Discord](https://img.shields.io/discord/1055990214411169892?label=discord)](https://discord.gg/zzWqvcKRMy) 10 | [![codecov](https://codecov.io/gh/hyprland-community/hyprland-autoname-workspaces/branch/main/graph/badge.svg?token=NYY5DRMLM4)](https://codecov.io/gh/hyprland-community/hyprland-autoname-workspaces) 11 | 12 | ⚠️ We are seeking for active maintainers ! 13 | 14 | This project need your help. See here https://github.com/hyprland-community/hyprland-autoname-workspaces/issues/117 15 | 16 | 🕹️This is a toy for Hyprland. 17 | 18 | This app automatically rename workspaces with icons of started applications - tested with _[waybar](https://github.com/Alexays/Waybar)_ and _[eww](https://github.com/elkowar/eww)_. 19 | 20 | You have to set the config file with your prefered rules based on `class` and `title`. Regex (match and captures) are supported. 21 | 22 | ## FAQ, tips and tricks ❓ 23 | 24 | https://github.com/hyprland-community/hyprland-autoname-workspaces/wiki/FAQ 25 | 26 | ## Install 27 | 28 | ### AUR 📦 29 | 30 | Available as AUR package under the program name [`hyprland-autoname-workspaces-git`](https://aur.archlinux.org/packages/hyprland-autoname-workspaces-git). 31 | You can then use the service `systemctl --user enable --now hyprland-autoname-workspaces.service`. 32 | 33 | ### Fedora 📦 34 | 35 | Package available here https://copr.fedorainfracloud.org/coprs/solopasha/hyprland/. 36 | 37 | ### Nix 📦 38 | 39 | Available in nixpkgs as [`hyprland-autoname-workspaces`](https://search.nixos.org/packages?channel=unstable&show=hyprland-autoname-workspaces). 40 | You can add it to your `systemPackages` or try it without installing it with `nix run`. 41 | 42 | ```bash 43 | $ nix run nixpkgs#hyprland-autoname-workspaces 44 | ``` 45 | 46 | ### Cargo 📦 47 | 48 | ```bash 49 | $ cargo install --locked hyprland-autoname-workspaces 50 | ``` 51 | 52 | ## Usage 53 | 54 | ```bash 55 | $ hyprland-autoname-workspaces 56 | ``` 57 | 58 | ## Configuration 59 | 60 | First, you have to set your `waybar` for example, with the good module `hyprland/workspaces`. The module `wlr/workspaces` is deprecated. 61 | You have to use `hyprland/workspaces` module with as config `"format" = "{name}"`. 62 | 63 | ``` 64 | "hyprland/workspaces": { 65 | "format": "{name}", 66 | } 67 | ``` 68 | 69 | _For all parameters, check the `config.toml.example` in this repository._ 70 | 71 | The config file can be specified using the `-c ` option, otherwise it defaults to `~/.config/hyprland-autoname-workspaces/config.toml`. If you specify a path that doesn't exist, a default configuration file will be generated. 72 | 73 | _You can use regex everywhere, and its case sensitive by default_ 74 | 75 | Edit the mapping of applications with `class = "icon"` in the `[icons]` part. 76 | 77 | In icons value, you can use the placeholders `{class}`, `{title}` and `{match1}`, `{match2}` if you use regex captures. 78 | 79 | Example: 80 | 81 | ``` 82 | [class] 83 | DEFAULT = "{class}: {title}" 84 | ... 85 | ``` 86 | 87 | - You can exclude applications in the `[exclude]` with `class = title`. 88 | 89 | In the `exclude` part, the key is the window `class`, and the value the `title`. 90 | You can use `""` in order to exclude window with empty title and `".*"` as value to match all title of a class name. 91 | 92 | Example: 93 | 94 | ``` 95 | ... 96 | [exclude] 97 | "(?i)fcitx" = ".*" # will match all title for fcitx 98 | "[Ss]team" = "Friends list.*" 99 | "[Ss]team" = "^$" # will match and exclude all Steam class with empty title (some popups) 100 | ``` 101 | 102 | - You can match on title with `[title_in_class.classname]` and `[title_in_class_active.class]` with `"a word in the title" = "icons"`. 103 | 104 | _Hint_: There is also `title_in_initial_class`, `initial_title_in_class`, `initial_title_in_initial_class` and so on. 105 | 106 | Example: 107 | 108 | ``` 109 | ... 110 | [title."(xterm|(?i)kitty|alacritty)"] 111 | "(?i)neomutt" = "mail" 112 | ncdu = "file manager" 113 | 114 | [title."(firefox|chrom.*)"] 115 | youtube = "yt" 116 | google = "gg" 117 | 118 | [title_active."(firefox|chrom.*)"] 119 | youtube = "yt" 120 | google = "{icon}" 121 | ... 122 | 123 | ``` 124 | 125 | - You can deduplicate icons with the `dedup` parameter in the `root` section of config file. 126 | 127 | ``` 128 | dedup = true 129 | dedup_inactive_fullscreen = true 130 | ... 131 | [title."(xterm|(?i)kitty|alacritty)"] 132 | "(?i)neomutt" = "mail" 133 | ncdu = "file manager" 134 | ... 135 | ``` 136 | 137 | - You can also redefine all the default formatter with those `[format]` section formatters parameters. 138 | The available list of `{placeholder}` is: 139 | 140 | workspace: 141 | 142 | - client 143 | - id (or id_long) 144 | - name (use value from `[workspaces_name]` mapping) 145 | - delim 146 | 147 | clients: 148 | 149 | - icon 150 | - counter_s, counter_unfocused_s, counter, counter_unfocused 151 | - class, iitle 152 | - delim 153 | - match1, match2, match3, matchN (for regex captures) 154 | 155 | ``` 156 | [format] 157 | # max_clients = 10 (default: usize::MAX) 158 | dedup = true 159 | dedup_inactive_fullscreen = true 160 | delim = " " # NARROW NO-BREAK SPACE 161 | workspace = "{id}:{delim}{clients}" 162 | workspace_empty = "{id}" 163 | client = "{icon}{delim}" 164 | client_active = "{icon}{delim}" 165 | client_dup = "{icon}{counter_sup}{delim}" 166 | client_dup_fullscreen = "[{icon}]{delim}{icon}{counter_unfocused_sup}" 167 | client_fullscreen = "[{icon}]{delim}" 168 | ... 169 | ``` 170 | 171 | See `config.toml.example` and the wiki for more example, feel free to share your config ! 172 | 173 | No need to restart the applications then, there is an autoreload. 174 | 175 | _Hint_: You can use glyphsearch and copy the unicode icon of your font for example https://glyphsearch.com/?query=book©=unicode 176 | 177 | _Hint_: You can find hyprland class names for currently running apps using: `hyprctl clients | grep -i class`, or you can also use `hyprland-autoname-workspaces --verbose`. 178 | 179 | _Hint_: Feel free to adapt and use this [script](https://github.com/Psykopear/i3autoname/blob/master/scripts/generate_icons.py) to generate your config file. This is untested for the moment. 180 | 181 | _Hint_: You can bootstrap your `[icons]` with the `contrib/generate_icons.py` script. 182 | 183 | _Hint_: All styling param that you can use with `` are here: https://docs.gtk.org/Pango/pango_markup.html 184 | -------------------------------------------------------------------------------- /config.toml.example: -------------------------------------------------------------------------------- 1 | version = "1.1.16" 2 | 3 | [format] 4 | dedup = true 5 | dedup_inactive_fullscreen = false 6 | delim = " " 7 | client = "{icon}{delim}" 8 | client_active = "{icon}" 9 | workspace = "{id}-{name}:{delim}{clients}" 10 | workspace_empty = "{id}-{name}:{delim}{clients}" 11 | client_dup = "{icon}{counter_sup}{delim}" 12 | client_dup_fullscreen = "[{icon}]{delim}{icon}{counter_unfocused_sup}" 13 | client_fullscreen = "[{icon}]{delim}" 14 | 15 | [class_active] 16 | DEFAULT="{icon}" 17 | "(?i)firefox" = " {class}" 18 | 19 | # [initial_class] 20 | # "DEFAULT" = " {class}: {title}" 21 | # "(?i)Kitty" = "term" 22 | 23 | # [initial_class_active] 24 | # "(?i)Kitty" = "*TERM*" 25 | 26 | # regex captures support is supported 27 | [title_in_class."(?i)foot"] 28 | "emerge: (.+?/.+?)-.*" = "{match1}" 29 | 30 | [initial_title_in_class."kitty"] 31 | "zsh" = "Zsh" 32 | 33 | [title_in_class."(firefox|chrom.*)"] 34 | "(?i)youtube" = "ꟳ" 35 | "(?i)twitch" = "ꟳ" 36 | 37 | [title_active."(firefox|chrom.*)"] 38 | "(?i)twitch" = "{icon}" 39 | 40 | # [title_in_initial_class."(?i)kitty"] 41 | # "(?i)neomutt" = "neomutt" 42 | 43 | # [initial_title_in_initial_class."(?i)kitty"] 44 | # "(?i)neomutt" = "neomutt" 45 | 46 | # [initial_title."(?i)kitty"] 47 | # "zsh" = "Zsh" 48 | 49 | # [initial_title_active."(?i)kitty"] 50 | # "zsh" = "*Zsh*" 51 | 52 | [workspaces_name] 53 | 0 = "zero" 54 | 1 = "one" 55 | 2 = "two" 56 | 3 = "three" 57 | 4 = "four" 58 | 5 = "five" 59 | 6 = "six" 60 | 7 = "seven" 61 | 8 = "eight" 62 | 9 = "nine" 63 | 10 = "ten" 64 | 65 | [class] 66 | DEFAULT = "" 67 | "(?i)firefox" = "" 68 | "(?i)kitty" = "" 69 | "(?i)alacritty" = "" 70 | bleachbit = "" 71 | burp-startburp = "" 72 | calibre-gui = "" 73 | "chrome-faolnafnngnfdaknnbpnkhgohbobgegn-default" = "" 74 | chromium = "" 75 | "Gimp-2.10" = "" 76 | code-oss = "" 77 | cssh = "" 78 | darktable = "" 79 | discord = "" 80 | dmenu-clipboard = "" 81 | dmenu-pass = "" 82 | duolingo = "" 83 | element = "" 84 | fontforge = "" 85 | gcr-prompter = "" 86 | gsimplecalc = "" 87 | "jetbrains-studio" = "" 88 | "kak" = "" 89 | kicad = "" 90 | "(?i)waydroid.*" = "droid" 91 | obsidian = "" 92 | "dmenu-emoji" = "" 93 | "dmenu-browser" = "" 94 | "dmenu-pass generator" = "" 95 | "qalculate-gtk" = "" 96 | krita = "" 97 | libreoffice-calc = "" 98 | libreoffice-impress = "" 99 | libreoffice-startcenter = "" 100 | libreoffice-writer = "" 101 | molotov = "" 102 | mpv = "" 103 | neomutt = "" 104 | nm-connection-editor = "" 105 | org-ksnip-ksnip = "" 106 | org-pwmt-zathura = "" 107 | org-qutebrowser-qutebrowser = "" 108 | org-telegram-desktop = "" 109 | paperwork = "" 110 | pavucontrol = "" 111 | personal = "" 112 | plexamp = "" 113 | qutepreview = "" 114 | rapid-photo-downloader = "" 115 | remote-viewer = "" 116 | sandboxed-tor-browser = "" 117 | scli = "" 118 | shopping = "" 119 | Signal = "" 120 | slack = "" 121 | snappergui = "" 122 | songrec = "" 123 | spotify = "" 124 | steam = "" 125 | streamlink-twitch-gui = "" 126 | sun-awt-x11-xframepeer = "" 127 | swappy = "" 128 | taskwarrior-tui = "" 129 | telegramdesktop = "" 130 | ".*transmission.*" = "" 131 | udiskie = "" 132 | vimiv = "" 133 | virt-manager = "" 134 | vlc = "" 135 | vncviewer = "" 136 | wayvnc = "󰀄" 137 | whatsapp-desktop = "" 138 | whatsapp-nativefier-d52542 = "" 139 | wire = "󰁀" 140 | wireshark-gtk = "" 141 | wlfreerdp = "󰀄" 142 | work = "" 143 | xplr = "" 144 | nemo = "" 145 | zoom = "" 146 | 147 | [exclude] 148 | "" = "^$" # prevent displaying clients with empty class 149 | -------------------------------------------------------------------------------- /contrib/generate_icons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This script pulls the font awesome spec from their repo on github 4 | then generate an [icon] list for `hyprland-autoname-workspaces`. 5 | > Taken from fontawesome-python. 6 | """ 7 | 8 | import argparse 9 | 10 | import requests 11 | import yaml 12 | 13 | 14 | def unicode_symbol(hex_code): 15 | """ 16 | Convert a hexadecimal code to its corresponding Unicode character. 17 | 18 | :param hex_code: The hexadecimal code of the Unicode character. 19 | :return: The Unicode character. 20 | """ 21 | return chr(int(hex_code, 16)) 22 | 23 | 24 | def print_custom_icons(): 25 | """ 26 | Print the custom icon for Alacritty. 27 | """ 28 | term_icon = "f120" 29 | print(f'"(?i)alacritty" = "{unicode_symbol(term_icon)}"') 30 | print(f'"(?i)kitty" = "{unicode_symbol(term_icon)}"') 31 | 32 | 33 | def print_icon_names(icons_dict, include_aliases): 34 | """ 35 | Print the icon names and their corresponding Unicode symbols 36 | from the provided icons dictionary. 37 | 38 | :param icons_dict: The dictionary containing icon information. 39 | :param include_aliases: Whether to include aliases in the output. 40 | """ 41 | for icon_name, icon in icons_dict.items(): 42 | # Create the names list with the icon_name and its aliases if required 43 | names = [icon_name] + ( 44 | icon["search"]["terms"] 45 | if include_aliases and icon["search"]["terms"] is not None 46 | else [] 47 | ) 48 | 49 | # Iterate through the names, filtering out empty strings 50 | for name in filter(lambda x: x != "", names): 51 | print(f'"(?i){name}" = "{unicode_symbol(icon["unicode"])}"') 52 | 53 | 54 | def main(uri, version, include_aliases): 55 | """ 56 | Main function to fetch icons dictionary from the provided URI and print 57 | the icon names and their corresponding Unicode symbols. 58 | 59 | :param uri: The URI to fetch the icons dictionary. 60 | :param version: The version of the icons (default to 'master'). 61 | :param include_aliases: Whether to include aliases in the output. 62 | """ 63 | icons_dict = yaml.full_load(requests.get(uri).text) 64 | 65 | print("[class]") 66 | 67 | # Custom icons 68 | print_custom_icons() 69 | 70 | # Icons from the provided dictionary 71 | print_icon_names(icons_dict, include_aliases) 72 | 73 | 74 | if __name__ == "__main__": 75 | parser = argparse.ArgumentParser( 76 | description="Generate icons.py, containing a python mapping for font awesome icons" 77 | ) 78 | parser.add_argument( 79 | "--revision", 80 | help="Version of font of font awesome to download and use. Should correspond to a git branch name.", 81 | default="master", 82 | ) 83 | parser.add_argument( 84 | "--include_aliases", 85 | help="If enabled, also adds aliases for icons in the output.", 86 | action="store_true", 87 | ) 88 | args = parser.parse_args() 89 | 90 | REVISION = args.revision 91 | URI = ( 92 | "https://raw.githubusercontent.com" 93 | f"/FortAwesome/Font-Awesome/{REVISION}/metadata/icons.yml" 94 | ) 95 | 96 | main(URI, args.revision, args.include_aliases) 97 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk": { 22 | "inputs": { 23 | "nixpkgs": "nixpkgs" 24 | }, 25 | "locked": { 26 | "lastModified": 1721727458, 27 | "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", 28 | "owner": "nix-community", 29 | "repo": "naersk", 30 | "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "nix-community", 35 | "ref": "master", 36 | "repo": "naersk", 37 | "type": "github" 38 | } 39 | }, 40 | "nixpkgs": { 41 | "locked": { 42 | "lastModified": 0, 43 | "narHash": "sha256-pP3Azj5d6M5nmG68Fu4JqZmdGt4S4vqI5f8te+E/FTw=", 44 | "path": "/nix/store/ia1zpg1s63v6b3vin3n7bxxjgcs51s2r-source", 45 | "type": "path" 46 | }, 47 | "original": { 48 | "id": "nixpkgs", 49 | "type": "indirect" 50 | } 51 | }, 52 | "nixpkgs_2": { 53 | "locked": { 54 | "lastModified": 1724395761, 55 | "narHash": "sha256-zRkDV/nbrnp3Y8oCADf5ETl1sDrdmAW6/bBVJ8EbIdQ=", 56 | "owner": "NixOS", 57 | "repo": "nixpkgs", 58 | "rev": "ae815cee91b417be55d43781eb4b73ae1ecc396c", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "NixOS", 63 | "ref": "nixpkgs-unstable", 64 | "repo": "nixpkgs", 65 | "type": "github" 66 | } 67 | }, 68 | "root": { 69 | "inputs": { 70 | "flake-utils": "flake-utils", 71 | "naersk": "naersk", 72 | "nixpkgs": "nixpkgs_2" 73 | } 74 | }, 75 | "systems": { 76 | "locked": { 77 | "lastModified": 1681028828, 78 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 79 | "owner": "nix-systems", 80 | "repo": "default", 81 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 82 | "type": "github" 83 | }, 84 | "original": { 85 | "owner": "nix-systems", 86 | "repo": "default", 87 | "type": "github" 88 | } 89 | } 90 | }, 91 | "root": "root", 92 | "version": 7 93 | } 94 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | naersk.url = "github:nix-community/naersk/master"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, flake-utils, naersk }: 9 | let systems = [ "x86_64-linux" "aarch64-linux" ]; 10 | in flake-utils.lib.eachSystem systems (system: 11 | let 12 | pkgs = import nixpkgs { inherit system; }; 13 | naersk-lib = pkgs.callPackage naersk { }; 14 | libs = with pkgs; [ libxkbcommon libinput libpulseaudio systemd ]; 15 | in 16 | { 17 | defaultPackage = naersk-lib.buildPackage { 18 | src = ./.; 19 | meta.mainProgram = "hyprland-autoname-workspaces"; 20 | nativeBuildInputs = [ pkgs.pkg-config ]; 21 | buildInputs = libs; 22 | }; 23 | devShell = with pkgs; mkShell { 24 | buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy pkg-config ] ++ libs; 25 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 26 | }; 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /hyprland-autoname-workspaces.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Autoname Hyprland workspaces 3 | PartOf=graphical-session.target 4 | After=graphical-session.target 5 | 6 | [Service] 7 | ExecStart=/usr/bin/hyprland-autoname-workspaces 8 | Restart=always 9 | RestartSec=10s 10 | 11 | [Install] 12 | WantedBy=graphical-session.target 13 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use semver::Version; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use std::error::Error; 6 | use std::fs; 7 | use std::fs::File; 8 | use std::io::Write; 9 | use std::path::PathBuf; 10 | use std::process; 11 | 12 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 13 | const BIN_NAME: &str = env!("CARGO_BIN_NAME"); 14 | 15 | #[derive(Default, Clone, Debug)] 16 | pub struct Config { 17 | pub config: ConfigFile, 18 | pub cfg_path: Option, 19 | } 20 | 21 | fn default_delim_formatter() -> String { 22 | " ".to_string() 23 | } 24 | 25 | fn default_client_formatter() -> String { 26 | "{icon}".to_string() 27 | } 28 | 29 | fn default_client_active_formatter() -> String { 30 | "*{icon}*".to_string() 31 | } 32 | 33 | fn default_client_fullscreen_formatter() -> String { 34 | "[{icon}]".to_string() 35 | } 36 | 37 | fn default_client_dup_formatter() -> String { 38 | "{icon}{counter_sup}".to_string() 39 | } 40 | 41 | fn default_client_dup_fullscreen_formatter() -> String { 42 | "[{icon}]{delim}{icon}{counter_unfocused_sup}".to_string() 43 | } 44 | 45 | fn default_client_dup_active_formatter() -> String { 46 | "*{icon}*{delim}{icon}{counter_unfocused_sup}".to_string() 47 | } 48 | 49 | fn default_workspace_empty_formatter() -> String { 50 | "{id}".to_string() 51 | } 52 | 53 | fn default_workspace_formatter() -> String { 54 | "{id}:{delim}{clients}".to_string() 55 | } 56 | 57 | fn default_class() -> HashMap { 58 | HashMap::from([("DEFAULT".to_string(), " {class}".to_string())]) 59 | } 60 | 61 | // Nested serde default doesnt work. 62 | impl Default for ConfigFormatRaw { 63 | fn default() -> Self { 64 | toml::from_str("").unwrap() 65 | } 66 | } 67 | 68 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 69 | pub struct ConfigFormatRaw { 70 | #[serde(default)] 71 | pub max_clients: Option, 72 | #[serde(default)] 73 | pub dedup: bool, 74 | #[serde(default)] 75 | pub dedup_inactive_fullscreen: bool, 76 | #[serde(default = "default_delim_formatter")] 77 | pub delim: String, 78 | #[serde(default = "default_workspace_formatter")] 79 | pub workspace: String, 80 | #[serde(default = "default_workspace_empty_formatter")] 81 | pub workspace_empty: String, 82 | #[serde(default = "default_client_formatter")] 83 | pub client: String, 84 | #[serde(default = "default_client_fullscreen_formatter")] 85 | pub client_fullscreen: String, 86 | #[serde(default = "default_client_active_formatter")] 87 | pub client_active: String, 88 | #[serde(default = "default_client_dup_formatter")] 89 | pub client_dup: String, 90 | #[serde(default = "default_client_dup_active_formatter")] 91 | pub client_dup_active: String, 92 | #[serde(default = "default_client_dup_fullscreen_formatter")] 93 | pub client_dup_fullscreen: String, 94 | } 95 | 96 | #[derive(Deserialize, Serialize)] 97 | pub struct ConfigFileRaw { 98 | #[serde(default)] 99 | pub version: String, 100 | #[serde(default = "default_class", alias = "icons")] 101 | pub class: HashMap, 102 | #[serde(default, alias = "active_icons", alias = "icons_active")] 103 | pub class_active: HashMap, 104 | #[serde(default)] 105 | pub initial_class: HashMap, 106 | #[serde(default)] 107 | pub initial_class_active: HashMap, 108 | #[serde(default)] 109 | pub workspaces_name: HashMap, 110 | #[serde(default, alias = "title_icons")] 111 | pub title_in_class: HashMap>, 112 | #[serde(default, alias = "title_active_icons")] 113 | pub title_in_class_active: HashMap>, 114 | #[serde(default)] 115 | pub title_in_initial_class: HashMap>, 116 | #[serde(default)] 117 | pub title_in_initial_class_active: HashMap>, 118 | #[serde(default)] 119 | pub initial_title_in_class: HashMap>, 120 | #[serde(default)] 121 | pub initial_title_in_class_active: HashMap>, 122 | #[serde(default)] 123 | pub initial_title_in_initial_class: HashMap>, 124 | #[serde(default)] 125 | pub initial_title_in_initial_class_active: HashMap>, 126 | #[serde(default)] 127 | pub exclude: HashMap, 128 | #[serde(default)] 129 | pub format: ConfigFormatRaw, 130 | } 131 | 132 | #[derive(Default, Debug, Clone)] 133 | pub struct ConfigFile { 134 | pub class: Vec<(Regex, String)>, 135 | pub class_active: Vec<(Regex, String)>, 136 | pub workspaces_name: Vec<(String, String)>, 137 | pub initial_class: Vec<(Regex, String)>, 138 | pub initial_class_active: Vec<(Regex, String)>, 139 | pub title_in_class: Vec<(Regex, Vec<(Regex, String)>)>, 140 | pub title_in_class_active: Vec<(Regex, Vec<(Regex, String)>)>, 141 | pub title_in_initial_class: Vec<(Regex, Vec<(Regex, String)>)>, 142 | pub title_in_initial_class_active: Vec<(Regex, Vec<(Regex, String)>)>, 143 | pub initial_title_in_class: Vec<(Regex, Vec<(Regex, String)>)>, 144 | pub initial_title_in_class_active: Vec<(Regex, Vec<(Regex, String)>)>, 145 | pub initial_title_in_initial_class: Vec<(Regex, Vec<(Regex, String)>)>, 146 | pub initial_title_in_initial_class_active: Vec<(Regex, Vec<(Regex, String)>)>, 147 | pub exclude: Vec<(Regex, Regex)>, 148 | pub format: ConfigFormatRaw, 149 | } 150 | 151 | impl Config { 152 | pub fn new( 153 | cfg_path: PathBuf, 154 | dump_config: bool, 155 | migrate_config: bool, 156 | ) -> Result> { 157 | if !cfg_path.exists() { 158 | _ = create_default_config(&cfg_path); 159 | } 160 | 161 | Ok(Config { 162 | config: read_config_file(Some(cfg_path.clone()), dump_config, migrate_config)?, 163 | cfg_path: Some(cfg_path), 164 | }) 165 | } 166 | } 167 | 168 | impl ConfigFileRaw { 169 | pub fn migrate(&mut self, cfg_path: &Option) -> Result<(), Box> { 170 | self.version = VERSION.to_string(); 171 | let config_updated = toml::to_string(&self)?; 172 | if let Some(path) = cfg_path { 173 | let config_file = &mut File::create(path)?; 174 | write!(config_file, "{config_updated}")?; 175 | println!("Config file successfully migrated in {path:?}"); 176 | } 177 | Ok(()) 178 | } 179 | } 180 | 181 | pub fn read_config_file( 182 | cfg_path: Option, 183 | dump_config: bool, 184 | migrate_config: bool, 185 | ) -> Result> { 186 | let mut config: ConfigFileRaw = match &cfg_path { 187 | Some(path) => { 188 | let config_string = fs::read_to_string(path)?; 189 | toml::from_str(&config_string).map_err(|e| format!("Unable to parse: {e:?}"))? 190 | } 191 | None => toml::from_str("").map_err(|e| format!("Unable to parse: {e:?}"))?, 192 | }; 193 | 194 | migrate_config_file(&mut config, migrate_config, cfg_path)?; 195 | 196 | if dump_config { 197 | println!("{}", serde_json::to_string_pretty(&config)?); 198 | process::exit(0); 199 | } 200 | 201 | Ok(ConfigFile { 202 | class: generate_icon_config(&config.class), 203 | class_active: generate_icon_config(&config.class_active), 204 | workspaces_name: generate_workspaces_name_config(&config.workspaces_name), 205 | initial_class: generate_icon_config(&config.initial_class), 206 | initial_class_active: generate_icon_config(&config.initial_class_active), 207 | title_in_class: generate_title_config(&config.title_in_class), 208 | title_in_class_active: generate_title_config(&config.title_in_class_active), 209 | title_in_initial_class: generate_title_config(&config.title_in_initial_class), 210 | title_in_initial_class_active: generate_title_config(&config.title_in_initial_class_active), 211 | initial_title_in_class: generate_title_config(&config.initial_title_in_class), 212 | initial_title_in_class_active: generate_title_config(&config.initial_title_in_class_active), 213 | initial_title_in_initial_class: generate_title_config( 214 | &config.initial_title_in_initial_class, 215 | ), 216 | initial_title_in_initial_class_active: generate_title_config( 217 | &config.initial_title_in_initial_class_active, 218 | ), 219 | exclude: generate_exclude_config(&config.exclude), 220 | format: config.format, 221 | }) 222 | } 223 | 224 | pub fn get_config_path(args: &Option) -> Result> { 225 | let cfg_path = match args { 226 | Some(path) => PathBuf::from(path), 227 | _ => { 228 | let xdg_dirs = xdg::BaseDirectories::with_prefix(BIN_NAME)?; 229 | xdg_dirs.place_config_file("config.toml")? 230 | } 231 | }; 232 | 233 | Ok(cfg_path) 234 | } 235 | 236 | fn migrate_config_file( 237 | config: &mut ConfigFileRaw, 238 | migrate_config: bool, 239 | cfg_path: Option, 240 | ) -> Result<(), Box> { 241 | let default_version = Version::parse("1.0.0")?; 242 | let actual_version = Version::parse(&config.version).unwrap_or(default_version); 243 | let last_version = Version::parse(VERSION)?; 244 | let need_migrate = actual_version < last_version; 245 | if need_migrate { 246 | println!("Config in version {actual_version} need to be updated in version {last_version}, run: {BIN_NAME} --migrate-config"); 247 | } 248 | if need_migrate && migrate_config { 249 | config 250 | .migrate(&cfg_path) 251 | .map_err(|e| format!("Unable to migrate config {e:?}"))?; 252 | }; 253 | Ok(()) 254 | } 255 | 256 | pub fn create_default_config(cfg_path: &PathBuf) -> Result<&'static str, Box> { 257 | // TODO: maybe we should dump the config from the default values of the struct? 258 | let default_config = r#" 259 | version = "1.1.14" 260 | 261 | # [format] 262 | # Deduplicate icons if enable. 263 | # A superscripted counter will be added. 264 | # dedup = false 265 | # dedup_inactive_fullscreen = false # dedup more 266 | # window delimiter 267 | # delim = " " 268 | # max_clients = 30 # you should not need this 269 | 270 | # available formatter: 271 | # {counter_sup} - superscripted count of clients on the workspace, and simple {counter}, {delim} 272 | # {icon}, {client} 273 | # workspace formatter 274 | # workspace = "{id}:{delim}{clients}" # {id}, {delim} and {clients} are supported 275 | # workspace_empty = "{id}" # {id}, {delim} and {clients} are supported 276 | # client formatter 277 | # client = "{icon}" 278 | # client_active = "*{icon}*" 279 | 280 | # deduplicate client formatter 281 | # client_fullscreen = "[{icon}]" 282 | # client_dup = "{client}{counter_sup}" 283 | # client_dup_fullscreen = "[{icon}]{delim}{icon}{counter_unfocused}" 284 | # client_dup_active = "*{icon}*{delim}{icon}{counter_unfocused}" 285 | 286 | [class] 287 | # Add your icons mapping 288 | # use double quote the key and the value 289 | # take class name from 'hyprctl clients' 290 | "DEFAULT" = " {class}: {title}" 291 | "(?i)Kitty" = "term" 292 | "[Ff]irefox" = "browser" 293 | "(?i)waydroid.*" = "droid" 294 | 295 | [class_active] 296 | DEFAULT = "*{icon}*" 297 | "(?i)ExampleOneTerm" = "{icon}" 298 | 299 | # [initial_class] 300 | # "DEFAULT" = " {class}: {title}" 301 | # "(?i)Kitty" = "term" 302 | 303 | # [initial_class_active] 304 | # "(?i)Kitty" = "*TERM*" 305 | 306 | [title_in_class."(?i)kitty"] 307 | "(?i)neomutt" = "neomutt" 308 | # regex captures support is supported 309 | # "emerge: (.+?/.+?)-.*" = "{match1}" 310 | 311 | [title_in_class_active."(?i)firefox"] 312 | "(?i)twitch" = "{icon}" 313 | 314 | # [title_in_initial_class."(?i)kitty"] 315 | # "(?i)neomutt" = "neomutt" 316 | 317 | # [initial_title_in_class."(?i)kitty"] 318 | # "(?i)neomutt" = "neomutt" 319 | 320 | # [initial_title_in_initial_class."(?i)kitty"] 321 | # "(?i)neomutt" = "neomutt" 322 | 323 | # [initial_title."(?i)kitty"] 324 | # "zsh" = "Zsh" 325 | 326 | # [initial_title_active."(?i)kitty"] 327 | # "zsh" = "*Zsh*" 328 | 329 | # Add your applications that need to be exclude 330 | # The key is the class, the value is the title. 331 | # You can put an empty title to exclude based on 332 | # class name only, "" make the job. 333 | [exclude] 334 | "" = "^$" # prevent displaying icon for empty class 335 | "(?i)fcitx" = ".*" # will match all title for fcitx 336 | "(?i)TestApp" = "" # will match all title for TestApp 337 | aProgram = "^$" # will match null title for aProgram 338 | "[Ss]team" = "^(Friends List.*)?$" # will match Steam friends list plus all popups (empty titles) 339 | 340 | [workspaces_name] 341 | 0 = "zero" 342 | 1 = "one" 343 | 2 = "two" 344 | 3 = "three" 345 | 4 = "four" 346 | 5 = "five" 347 | 6 = "six" 348 | 7 = "seven" 349 | 8 = "eight" 350 | 9 = "nine" 351 | 10 = "ten" 352 | 353 | "# 354 | .trim(); 355 | 356 | let mut config_file = File::create(cfg_path)?; 357 | write!(&mut config_file, "{default_config}")?; 358 | println!("Default config created in {cfg_path:?}"); 359 | 360 | Ok(default_config) 361 | } 362 | 363 | /// Creates a Regex from a given pattern and logs an error if the pattern is invalid. 364 | /// 365 | /// # Arguments 366 | /// 367 | /// * `pattern` - A string representing the regex pattern to be compiled. 368 | /// 369 | /// # Returns 370 | /// 371 | /// * `Option` - Returns Some(Regex) if the pattern is valid, otherwise None. 372 | /// 373 | /// # Example 374 | /// 375 | /// ``` 376 | /// use regex::Regex; 377 | /// use crate::regex_with_error_logging; 378 | /// 379 | /// let valid_pattern = "Class1"; 380 | /// let invalid_pattern = "Class1["; 381 | /// 382 | /// assert!(regex_with_error_logging(valid_pattern).is_some()); 383 | /// assert!(regex_with_error_logging(invalid_pattern).is_none()); 384 | /// ``` 385 | fn regex_with_error_logging(pattern: &str) -> Option { 386 | match Regex::new(pattern) { 387 | Ok(re) => Some(re), 388 | Err(e) => { 389 | println!("Unable to parse regex: {e:?}"); 390 | None 391 | } 392 | } 393 | } 394 | 395 | /// Generates the title configuration for the application. 396 | /// 397 | /// This function accepts a nested HashMap where the outer HashMap's keys represent class names, 398 | /// and the inner HashMap's keys represent titles, and their values are icons. 399 | /// It returns a Vec of tuples, where the first element is a Regex object created from the class name, 400 | /// and the second element is a Vec of tuples containing a Regex object created from the title and the corresponding icon as a String. 401 | /// 402 | /// # Arguments 403 | /// 404 | /// * `icons` - A nested HashMap where the outer keys are class names, and the inner keys are titles with their corresponding icon values. 405 | /// 406 | /// # Examples 407 | /// 408 | /// ``` 409 | /// let title_icons = generate_title_config(title_icons_map); 410 | /// ``` 411 | fn generate_title_config( 412 | icons: &HashMap>, 413 | ) -> Vec<(Regex, Vec<(Regex, String)>)> { 414 | icons 415 | .iter() 416 | .filter_map(|(class, title_icon)| { 417 | regex_with_error_logging(class).map(|re| { 418 | ( 419 | re, 420 | title_icon 421 | .iter() 422 | .filter_map(|(title, icon)| { 423 | regex_with_error_logging(title).map(|re| (re, icon.to_string())) 424 | }) 425 | .collect(), 426 | ) 427 | }) 428 | }) 429 | .collect() 430 | } 431 | 432 | /// Generates the icon configuration for the application. 433 | /// 434 | /// This function accepts a HashMap where the keys represent class names and the values are icons. 435 | /// It returns a Vec of tuples, where the first element is a Regex object created from the class name, 436 | /// and the second element is the corresponding icon as a String. 437 | /// 438 | /// # Arguments 439 | /// 440 | /// * `icons` - A HashMap with keys as class names and values as icons. 441 | /// 442 | /// # Examples 443 | /// 444 | /// ``` 445 | /// let icons_config = generate_icon_config(icons_map); 446 | /// ``` 447 | fn generate_icon_config(icons: &HashMap) -> Vec<(Regex, String)> { 448 | icons 449 | .iter() 450 | .filter_map(|(class, icon)| { 451 | regex_with_error_logging(class).map(|re| (re, icon.to_string())) 452 | }) 453 | .collect() 454 | } 455 | 456 | /// Generates the exclude configuration for the application. 457 | /// 458 | /// This function accepts a HashMap where the keys represent class names and the values are titles. 459 | /// It returns a Vec of tuples, where the first element is a Regex object created from the class name, 460 | /// and the second element is a Regex object created from the title. 461 | /// 462 | /// # Arguments 463 | /// 464 | /// * `icons` - A HashMap with keys as class names and values as titles. 465 | /// 466 | /// # Examples 467 | /// 468 | /// ``` 469 | /// let exclude_config = generate_exclude_config(exclude_map); 470 | /// ``` 471 | fn generate_exclude_config(icons: &HashMap) -> Vec<(Regex, Regex)> { 472 | icons 473 | .iter() 474 | .filter_map(|(class, title)| { 475 | regex_with_error_logging(class).and_then(|re_class| { 476 | regex_with_error_logging(title).map(|re_title| (re_class, re_title)) 477 | }) 478 | }) 479 | .collect() 480 | } 481 | 482 | /// Generates the workspaces id to name mapping 483 | fn generate_workspaces_name_config( 484 | workspaces_name: &HashMap, 485 | ) -> Vec<(String, String)> { 486 | workspaces_name 487 | .iter() 488 | .filter_map(|(id, name)| { 489 | if id.parse::().is_ok() { 490 | Some((id.to_string(), name.to_string())) 491 | } else { 492 | None 493 | } 494 | }) 495 | .collect() 496 | } 497 | 498 | #[cfg(test)] 499 | mod tests { 500 | use super::*; 501 | use std::collections::HashMap; 502 | 503 | #[test] 504 | fn test_generate_title_config() { 505 | let mut title_icons_map: HashMap> = HashMap::new(); 506 | let mut inner_map: HashMap = HashMap::new(); 507 | inner_map.insert("Title1".to_string(), "Icon1".to_string()); 508 | title_icons_map.insert("Class1".to_string(), inner_map); 509 | 510 | let title_config = generate_title_config(&title_icons_map); 511 | 512 | assert_eq!(title_config.len(), 1); 513 | assert!(title_config[0].0.is_match("Class1")); 514 | assert_eq!(title_config[0].1.len(), 1); 515 | assert!(title_config[0].1[0].0.is_match("Title1")); 516 | assert_eq!(title_config[0].1[0].1, "Icon1"); 517 | } 518 | 519 | #[test] 520 | fn test_generate_icon_config() { 521 | let mut list_class: HashMap = HashMap::new(); 522 | list_class.insert("Class1".to_string(), "Icon1".to_string()); 523 | 524 | let icons_config = generate_icon_config(&list_class); 525 | 526 | assert_eq!(icons_config.len(), 1); 527 | assert!(icons_config[0].0.is_match("Class1")); 528 | assert_eq!(icons_config[0].1, "Icon1"); 529 | } 530 | 531 | #[test] 532 | fn test_generate_exclude_config() { 533 | let mut list_exclude: HashMap = HashMap::new(); 534 | list_exclude.insert("Class1".to_string(), "Title1".to_string()); 535 | 536 | let exclude_config = generate_exclude_config(&list_exclude); 537 | 538 | assert_eq!(exclude_config.len(), 1); 539 | assert!(exclude_config[0].0.is_match("Class1")); 540 | assert!(exclude_config[0].1.is_match("Title1")); 541 | } 542 | 543 | #[test] 544 | fn test_regex_with_error_logging() { 545 | let valid_pattern = "Class1"; 546 | let invalid_pattern = "Class1["; 547 | 548 | assert!(regex_with_error_logging(valid_pattern).is_some()); 549 | assert!(regex_with_error_logging(invalid_pattern).is_none()); 550 | } 551 | 552 | #[test] 553 | fn test_config_new_and_read_again_then_compare_format() { 554 | let cfg_path = PathBuf::from("/tmp/hyprland-autoname-workspaces-test.toml"); 555 | let config = Config::new(cfg_path.clone(), false, false); 556 | assert_eq!(config.is_ok(), true); 557 | let config = config.unwrap().clone(); 558 | assert_eq!(config.cfg_path.clone(), Some(cfg_path.clone())); 559 | let format = config.config.format.clone(); 560 | let config2 = read_config_file(Some(cfg_path.clone()), false, false).unwrap(); 561 | let format2 = config2.format.clone(); 562 | assert_eq!(format, format2); 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod params; 3 | mod renamer; 4 | 5 | use crate::config::Config; 6 | use crate::params::Args; 7 | use crate::renamer::*; 8 | 9 | use clap::Parser; 10 | use config::get_config_path; 11 | use signal_hook::consts::{SIGINT, SIGTERM}; 12 | use signal_hook::iterator::Signals; 13 | use single_instance::SingleInstance; 14 | use std::{process, thread}; 15 | 16 | fn main() { 17 | let args = Args::parse(); 18 | let cfg_path = get_config_path(&args.config).expect("Can't get config path"); 19 | let cfg = Config::new(cfg_path, args.dump, args.migrate_config).expect("Unable to read config"); 20 | 21 | let instance = SingleInstance::new("Hyprland-autoname-workspaces").unwrap(); 22 | if !instance.is_single() { 23 | eprintln!("Hyprland-autoname-workspaces is already running, exit"); 24 | process::exit(1); 25 | } 26 | 27 | // Init 28 | let renamer = Renamer::new(cfg.clone(), args); 29 | renamer 30 | .rename_workspace() 31 | .expect("App can't rename workspaces on start"); 32 | 33 | // Handle unix signals 34 | let mut signals = Signals::new([SIGINT, SIGTERM]).expect("Can't listen on SIGINT or SIGTERM"); 35 | let final_renamer = renamer.clone(); 36 | 37 | thread::spawn(move || { 38 | if signals.forever().next().is_some() { 39 | match final_renamer.reset_workspaces(cfg.config) { 40 | Err(_) => println!("Workspaces name can't be cleared"), 41 | Ok(_) => println!("Workspaces name cleared, bye"), 42 | }; 43 | process::exit(0); 44 | } 45 | }); 46 | 47 | let config_renamer = renamer.clone(); 48 | thread::spawn(move || { 49 | config_renamer 50 | .watch_config_changes(cfg.cfg_path) 51 | .expect("Unable to watch for config changes") 52 | }); 53 | 54 | renamer.start_listeners() 55 | } 56 | -------------------------------------------------------------------------------- /src/params/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct Args { 6 | #[arg(short, long)] 7 | pub verbose: bool, 8 | #[arg(short, long)] 9 | pub debug: bool, 10 | #[arg(long)] 11 | pub dump: bool, 12 | #[arg(long)] 13 | pub migrate_config: bool, 14 | #[arg(short, long, default_value = None)] 15 | pub config: Option, 16 | } 17 | -------------------------------------------------------------------------------- /src/renamer/formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::renamer::ConfigFile; 2 | use crate::renamer::IconStatus::*; 3 | use crate::{AppClient, Renamer}; 4 | use hyprland::data::FullscreenMode; 5 | use std::collections::HashMap; 6 | use strfmt::strfmt; 7 | 8 | #[derive(Clone)] 9 | pub struct AppWorkspace { 10 | pub id: i32, 11 | pub clients: Vec, 12 | } 13 | 14 | impl AppWorkspace { 15 | pub fn new(id: i32, clients: Vec) -> Self { 16 | AppWorkspace { id, clients } 17 | } 18 | } 19 | 20 | impl Renamer { 21 | pub fn generate_workspaces_string( 22 | &self, 23 | workspaces: Vec, 24 | config: &ConfigFile, 25 | ) -> HashMap { 26 | let vars = HashMap::from([("delim".to_string(), config.format.delim.to_string())]); 27 | workspaces 28 | .iter() 29 | .map(|workspace| { 30 | let mut counted = 31 | generate_counted_clients(workspace.clients.clone(), config.format.dedup); 32 | 33 | let workspace_output = counted 34 | .iter_mut() 35 | .map(|(client, counter)| self.handle_new_client(client, *counter, config)) 36 | .take( 37 | config 38 | .format 39 | .max_clients 40 | .map_or(usize::MAX, |max| max as usize), 41 | ) 42 | .collect::>(); 43 | 44 | let delimiter = formatter("{delim}", &vars); 45 | let joined_string = workspace_output.join(&delimiter); 46 | 47 | (workspace.id, joined_string) 48 | }) 49 | .collect() 50 | } 51 | 52 | fn handle_new_client(&self, client: &AppClient, counter: i32, config: &ConfigFile) -> String { 53 | let config_format = &config.format; 54 | let client = client.clone(); 55 | 56 | let is_dedup = config_format.dedup && (counter > 1); 57 | let is_dedup_inactive_fullscreen = config_format.dedup_inactive_fullscreen; 58 | 59 | let counter_sup = to_superscript(counter); 60 | let prev_counter = (counter - 1).to_string(); 61 | let prev_counter_sup = to_superscript(counter - 1); 62 | let delim = &config_format.delim.to_string(); 63 | 64 | let fmt_client = &config_format.client.to_string(); 65 | let fmt_client_active = &config_format.client_active.to_string(); 66 | let fmt_client_fullscreen = &config_format.client_fullscreen.to_string(); 67 | let fmt_client_dup = &config_format.client_dup.to_string(); 68 | let fmt_client_dup_fullscreen = &config_format.client_dup_fullscreen.to_string(); 69 | 70 | let mut vars = HashMap::from([ 71 | ("title".to_string(), client.title.clone()), 72 | ("class".to_string(), client.class.clone()), 73 | ("counter".to_string(), counter.to_string()), 74 | ("counter_unfocused".to_string(), prev_counter), 75 | ("counter_sup".to_string(), counter_sup), 76 | ("counter_unfocused_sup".to_string(), prev_counter_sup), 77 | ("delim".to_string(), delim.to_string()), 78 | ]); 79 | 80 | // get regex captures and merge them with vars 81 | if let Some(re_captures) = client.matched_rule.captures() { 82 | merge_vars(&mut vars, re_captures); 83 | }; 84 | 85 | let icon = match (client.is_active, client.matched_rule.clone()) { 86 | (true, c @ Inactive(_)) => { 87 | vars.insert("default_icon".to_string(), c.icon()); 88 | formatter( 89 | &fmt_client_active.replace("{icon}", "{default_icon}"), 90 | &vars, 91 | ) 92 | } 93 | (_, c) => c.icon(), 94 | }; 95 | 96 | vars.insert("icon".to_string(), icon); 97 | vars.insert("client".to_string(), fmt_client.to_string()); 98 | vars.insert("client_dup".to_string(), fmt_client_dup.to_string()); 99 | vars.insert( 100 | "client_fullscreen".to_string(), 101 | fmt_client_fullscreen.to_string(), 102 | ); 103 | 104 | if self.args.debug { 105 | println!("client: {client:#?}\nformatter vars => {vars:#?}"); 106 | } 107 | 108 | let is_grouped = client.is_fullscreen != FullscreenMode::None 109 | && (client.is_active || !is_dedup_inactive_fullscreen); 110 | 111 | match (is_grouped, is_dedup) { 112 | (true, true) => formatter(fmt_client_dup_fullscreen, &vars), 113 | (false, true) => formatter(fmt_client_dup, &vars), 114 | (true, false) => formatter(fmt_client_fullscreen, &vars), 115 | (false, false) => formatter(fmt_client, &vars), 116 | } 117 | } 118 | } 119 | 120 | pub fn formatter(fmt: &str, vars: &HashMap) -> String { 121 | let mut result = fmt.to_owned(); 122 | let mut i = 0; 123 | loop { 124 | if !(result.contains('{') && result.contains('}')) { 125 | break result; 126 | } 127 | let formatted = strfmt(&result, vars).unwrap_or_else(|_| result.clone()); 128 | if formatted == result { 129 | break result; 130 | } 131 | result = formatted; 132 | i += 1; 133 | if i > 3 { 134 | eprintln!("placeholders loop, aborting"); 135 | break result; 136 | } 137 | } 138 | } 139 | 140 | pub fn generate_counted_clients( 141 | clients: Vec, 142 | need_dedup: bool, 143 | ) -> Vec<(AppClient, i32)> { 144 | if need_dedup { 145 | let mut sorted_clients = clients; 146 | sorted_clients.sort_by(|a, b| { 147 | let bf = b.is_fullscreen != FullscreenMode::None; 148 | let af = a.is_fullscreen != FullscreenMode::None; 149 | bf.cmp(&af) 150 | }); 151 | sorted_clients.sort_by(|a, b| b.is_active.cmp(&a.is_active)); 152 | 153 | sorted_clients 154 | .into_iter() 155 | .fold(vec![], |mut state, client| { 156 | match state.iter_mut().find(|(c, _)| c == &client) { 157 | Some(c) => c.1 += 1, 158 | None => state.push((client, 1)), 159 | } 160 | state 161 | }) 162 | } else { 163 | clients.into_iter().map(|c| (c, 1)).collect() 164 | } 165 | } 166 | 167 | fn merge_vars(map1: &mut HashMap, map2: HashMap) { 168 | map1.extend(map2); 169 | } 170 | 171 | pub fn to_superscript(number: i32) -> String { 172 | let m: HashMap<_, _> = [ 173 | ('0', "⁰"), 174 | ('1', "¹"), 175 | ('2', "²"), 176 | ('3', "³"), 177 | ('4', "⁴"), 178 | ('5', "⁵"), 179 | ('6', "⁶"), 180 | ('7', "⁷"), 181 | ('8', "⁸"), 182 | ('9', "⁹"), 183 | ] 184 | .into_iter() 185 | .collect(); 186 | 187 | number.to_string().chars().map(|c| m[&c]).collect() 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | use crate::renamer::IconConfig::*; 194 | 195 | #[test] 196 | fn test_app_workspace_new() { 197 | let client = AppClient { 198 | class: String::from("Class"), 199 | initial_class: String::from("Class"), 200 | title: String::from("Title"), 201 | initial_title: String::from("Title"), 202 | is_active: false, 203 | is_fullscreen: FullscreenMode::Fullscreen, 204 | matched_rule: Inactive(Default(String::from("DefaultIcon"))), 205 | is_dedup_inactive_fullscreen: false, 206 | }; 207 | 208 | let workspace = AppWorkspace::new(1, vec![client]); 209 | 210 | assert_eq!(workspace.id, 1); 211 | assert_eq!(workspace.clients.len(), 1); 212 | assert_eq!(workspace.clients[0].class, "Class"); 213 | assert_eq!(workspace.clients[0].title, "Title"); 214 | assert_eq!(workspace.clients[0].is_active, false); 215 | assert_eq!( 216 | workspace.clients[0].is_fullscreen, 217 | FullscreenMode::Fullscreen 218 | ); 219 | match &workspace.clients[0].matched_rule { 220 | Inactive(Default(icon)) => assert_eq!(icon, "DefaultIcon"), 221 | _ => panic!("Unexpected IconConfig value"), 222 | }; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/renamer/icon.rs: -------------------------------------------------------------------------------- 1 | use crate::renamer::IconConfig::*; 2 | use crate::renamer::IconStatus::*; 3 | use crate::renamer::{ConfigFile, Renamer}; 4 | use std::collections::HashMap; 5 | 6 | type Rule = String; 7 | type Icon = String; 8 | type Title = String; 9 | type Class = String; 10 | type Captures = Option>; 11 | type ListTitleInClass<'a> = Option<&'a [(regex::Regex, Vec<(regex::Regex, Icon)>)]>; 12 | type ListClass<'a> = Option<&'a [(regex::Regex, Icon)]>; 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub enum IconConfig { 16 | Class(Rule, Icon), 17 | InitialClass(Rule, Icon), 18 | TitleInClass(Rule, Icon, Captures), 19 | TitleInInitialClass(Rule, Icon, Captures), 20 | InitialTitleInClass(Rule, Icon, Captures), 21 | InitialTitleInInitialClass(Rule, Icon, Captures), 22 | Default(Icon), 23 | } 24 | 25 | impl IconConfig { 26 | pub fn icon(&self) -> Icon { 27 | let (_, icon, _) = self.get(); 28 | icon 29 | } 30 | 31 | pub fn captures(&self) -> Captures { 32 | let (_, _, captures) = self.get(); 33 | captures 34 | } 35 | 36 | pub fn get(&self) -> (Rule, Icon, Captures) { 37 | match &self { 38 | Default(icon) => ("DEFAULT".to_string(), icon.to_string(), None), 39 | Class(rule, icon) | InitialClass(rule, icon) => { 40 | (rule.to_string(), icon.to_string(), None) 41 | } 42 | TitleInClass(rule, icon, captures) 43 | | TitleInInitialClass(rule, icon, captures) 44 | | InitialTitleInClass(rule, icon, captures) 45 | | InitialTitleInInitialClass(rule, icon, captures) => { 46 | (rule.to_string(), icon.to_string(), captures.clone()) 47 | } 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone, Debug, PartialEq, Eq)] 53 | pub enum IconStatus { 54 | Active(IconConfig), 55 | Inactive(IconConfig), 56 | } 57 | 58 | impl IconStatus { 59 | pub fn icon(&self) -> Icon { 60 | match self { 61 | Active(config) | Inactive(config) => config.icon(), 62 | } 63 | } 64 | 65 | pub fn captures(&self) -> Captures { 66 | match self { 67 | Active(config) | Inactive(config) => config.captures(), 68 | } 69 | } 70 | } 71 | 72 | impl Renamer { 73 | fn find_icon( 74 | &self, 75 | initial_class: &str, 76 | class: &str, 77 | initial_title: &str, 78 | title: &str, 79 | is_active: bool, 80 | config: &ConfigFile, 81 | ) -> Option { 82 | let ( 83 | list_initial_title_in_initial_class, 84 | list_initial_title_in_class, 85 | list_title_in_initial_class, 86 | list_title_in_class, 87 | list_initial_class, 88 | list_class, 89 | ) = if is_active { 90 | ( 91 | &config.initial_title_in_initial_class_active, 92 | &config.initial_title_in_class_active, 93 | &config.title_in_initial_class_active, 94 | &config.title_in_class_active, 95 | &config.initial_class_active, 96 | &config.class_active, 97 | ) 98 | } else { 99 | ( 100 | &config.initial_title_in_initial_class, 101 | &config.initial_title_in_class, 102 | &config.title_in_initial_class, 103 | &config.title_in_class, 104 | &config.initial_class, 105 | &config.class, 106 | ) 107 | }; 108 | 109 | find_icon_helper( 110 | is_active, 111 | Some(list_initial_title_in_initial_class), 112 | None, 113 | IconParams { 114 | class: None, 115 | title: None, 116 | initial_class: Some(initial_class), 117 | initial_title: Some(initial_title), 118 | }, 119 | ) 120 | .or(find_icon_helper( 121 | is_active, 122 | Some(list_initial_title_in_class), 123 | None, 124 | IconParams { 125 | class: Some(class), 126 | title: None, 127 | initial_class: None, 128 | initial_title: Some(initial_title), 129 | }, 130 | ) 131 | .or(find_icon_helper( 132 | is_active, 133 | Some(list_title_in_initial_class), 134 | None, 135 | IconParams { 136 | class: None, 137 | title: Some(title), 138 | initial_class: Some(initial_class), 139 | initial_title: None, 140 | }, 141 | ) 142 | .or(find_icon_helper( 143 | is_active, 144 | Some(list_title_in_class), 145 | None, 146 | IconParams { 147 | class: Some(class), 148 | title: Some(title), 149 | initial_class: None, 150 | initial_title: None, 151 | }, 152 | ) 153 | .or(find_icon_helper( 154 | is_active, 155 | None, 156 | Some(list_initial_class), 157 | IconParams { 158 | class: None, 159 | title: None, 160 | initial_class: Some(initial_class), 161 | initial_title: None, 162 | }, 163 | )) 164 | .or(find_icon_helper( 165 | is_active, 166 | None, 167 | Some(list_class), 168 | IconParams { 169 | class: Some(class), 170 | title: None, 171 | initial_class: None, 172 | initial_title: None, 173 | }, 174 | ))))) 175 | } 176 | 177 | pub fn parse_icon( 178 | &self, 179 | initial_class: Class, 180 | class: Class, 181 | initial_title: Title, 182 | title: Title, 183 | is_active: bool, 184 | config: &ConfigFile, 185 | ) -> IconStatus { 186 | let icon = self.find_icon( 187 | &initial_class, 188 | &class, 189 | &initial_title, 190 | &title, 191 | false, 192 | config, 193 | ); 194 | 195 | let icon_active = 196 | self.find_icon(&initial_class, &class, &initial_title, &title, true, config); 197 | 198 | let icon_default = self 199 | .find_icon("DEFAULT", "DEFAULT", "", "", false, config) 200 | .unwrap_or(Inactive(Default("no icon".to_string()))); 201 | 202 | let icon_default_active = self 203 | .find_icon("DEFAULT", "DEFAULT", "", "", true, config) 204 | .unwrap_or(icon_default.clone()); 205 | 206 | if is_active { 207 | icon_active.unwrap_or(match icon { 208 | Some(i) => i, 209 | None => icon_default_active, 210 | }) 211 | } else { 212 | icon.unwrap_or_else(|| { 213 | if self.args.verbose { 214 | println!("- window: class '{}' need a shiny icon", class); 215 | } 216 | icon_default 217 | }) 218 | } 219 | } 220 | } 221 | 222 | pub struct IconParams<'a> { 223 | class: Option<&'a str>, 224 | title: Option<&'a str>, 225 | initial_class: Option<&'a str>, 226 | initial_title: Option<&'a str>, 227 | } 228 | 229 | pub fn forge_icon_status( 230 | is_active: bool, 231 | rule: String, 232 | icon: String, 233 | params: IconParams, 234 | captures: Captures, 235 | ) -> IconStatus { 236 | let icon = match ( 237 | params.class, 238 | params.title, 239 | params.initial_class, 240 | params.initial_title, 241 | captures, 242 | ) { 243 | (None, None, None, None, None) => Default(icon), 244 | (Some(_), None, None, None, None) => Class(rule, icon), 245 | (None, None, Some(_), None, None) => InitialClass(rule, icon), 246 | (Some(_), Some(_), None, None, c) => TitleInClass(rule, icon, c), 247 | (None, None, Some(_), Some(_), c) => InitialTitleInInitialClass(rule, icon, c), 248 | (None, Some(_), Some(_), None, c) => TitleInInitialClass(rule, icon, c), 249 | (Some(_), None, None, Some(_), c) => InitialTitleInClass(rule, icon, c), 250 | (_, _, _, _, _) => Default(icon), 251 | }; 252 | 253 | if is_active { 254 | Active(icon) 255 | } else { 256 | Inactive(icon) 257 | } 258 | } 259 | 260 | fn find_icon_helper( 261 | is_active: bool, 262 | list_title_in_class: ListTitleInClass, 263 | list_class: ListClass, 264 | params: IconParams, 265 | ) -> Option { 266 | let the_class = match (params.class, params.initial_class) { 267 | (Some(c), None) | (None, Some(c)) => c, 268 | (_, _) => unreachable!(), 269 | }; 270 | 271 | match (list_class, list_title_in_class) { 272 | (Some(list), None) => { 273 | list.iter() 274 | .find(|(rule, _)| rule.is_match(the_class)) 275 | .map(|(rule, icon)| { 276 | forge_icon_status(is_active, rule.to_string(), icon.to_string(), params, None) 277 | }) 278 | } 279 | (None, Some(list)) => { 280 | let the_title = match (params.title, params.initial_title) { 281 | (Some(t), None) | (None, Some(t)) => t, 282 | (_, _) => unreachable!(), 283 | }; 284 | 285 | list.iter() 286 | .find(|(re_class, _)| re_class.is_match(the_class)) 287 | .and_then(|(_, title_icon)| { 288 | title_icon 289 | .iter() 290 | .find(|(rule, _)| rule.is_match(the_title)) 291 | .map(|(rule, icon)| { 292 | forge_icon_status( 293 | is_active, 294 | rule.to_string(), 295 | icon.to_string(), 296 | params, 297 | get_captures(Some(the_title), rule), 298 | ) 299 | }) 300 | }) 301 | } 302 | (_, _) => unreachable!(), 303 | } 304 | } 305 | 306 | fn get_captures(title: Option<&str>, rule: ®ex::Regex) -> Captures { 307 | match title { 308 | Some(t) => rule.captures(t).map(|re_captures| { 309 | re_captures 310 | .iter() 311 | .enumerate() 312 | .map(|(k, v)| { 313 | ( 314 | format!("match{k}"), 315 | v.map_or("", |m| m.as_str()).to_string(), 316 | ) 317 | }) 318 | .collect() 319 | }), 320 | _ => None, 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/renamer/macros.rs: -------------------------------------------------------------------------------- 1 | /// Renames the workspace if the given events occur. 2 | /// 3 | /// # Arguments 4 | /// 5 | /// * `$self` - The main struct containing the renameworkspace method. 6 | /// * `$ev` - The event manager to attach event handlers. 7 | /// * `$x` - A list of events to attach the handlers to. 8 | macro_rules! rename_workspace_if { 9 | ( $self: ident, $ev: ident, $( $x:ident ), * ) => { 10 | $( 11 | let this = $self.clone(); 12 | $ev.$x(move |_| _ = this.rename_workspace()); 13 | )* 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/renamer/mod.rs: -------------------------------------------------------------------------------- 1 | mod formatter; 2 | mod icon; 3 | 4 | #[macro_use] 5 | mod macros; 6 | 7 | use crate::config::{Config, ConfigFile, ConfigFormatRaw}; 8 | use crate::params::Args; 9 | use formatter::*; 10 | use hyprland::data::{Client, Clients, FullscreenMode, Workspace}; 11 | use hyprland::dispatch::*; 12 | use hyprland::event_listener::{EventListener, WorkspaceEventData}; 13 | use hyprland::prelude::*; 14 | use hyprland::shared::Address; 15 | use icon::{IconConfig, IconStatus}; 16 | use inotify::{Inotify, WatchMask}; 17 | use std::collections::{HashMap, HashSet}; 18 | use std::error::Error; 19 | use std::path::PathBuf; 20 | use std::sync::{Arc, Mutex}; 21 | 22 | pub struct Renamer { 23 | known_workspaces: Mutex>, 24 | cfg: Mutex, 25 | args: Args, 26 | workspace_strings_cache: Mutex>, 27 | } 28 | 29 | #[derive(Clone, Eq, Debug)] 30 | pub struct AppClient { 31 | class: String, 32 | title: String, 33 | //FIXME: I can't understand why clippy 34 | // see dead code, but for me, my code is not dead! 35 | #[allow(dead_code)] 36 | initial_class: String, 37 | #[allow(dead_code)] 38 | initial_title: String, 39 | is_active: bool, 40 | is_fullscreen: FullscreenMode, 41 | is_dedup_inactive_fullscreen: bool, 42 | matched_rule: IconStatus, 43 | } 44 | 45 | impl PartialEq for AppClient { 46 | fn eq(&self, other: &Self) -> bool { 47 | self.matched_rule == other.matched_rule 48 | && self.is_active == other.is_active 49 | && (self.is_dedup_inactive_fullscreen || self.is_fullscreen == other.is_fullscreen) 50 | } 51 | } 52 | 53 | impl AppClient { 54 | fn new( 55 | client: Client, 56 | is_active: bool, 57 | is_dedup_inactive_fullscreen: bool, 58 | matched_rule: IconStatus, 59 | ) -> Self { 60 | AppClient { 61 | initial_class: client.initial_class, 62 | class: client.class, 63 | initial_title: client.initial_title, 64 | title: client.title, 65 | is_active, 66 | is_fullscreen: client.fullscreen, 67 | is_dedup_inactive_fullscreen, 68 | matched_rule, 69 | } 70 | } 71 | } 72 | 73 | impl Renamer { 74 | pub fn new(cfg: Config, args: Args) -> Arc { 75 | Arc::new(Renamer { 76 | known_workspaces: Mutex::new(HashSet::default()), 77 | cfg: Mutex::new(cfg), 78 | args, 79 | workspace_strings_cache: Mutex::new(HashMap::new()), 80 | }) 81 | } 82 | 83 | pub fn rename_workspace(&self) -> Result<(), Box> { 84 | // Config 85 | let config = &self.cfg.lock()?.config.clone(); 86 | 87 | // Rename active workspace if empty 88 | rename_empty_workspace(config); 89 | 90 | // Filter clients 91 | let clients = get_filtered_clients(config); 92 | 93 | // Get the active client 94 | let active_client = get_active_client(); 95 | 96 | // Get workspaces based on open clients 97 | let workspaces = self.get_workspaces_from_clients(clients, active_client, config)?; 98 | let workspace_ids: HashSet<_> = workspaces.iter().map(|w| w.id).collect(); 99 | 100 | // Generate workspace strings 101 | let workspaces_strings = self.generate_workspaces_string(workspaces, config); 102 | 103 | // Filter out unchanged workspaces 104 | let altered_workspaces = self.get_altered_workspaces(&workspaces_strings)?; 105 | 106 | altered_workspaces.iter().for_each(|(&id, clients)| { 107 | rename_cmd(id, clients, &config.format, &config.workspaces_name); 108 | }); 109 | 110 | self.update_cache(&altered_workspaces, &workspace_ids)?; 111 | 112 | Ok(()) 113 | } 114 | 115 | fn get_altered_workspaces( 116 | &self, 117 | workspaces_strings: &HashMap, 118 | ) -> Result, Box> { 119 | let cache = self.workspace_strings_cache.lock()?; 120 | Ok(workspaces_strings 121 | .iter() 122 | .filter_map(|(&id, new_string)| { 123 | if cache.get(&id) != Some(new_string) { 124 | Some((id, new_string.clone())) 125 | } else { 126 | None 127 | } 128 | }) 129 | .collect()) 130 | } 131 | 132 | fn update_cache( 133 | &self, 134 | workspaces_strings: &HashMap, 135 | workspace_ids: &HashSet, 136 | ) -> Result<(), Box> { 137 | let mut cache = self.workspace_strings_cache.lock()?; 138 | for (&id, new_string) in workspaces_strings { 139 | cache.insert(id, new_string.clone()); 140 | } 141 | 142 | // Remove cached entries for workspaces that no longer exist 143 | cache.retain(|&id, _| workspace_ids.contains(&id)); 144 | 145 | Ok(()) 146 | } 147 | 148 | fn get_workspaces_from_clients( 149 | &self, 150 | clients: Vec, 151 | active_client: String, 152 | config: &ConfigFile, 153 | ) -> Result, Box> { 154 | let mut workspaces = self 155 | .known_workspaces 156 | .lock()? 157 | .iter() 158 | .map(|&i| (i, Vec::new())) 159 | .collect::>>(); 160 | 161 | let is_dedup_inactive_fullscreen = config.format.dedup_inactive_fullscreen; 162 | 163 | for client in clients { 164 | let workspace_id = client.workspace.id; 165 | self.known_workspaces.lock()?.insert(workspace_id); 166 | let is_active = active_client == client.address.to_string(); 167 | workspaces 168 | .entry(workspace_id) 169 | .or_insert_with(Vec::new) 170 | .push(AppClient::new( 171 | client.clone(), 172 | is_active, 173 | is_dedup_inactive_fullscreen, 174 | self.parse_icon( 175 | client.initial_class, 176 | client.class, 177 | client.initial_title, 178 | client.title, 179 | is_active, 180 | config, 181 | ), 182 | )); 183 | } 184 | 185 | Ok(workspaces 186 | .iter() 187 | .map(|(&id, clients)| AppWorkspace::new(id, clients.to_vec())) 188 | .collect()) 189 | } 190 | 191 | pub fn reset_workspaces(&self, config: ConfigFile) -> Result<(), Box> { 192 | self.workspace_strings_cache.lock()?.clear(); 193 | 194 | self.known_workspaces 195 | .lock()? 196 | .iter() 197 | .for_each(|&id| rename_cmd(id, "", &config.format, &config.workspaces_name)); 198 | 199 | Ok(()) 200 | } 201 | 202 | pub fn start_listeners(self: &Arc) { 203 | let mut event_listener = EventListener::new(); 204 | 205 | rename_workspace_if!( 206 | self, 207 | event_listener, 208 | add_window_opened_handler, 209 | add_window_closed_handler, 210 | add_window_moved_handler, 211 | add_active_window_changed_handler, 212 | add_workspace_added_handler, 213 | add_workspace_moved_handler, 214 | add_workspace_changed_handler, 215 | add_fullscreen_state_changed_handler, 216 | add_window_title_changed_handler 217 | ); 218 | 219 | let this = self.clone(); 220 | event_listener.add_workspace_deleted_handler(move |wt| { 221 | _ = this.rename_workspace(); 222 | _ = this.remove_workspace(wt); 223 | }); 224 | 225 | _ = event_listener.start_listener(); 226 | } 227 | 228 | pub fn watch_config_changes( 229 | &self, 230 | cfg_path: Option, 231 | ) -> Result<(), Box> { 232 | match &cfg_path { 233 | Some(cfg_path) => { 234 | loop { 235 | // Watch for modify events. 236 | let mut notify = Inotify::init()?; 237 | 238 | notify.watches().add(cfg_path, WatchMask::MODIFY)?; 239 | let mut buffer = [0; 1024]; 240 | notify.read_events_blocking(&mut buffer)?.last(); 241 | 242 | println!("Reloading config !"); 243 | // Clojure to force quick release of lock 244 | { 245 | match Config::new(cfg_path.clone(), false, false) { 246 | Ok(config) => self.cfg.lock()?.config = config.config, 247 | Err(err) => println!("Unable to reload config: {err:?}"), 248 | } 249 | } 250 | 251 | // Handle event 252 | // Run on window events 253 | _ = self.rename_workspace(); 254 | } 255 | } 256 | None => Ok(()), 257 | } 258 | } 259 | 260 | fn remove_workspace(&self, wt: WorkspaceEventData) -> Result> { 261 | Ok(self.known_workspaces.lock()?.remove(&wt.id)) 262 | } 263 | } 264 | 265 | fn rename_empty_workspace(config: &ConfigFile) { 266 | _ = Workspace::get_active().map(|workspace| { 267 | if workspace.windows == 0 { 268 | rename_cmd(workspace.id, "", &config.format, &config.workspaces_name); 269 | } 270 | }); 271 | } 272 | 273 | fn rename_cmd( 274 | id: i32, 275 | clients: &str, 276 | config_format: &ConfigFormatRaw, 277 | workspaces_name: &[(String, String)], 278 | ) { 279 | let workspace_fmt = &config_format.workspace.to_string(); 280 | let workspace_empty_fmt = &config_format.workspace_empty.to_string(); 281 | let id_two_digits = format!("{:02}", id); 282 | let workspace_name = get_workspace_name(id, workspaces_name); 283 | 284 | let mut vars = HashMap::from([ 285 | ("id".to_string(), id.to_string()), 286 | ("id_long".to_string(), id_two_digits), 287 | ("name".to_string(), workspace_name), 288 | ("delim".to_string(), config_format.delim.to_string()), 289 | ]); 290 | 291 | vars.insert("clients".to_string(), clients.to_string()); 292 | let workspace = if !clients.is_empty() { 293 | formatter(workspace_fmt, &vars) 294 | } else { 295 | formatter(workspace_empty_fmt, &vars) 296 | }; 297 | 298 | let _ = hyprland::dispatch!(RenameWorkspace, id, Some(workspace.trim())); 299 | } 300 | 301 | fn get_workspace_name(id: i32, workspaces_name: &[(String, String)]) -> String { 302 | let default_workspace_name = id.to_string(); 303 | workspaces_name 304 | .iter() 305 | .find_map(|(x, name)| { 306 | if x.eq(&id.to_string()) { 307 | Some(name) 308 | } else { 309 | None 310 | } 311 | }) 312 | .unwrap_or(&default_workspace_name) 313 | .to_string() 314 | } 315 | 316 | fn get_filtered_clients(config: &ConfigFile) -> Vec { 317 | let binding = Clients::get().unwrap(); 318 | let config_exclude = &config.exclude; 319 | 320 | binding 321 | .into_iter() 322 | .filter(|client| client.pid > 0) 323 | .filter(|client| { 324 | !config_exclude.iter().any(|(class, title)| { 325 | class.is_match(&client.class) && (title.is_match(&client.title)) 326 | }) 327 | }) 328 | .collect::>() 329 | } 330 | 331 | fn get_active_client() -> String { 332 | Client::get_active() 333 | .unwrap_or(None) 334 | .map(|x| x.address) 335 | .unwrap_or(Address::new("0")) 336 | .to_string() 337 | } 338 | 339 | #[cfg(test)] 340 | mod tests { 341 | use regex::Regex; 342 | 343 | use super::*; 344 | use crate::renamer::IconConfig::*; 345 | use crate::renamer::IconStatus::*; 346 | 347 | #[test] 348 | fn test_app_client_partial_eq() { 349 | let client1 = AppClient { 350 | initial_class: "kitty".to_string(), 351 | class: "kitty".to_string(), 352 | title: "~".to_string(), 353 | is_active: false, 354 | is_fullscreen: FullscreenMode::Fullscreen, 355 | initial_title: "zsh".to_string(), 356 | matched_rule: Inactive(Class("(kitty|alacritty)".to_string(), "term".to_string())), 357 | is_dedup_inactive_fullscreen: false, 358 | }; 359 | 360 | let client2 = AppClient { 361 | initial_class: "alacritty".to_string(), 362 | class: "alacritty".to_string(), 363 | title: "xplr".to_string(), 364 | initial_title: "zsh".to_string(), 365 | is_active: false, 366 | is_fullscreen: FullscreenMode::Fullscreen, 367 | matched_rule: Inactive(Class("(kitty|alacritty)".to_string(), "term".to_string())), 368 | is_dedup_inactive_fullscreen: false, 369 | }; 370 | 371 | let client3 = AppClient { 372 | initial_class: "kitty".to_string(), 373 | class: "kitty".to_string(), 374 | title: "".to_string(), 375 | initial_title: "zsh".to_string(), 376 | is_active: true, 377 | is_fullscreen: FullscreenMode::None, 378 | matched_rule: Active(Class("(kitty|alacritty)".to_string(), "term".to_string())), 379 | is_dedup_inactive_fullscreen: false, 380 | }; 381 | 382 | let client4 = AppClient { 383 | initial_class: "alacritty".to_string(), 384 | class: "alacritty".to_string(), 385 | title: "".to_string(), 386 | initial_title: "zsh".to_string(), 387 | is_active: false, 388 | is_fullscreen: FullscreenMode::Fullscreen, 389 | matched_rule: Inactive(Class("(kitty|alacritty)".to_string(), "term".to_string())), 390 | is_dedup_inactive_fullscreen: false, 391 | }; 392 | 393 | let client5 = AppClient { 394 | initial_class: "kitty".to_string(), 395 | class: "kitty".to_string(), 396 | title: "".to_string(), 397 | initial_title: "zsh".to_string(), 398 | is_active: false, 399 | is_fullscreen: FullscreenMode::Fullscreen, 400 | matched_rule: Inactive(Class("(kitty|alacritty)".to_string(), "term".to_string())), 401 | is_dedup_inactive_fullscreen: false, 402 | }; 403 | 404 | let client6 = AppClient { 405 | initial_class: "alacritty".to_string(), 406 | class: "alacritty".to_string(), 407 | title: "".to_string(), 408 | initial_title: "zsh".to_string(), 409 | is_active: false, 410 | is_fullscreen: FullscreenMode::None, 411 | matched_rule: Inactive(Class("alacritty".to_string(), "term".to_string())), 412 | is_dedup_inactive_fullscreen: false, 413 | }; 414 | 415 | assert_eq!(client1 == client2, true); 416 | assert_eq!(client4 == client5, true); 417 | assert_eq!(client1 == client4, true); 418 | assert_eq!(client1 == client3, false); 419 | assert_eq!(client5 == client6, false); 420 | } 421 | 422 | #[test] 423 | fn test_dedup_kitty_and_alacritty_if_one_regex() { 424 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 425 | config 426 | .class 427 | .push((Regex::new("(kitty|alacritty)").unwrap(), "term".to_string())); 428 | 429 | config.format.dedup = true; 430 | config.format.client_dup = "{icon}{counter}".to_string(); 431 | 432 | let renamer = Renamer::new( 433 | Config { 434 | cfg_path: None, 435 | config: config.clone(), 436 | }, 437 | Args { 438 | verbose: false, 439 | debug: false, 440 | config: None, 441 | dump: false, 442 | migrate_config: false, 443 | }, 444 | ); 445 | 446 | let expected = [(1, "term5".to_string())].into_iter().collect(); 447 | 448 | let actual = renamer.generate_workspaces_string( 449 | vec![AppWorkspace { 450 | id: 1, 451 | clients: vec![ 452 | AppClient { 453 | initial_class: "kitty".to_string(), 454 | class: "kitty".to_string(), 455 | title: "kitty".to_string(), 456 | initial_title: "kitty".to_string(), 457 | is_active: false, 458 | is_fullscreen: FullscreenMode::None, 459 | matched_rule: renamer.parse_icon( 460 | "kitty".to_string(), 461 | "kitty".to_string(), 462 | "kitty".to_string(), 463 | "kitty".to_string(), 464 | false, 465 | &config, 466 | ), 467 | is_dedup_inactive_fullscreen: false, 468 | }, 469 | AppClient { 470 | class: "kitty".to_string(), 471 | initial_class: "kitty".to_string(), 472 | title: "kitty".to_string(), 473 | initial_title: "kitty".to_string(), 474 | is_active: false, 475 | is_fullscreen: FullscreenMode::None, 476 | matched_rule: renamer.parse_icon( 477 | "kitty".to_string(), 478 | "kitty".to_string(), 479 | "kitty".to_string(), 480 | "kitty".to_string(), 481 | false, 482 | &config, 483 | ), 484 | is_dedup_inactive_fullscreen: false, 485 | }, 486 | AppClient { 487 | initial_class: "alacritty".to_string(), 488 | class: "alacritty".to_string(), 489 | title: "alacritty".to_string(), 490 | initial_title: "alacritty".to_string(), 491 | is_active: false, 492 | is_fullscreen: FullscreenMode::None, 493 | matched_rule: renamer.parse_icon( 494 | "alacritty".to_string(), 495 | "alacritty".to_string(), 496 | "alacritty".to_string(), 497 | "alacritty".to_string(), 498 | false, 499 | &config, 500 | ), 501 | is_dedup_inactive_fullscreen: false, 502 | }, 503 | AppClient { 504 | class: "alacritty".to_string(), 505 | initial_class: "alacritty".to_string(), 506 | title: "alacritty".to_string(), 507 | initial_title: "alacritty".to_string(), 508 | is_active: false, 509 | is_fullscreen: FullscreenMode::None, 510 | matched_rule: renamer.parse_icon( 511 | "alacritty".to_string(), 512 | "alacritty".to_string(), 513 | "alacritty".to_string(), 514 | "alacritty".to_string(), 515 | false, 516 | &config, 517 | ), 518 | is_dedup_inactive_fullscreen: false, 519 | }, 520 | AppClient { 521 | initial_class: "alacritty".to_string(), 522 | class: "alacritty".to_string(), 523 | title: "alacritty".to_string(), 524 | initial_title: "alacritty".to_string(), 525 | is_active: false, 526 | is_fullscreen: FullscreenMode::None, 527 | matched_rule: renamer.parse_icon( 528 | "alacritty".to_string(), 529 | "alacritty".to_string(), 530 | "alacritty".to_string(), 531 | "alacritty".to_string(), 532 | false, 533 | &config, 534 | ), 535 | is_dedup_inactive_fullscreen: false, 536 | }, 537 | ], 538 | }], 539 | &config, 540 | ); 541 | 542 | assert_eq!(actual, expected); 543 | } 544 | 545 | #[test] 546 | fn test_parse_icon_initial_title_and_initial_title_active() { 547 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 548 | config 549 | .class 550 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 551 | 552 | config 553 | .class 554 | .push((Regex::new("alacritty").unwrap(), "term".to_string())); 555 | 556 | config.initial_title_in_class.push(( 557 | Regex::new("(kitty|alacritty)").unwrap(), 558 | vec![(Regex::new("zsh").unwrap(), "Zsh".to_string())], 559 | )); 560 | 561 | config.initial_title_in_class_active.push(( 562 | Regex::new("alacritty").unwrap(), 563 | vec![(Regex::new("zsh").unwrap(), "#Zsh#".to_string())], 564 | )); 565 | 566 | config.format.client_dup = "{icon}{counter}".to_string(); 567 | 568 | let renamer = Renamer::new( 569 | Config { 570 | cfg_path: None, 571 | config: config.clone(), 572 | }, 573 | Args { 574 | verbose: false, 575 | debug: false, 576 | config: None, 577 | dump: false, 578 | migrate_config: false, 579 | }, 580 | ); 581 | 582 | let expected = [(1, "Zsh #Zsh# *Zsh*".to_string())].into_iter().collect(); 583 | 584 | let actual = renamer.generate_workspaces_string( 585 | vec![AppWorkspace { 586 | id: 1, 587 | clients: vec![ 588 | AppClient { 589 | initial_class: "alacritty".to_string(), 590 | class: "alacritty".to_string(), 591 | title: "alacritty".to_string(), 592 | initial_title: "zsh".to_string(), 593 | is_active: false, 594 | is_fullscreen: FullscreenMode::None, 595 | matched_rule: renamer.parse_icon( 596 | "alacritty".to_string(), 597 | "alacritty".to_string(), 598 | "zsh".to_string(), 599 | "alacritty".to_string(), 600 | false, 601 | &config, 602 | ), 603 | is_dedup_inactive_fullscreen: false, 604 | }, 605 | AppClient { 606 | initial_class: "alacritty".to_string(), 607 | class: "alacritty".to_string(), 608 | title: "alacritty".to_string(), 609 | initial_title: "zsh".to_string(), 610 | is_active: true, 611 | is_fullscreen: FullscreenMode::None, 612 | matched_rule: renamer.parse_icon( 613 | "alacritty".to_string(), 614 | "alacritty".to_string(), 615 | "zsh".to_string(), 616 | "alacritty".to_string(), 617 | true, 618 | &config, 619 | ), 620 | is_dedup_inactive_fullscreen: false, 621 | }, 622 | AppClient { 623 | initial_class: "kitty".to_string(), 624 | class: "kitty".to_string(), 625 | title: "~".to_string(), 626 | initial_title: "zsh".to_string(), 627 | is_active: true, 628 | is_fullscreen: FullscreenMode::None, 629 | matched_rule: renamer.parse_icon( 630 | "kitty".to_string(), 631 | "kitty".to_string(), 632 | "zsh".to_string(), 633 | "~".to_string(), 634 | true, 635 | &config, 636 | ), 637 | is_dedup_inactive_fullscreen: false, 638 | }, 639 | ], 640 | }], 641 | &config, 642 | ); 643 | assert_eq!(actual, expected); 644 | } 645 | 646 | #[test] 647 | fn test_dedup_kitty_and_alacritty_if_two_regex() { 648 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 649 | config 650 | .class 651 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 652 | 653 | config 654 | .class 655 | .push((Regex::new("alacritty").unwrap(), "term".to_string())); 656 | 657 | config.format.dedup = true; 658 | config.format.client_dup = "{icon}{counter}".to_string(); 659 | 660 | let renamer = Renamer::new( 661 | Config { 662 | cfg_path: None, 663 | config: config.clone(), 664 | }, 665 | Args { 666 | verbose: false, 667 | debug: false, 668 | config: None, 669 | dump: false, 670 | migrate_config: false, 671 | }, 672 | ); 673 | 674 | let expected = [(1, "term2 term3".to_string())].into_iter().collect(); 675 | 676 | let actual = renamer.generate_workspaces_string( 677 | vec![AppWorkspace { 678 | id: 1, 679 | clients: vec![ 680 | AppClient { 681 | class: "kitty".to_string(), 682 | title: "kitty".to_string(), 683 | initial_class: "kitty".to_string(), 684 | initial_title: "kitty".to_string(), 685 | is_active: false, 686 | is_fullscreen: FullscreenMode::None, 687 | matched_rule: renamer.parse_icon( 688 | "kitty".to_string(), 689 | "kitty".to_string(), 690 | "kitty".to_string(), 691 | "kitty".to_string(), 692 | false, 693 | &config, 694 | ), 695 | is_dedup_inactive_fullscreen: false, 696 | }, 697 | AppClient { 698 | class: "kitty".to_string(), 699 | initial_class: "kitty".to_string(), 700 | title: "kitty".to_string(), 701 | initial_title: "kitty".to_string(), 702 | is_active: false, 703 | is_fullscreen: FullscreenMode::None, 704 | matched_rule: renamer.parse_icon( 705 | "kitty".to_string(), 706 | "kitty".to_string(), 707 | "kitty".to_string(), 708 | "kitty".to_string(), 709 | false, 710 | &config, 711 | ), 712 | is_dedup_inactive_fullscreen: false, 713 | }, 714 | AppClient { 715 | class: "alacritty".to_string(), 716 | initial_class: "alacritty".to_string(), 717 | title: "alacritty".to_string(), 718 | initial_title: "alacritty".to_string(), 719 | is_active: false, 720 | is_fullscreen: FullscreenMode::None, 721 | matched_rule: renamer.parse_icon( 722 | "alacritty".to_string(), 723 | "alacritty".to_string(), 724 | "alacritty".to_string(), 725 | "alacritty".to_string(), 726 | false, 727 | &config, 728 | ), 729 | is_dedup_inactive_fullscreen: false, 730 | }, 731 | AppClient { 732 | class: "alacritty".to_string(), 733 | initial_class: "alacritty".to_string(), 734 | title: "alacritty".to_string(), 735 | initial_title: "alacritty".to_string(), 736 | is_active: false, 737 | is_fullscreen: FullscreenMode::None, 738 | matched_rule: renamer.parse_icon( 739 | "alacritty".to_string(), 740 | "alacritty".to_string(), 741 | "alacritty".to_string(), 742 | "alacritty".to_string(), 743 | false, 744 | &config, 745 | ), 746 | is_dedup_inactive_fullscreen: false, 747 | }, 748 | AppClient { 749 | initial_class: "alacritty".to_string(), 750 | class: "alacritty".to_string(), 751 | title: "alacritty".to_string(), 752 | initial_title: "alacritty".to_string(), 753 | is_active: false, 754 | is_fullscreen: FullscreenMode::None, 755 | matched_rule: renamer.parse_icon( 756 | "alacritty".to_string(), 757 | "alacritty".to_string(), 758 | "alacritty".to_string(), 759 | "alacritty".to_string(), 760 | false, 761 | &config, 762 | ), 763 | is_dedup_inactive_fullscreen: false, 764 | }, 765 | ], 766 | }], 767 | &config, 768 | ); 769 | 770 | assert_eq!(actual, expected); 771 | } 772 | 773 | #[test] 774 | fn test_to_superscript() { 775 | let input = 1234567890; 776 | let expected = "¹²³⁴⁵⁶⁷⁸⁹⁰"; 777 | let output = to_superscript(input); 778 | assert_eq!(expected, output); 779 | } 780 | 781 | #[test] 782 | fn test_no_dedup_no_focus_no_fullscreen_one_workspace() { 783 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 784 | config 785 | .class 786 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 787 | 788 | let renamer = Renamer::new( 789 | Config { 790 | cfg_path: None, 791 | config: config.clone(), 792 | }, 793 | Args { 794 | verbose: false, 795 | debug: false, 796 | config: None, 797 | dump: false, 798 | migrate_config: false, 799 | }, 800 | ); 801 | 802 | let expected = [(1, "term term term term term".to_string())] 803 | .into_iter() 804 | .collect(); 805 | 806 | let actual = renamer.generate_workspaces_string( 807 | vec![AppWorkspace { 808 | id: 1, 809 | clients: vec![ 810 | AppClient { 811 | initial_class: "kitty".to_string(), 812 | class: "kitty".to_string(), 813 | title: "kitty".to_string(), 814 | initial_title: "kitty".to_string(), 815 | is_active: false, 816 | is_fullscreen: FullscreenMode::None, 817 | matched_rule: renamer.parse_icon( 818 | "kitty".to_string(), 819 | "kitty".to_string(), 820 | "kitty".to_string(), 821 | "kitty".to_string(), 822 | false, 823 | &config, 824 | ), 825 | is_dedup_inactive_fullscreen: false, 826 | }, 827 | AppClient { 828 | class: "kitty".to_string(), 829 | initial_class: "kitty".to_string(), 830 | title: "kitty".to_string(), 831 | initial_title: "kitty".to_string(), 832 | is_active: false, 833 | is_fullscreen: FullscreenMode::None, 834 | matched_rule: renamer.parse_icon( 835 | "kitty".to_string(), 836 | "kitty".to_string(), 837 | "kitty".to_string(), 838 | "kitty".to_string(), 839 | false, 840 | &config, 841 | ), 842 | is_dedup_inactive_fullscreen: false, 843 | }, 844 | AppClient { 845 | class: "kitty".to_string(), 846 | initial_class: "kitty".to_string(), 847 | title: "kitty".to_string(), 848 | initial_title: "kitty".to_string(), 849 | is_active: false, 850 | is_fullscreen: FullscreenMode::None, 851 | matched_rule: renamer.parse_icon( 852 | "kitty".to_string(), 853 | "kitty".to_string(), 854 | "kitty".to_string(), 855 | "kitty".to_string(), 856 | false, 857 | &config, 858 | ), 859 | is_dedup_inactive_fullscreen: false, 860 | }, 861 | AppClient { 862 | initial_class: "kitty".to_string(), 863 | class: "kitty".to_string(), 864 | title: "kitty".to_string(), 865 | initial_title: "kitty".to_string(), 866 | is_active: false, 867 | is_fullscreen: FullscreenMode::None, 868 | matched_rule: renamer.parse_icon( 869 | "kitty".to_string(), 870 | "kitty".to_string(), 871 | "kitty".to_string(), 872 | "kitty".to_string(), 873 | false, 874 | &config, 875 | ), 876 | is_dedup_inactive_fullscreen: false, 877 | }, 878 | AppClient { 879 | class: "kitty".to_string(), 880 | initial_class: "kitty".to_string(), 881 | title: "kitty".to_string(), 882 | initial_title: "kitty".to_string(), 883 | is_active: false, 884 | is_fullscreen: FullscreenMode::None, 885 | matched_rule: renamer.parse_icon( 886 | "kitty".to_string(), 887 | "kitty".to_string(), 888 | "kitty".to_string(), 889 | "kitty".to_string(), 890 | false, 891 | &config, 892 | ), 893 | is_dedup_inactive_fullscreen: false, 894 | }, 895 | ], 896 | }], 897 | &config, 898 | ); 899 | 900 | assert_eq!(actual, expected); 901 | } 902 | 903 | #[test] 904 | fn test_no_dedup_focus_no_fullscreen_one_workspace_middle() { 905 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 906 | config 907 | .class 908 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 909 | config.format.client_active = "*{icon}*".to_string(); 910 | 911 | let renamer = Renamer::new( 912 | Config { 913 | cfg_path: None, 914 | config: config.clone(), 915 | }, 916 | Args { 917 | verbose: false, 918 | debug: false, 919 | dump: false, 920 | config: None, 921 | migrate_config: false, 922 | }, 923 | ); 924 | 925 | let expected = [(1, "term term *term* term term".to_string())] 926 | .into_iter() 927 | .collect(); 928 | 929 | let actual = renamer.generate_workspaces_string( 930 | vec![AppWorkspace { 931 | id: 1, 932 | clients: vec![ 933 | AppClient { 934 | initial_class: "kitty".to_string(), 935 | class: "kitty".to_string(), 936 | title: "kitty".to_string(), 937 | initial_title: "kitty".to_string(), 938 | is_active: false, 939 | is_fullscreen: FullscreenMode::None, 940 | matched_rule: renamer.parse_icon( 941 | "kitty".to_string(), 942 | "kitty".to_string(), 943 | "kitty".to_string(), 944 | "kitty".to_string(), 945 | false, 946 | &config, 947 | ), 948 | is_dedup_inactive_fullscreen: false, 949 | }, 950 | AppClient { 951 | initial_class: "kitty".to_string(), 952 | class: "kitty".to_string(), 953 | title: "kitty".to_string(), 954 | initial_title: "kitty".to_string(), 955 | is_active: false, 956 | is_fullscreen: FullscreenMode::None, 957 | matched_rule: renamer.parse_icon( 958 | "kitty".to_string(), 959 | "kitty".to_string(), 960 | "kitty".to_string(), 961 | "kitty".to_string(), 962 | false, 963 | &config, 964 | ), 965 | is_dedup_inactive_fullscreen: false, 966 | }, 967 | AppClient { 968 | class: "kitty".to_string(), 969 | initial_class: "kitty".to_string(), 970 | title: "kitty".to_string(), 971 | initial_title: "kitty".to_string(), 972 | is_active: true, 973 | is_fullscreen: FullscreenMode::None, 974 | matched_rule: renamer.parse_icon( 975 | "kitty".to_string(), 976 | "kitty".to_string(), 977 | "kitty".to_string(), 978 | "kitty".to_string(), 979 | true, 980 | &config, 981 | ), 982 | is_dedup_inactive_fullscreen: false, 983 | }, 984 | AppClient { 985 | class: "kitty".to_string(), 986 | initial_class: "kitty".to_string(), 987 | title: "kitty".to_string(), 988 | initial_title: "kitty".to_string(), 989 | is_active: false, 990 | is_fullscreen: FullscreenMode::None, 991 | matched_rule: renamer.parse_icon( 992 | "kitty".to_string(), 993 | "kitty".to_string(), 994 | "kitty".to_string(), 995 | "kitty".to_string(), 996 | false, 997 | &config, 998 | ), 999 | is_dedup_inactive_fullscreen: false, 1000 | }, 1001 | AppClient { 1002 | class: "kitty".to_string(), 1003 | initial_class: "kitty".to_string(), 1004 | title: "kitty".to_string(), 1005 | initial_title: "kitty".to_string(), 1006 | is_active: false, 1007 | is_fullscreen: FullscreenMode::None, 1008 | matched_rule: renamer.parse_icon( 1009 | "kitty".to_string(), 1010 | "kitty".to_string(), 1011 | "kitty".to_string(), 1012 | "kitty".to_string(), 1013 | false, 1014 | &config, 1015 | ), 1016 | is_dedup_inactive_fullscreen: false, 1017 | }, 1018 | ], 1019 | }], 1020 | &config, 1021 | ); 1022 | 1023 | assert_eq!(actual, expected); 1024 | } 1025 | 1026 | #[test] 1027 | fn test_no_dedup_no_focus_fullscreen_one_workspace_middle() { 1028 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1029 | config 1030 | .class 1031 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1032 | config.format.client_active = "*{icon}*".to_string(); 1033 | config.format.client_fullscreen = "[{icon}]".to_string(); 1034 | 1035 | let renamer = Renamer::new( 1036 | Config { 1037 | cfg_path: None, 1038 | config: config.clone(), 1039 | }, 1040 | Args { 1041 | verbose: false, 1042 | debug: false, 1043 | dump: false, 1044 | migrate_config: false, 1045 | config: None, 1046 | }, 1047 | ); 1048 | 1049 | let expected = [(1, "term term [term] term term".to_string())] 1050 | .into_iter() 1051 | .collect(); 1052 | 1053 | let actual = renamer.generate_workspaces_string( 1054 | vec![AppWorkspace { 1055 | id: 1, 1056 | clients: vec![ 1057 | AppClient { 1058 | initial_class: "kitty".to_string(), 1059 | class: "kitty".to_string(), 1060 | title: "kitty".to_string(), 1061 | initial_title: "kitty".to_string(), 1062 | is_active: false, 1063 | is_fullscreen: FullscreenMode::None, 1064 | matched_rule: renamer.parse_icon( 1065 | "kitty".to_string(), 1066 | "kitty".to_string(), 1067 | "kitty".to_string(), 1068 | "kitty".to_string(), 1069 | false, 1070 | &config, 1071 | ), 1072 | is_dedup_inactive_fullscreen: false, 1073 | }, 1074 | AppClient { 1075 | class: "kitty".to_string(), 1076 | initial_class: "kitty".to_string(), 1077 | title: "kitty".to_string(), 1078 | initial_title: "kitty".to_string(), 1079 | is_active: false, 1080 | is_fullscreen: FullscreenMode::None, 1081 | matched_rule: renamer.parse_icon( 1082 | "kitty".to_string(), 1083 | "kitty".to_string(), 1084 | "kitty".to_string(), 1085 | "kitty".to_string(), 1086 | false, 1087 | &config, 1088 | ), 1089 | is_dedup_inactive_fullscreen: false, 1090 | }, 1091 | AppClient { 1092 | class: "kitty".to_string(), 1093 | initial_class: "kitty".to_string(), 1094 | title: "kitty".to_string(), 1095 | initial_title: "kitty".to_string(), 1096 | is_active: false, 1097 | is_fullscreen: FullscreenMode::Fullscreen, 1098 | matched_rule: renamer.parse_icon( 1099 | "kitty".to_string(), 1100 | "kitty".to_string(), 1101 | "kitty".to_string(), 1102 | "kitty".to_string(), 1103 | false, 1104 | &config, 1105 | ), 1106 | is_dedup_inactive_fullscreen: false, 1107 | }, 1108 | AppClient { 1109 | class: "kitty".to_string(), 1110 | initial_class: "kitty".to_string(), 1111 | title: "kitty".to_string(), 1112 | initial_title: "kitty".to_string(), 1113 | is_active: false, 1114 | is_fullscreen: FullscreenMode::None, 1115 | matched_rule: renamer.parse_icon( 1116 | "kitty".to_string(), 1117 | "kitty".to_string(), 1118 | "kitty".to_string(), 1119 | "kitty".to_string(), 1120 | false, 1121 | &config, 1122 | ), 1123 | is_dedup_inactive_fullscreen: false, 1124 | }, 1125 | AppClient { 1126 | initial_class: "kitty".to_string(), 1127 | class: "kitty".to_string(), 1128 | title: "kitty".to_string(), 1129 | initial_title: "kitty".to_string(), 1130 | is_active: false, 1131 | is_fullscreen: FullscreenMode::None, 1132 | matched_rule: renamer.parse_icon( 1133 | "kitty".to_string(), 1134 | "kitty".to_string(), 1135 | "kitty".to_string(), 1136 | "kitty".to_string(), 1137 | false, 1138 | &config, 1139 | ), 1140 | is_dedup_inactive_fullscreen: false, 1141 | }, 1142 | ], 1143 | }], 1144 | &config, 1145 | ); 1146 | 1147 | assert_eq!(actual, expected); 1148 | } 1149 | 1150 | #[test] 1151 | fn test_no_dedup_focus_fullscreen_one_workspace_middle() { 1152 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1153 | config 1154 | .class 1155 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1156 | config.format.client_active = "*{icon}*".to_string(); 1157 | config.format.client_fullscreen = "[{icon}]".to_string(); 1158 | 1159 | let renamer = Renamer::new( 1160 | Config { 1161 | cfg_path: None, 1162 | config: config.clone(), 1163 | }, 1164 | Args { 1165 | verbose: false, 1166 | debug: false, 1167 | dump: false, 1168 | migrate_config: false, 1169 | config: None, 1170 | }, 1171 | ); 1172 | 1173 | let expected = [(1, "term term [*term*] term term".to_string())] 1174 | .into_iter() 1175 | .collect(); 1176 | 1177 | let actual = renamer.generate_workspaces_string( 1178 | vec![AppWorkspace { 1179 | id: 1, 1180 | clients: vec![ 1181 | AppClient { 1182 | class: "kitty".to_string(), 1183 | initial_class: "kitty".to_string(), 1184 | title: "kitty".to_string(), 1185 | initial_title: "kitty".to_string(), 1186 | is_active: false, 1187 | is_fullscreen: FullscreenMode::None, 1188 | matched_rule: renamer.parse_icon( 1189 | "kitty".to_string(), 1190 | "kitty".to_string(), 1191 | "kitty".to_string(), 1192 | "kitty".to_string(), 1193 | false, 1194 | &config, 1195 | ), 1196 | is_dedup_inactive_fullscreen: false, 1197 | }, 1198 | AppClient { 1199 | class: "kitty".to_string(), 1200 | initial_class: "kitty".to_string(), 1201 | title: "kitty".to_string(), 1202 | initial_title: "kitty".to_string(), 1203 | is_active: false, 1204 | is_fullscreen: FullscreenMode::None, 1205 | matched_rule: renamer.parse_icon( 1206 | "kitty".to_string(), 1207 | "kitty".to_string(), 1208 | "kitty".to_string(), 1209 | "kitty".to_string(), 1210 | false, 1211 | &config, 1212 | ), 1213 | is_dedup_inactive_fullscreen: false, 1214 | }, 1215 | AppClient { 1216 | class: "kitty".to_string(), 1217 | initial_class: "kitty".to_string(), 1218 | title: "kitty".to_string(), 1219 | initial_title: "kitty".to_string(), 1220 | is_active: true, 1221 | is_fullscreen: FullscreenMode::Fullscreen, 1222 | matched_rule: renamer.parse_icon( 1223 | "kitty".to_string(), 1224 | "kitty".to_string(), 1225 | "kitty".to_string(), 1226 | "kitty".to_string(), 1227 | true, 1228 | &config, 1229 | ), 1230 | is_dedup_inactive_fullscreen: false, 1231 | }, 1232 | AppClient { 1233 | class: "kitty".to_string(), 1234 | initial_class: "kitty".to_string(), 1235 | title: "kitty".to_string(), 1236 | initial_title: "kitty".to_string(), 1237 | is_active: false, 1238 | is_fullscreen: FullscreenMode::None, 1239 | matched_rule: renamer.parse_icon( 1240 | "kitty".to_string(), 1241 | "kitty".to_string(), 1242 | "kitty".to_string(), 1243 | "kitty".to_string(), 1244 | false, 1245 | &config, 1246 | ), 1247 | is_dedup_inactive_fullscreen: false, 1248 | }, 1249 | AppClient { 1250 | class: "kitty".to_string(), 1251 | initial_class: "kitty".to_string(), 1252 | title: "kitty".to_string(), 1253 | initial_title: "kitty".to_string(), 1254 | is_active: false, 1255 | is_fullscreen: FullscreenMode::None, 1256 | matched_rule: renamer.parse_icon( 1257 | "kitty".to_string(), 1258 | "kitty".to_string(), 1259 | "kitty".to_string(), 1260 | "kitty".to_string(), 1261 | false, 1262 | &config, 1263 | ), 1264 | is_dedup_inactive_fullscreen: false, 1265 | }, 1266 | ], 1267 | }], 1268 | &config, 1269 | ); 1270 | 1271 | assert_eq!(actual, expected); 1272 | } 1273 | 1274 | #[test] 1275 | fn test_dedup_no_focus_no_fullscreen_one_workspace() { 1276 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1277 | config 1278 | .class 1279 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1280 | config.format.dedup = true; 1281 | config.format.client_dup = "{icon}{counter}".to_string(); 1282 | 1283 | let renamer = Renamer::new( 1284 | Config { 1285 | cfg_path: None, 1286 | config: config.clone(), 1287 | }, 1288 | Args { 1289 | verbose: false, 1290 | debug: false, 1291 | dump: false, 1292 | migrate_config: false, 1293 | config: None, 1294 | }, 1295 | ); 1296 | 1297 | let expected = [(1, "term5".to_string())].into_iter().collect(); 1298 | 1299 | let actual = renamer.generate_workspaces_string( 1300 | vec![AppWorkspace { 1301 | id: 1, 1302 | clients: vec![ 1303 | AppClient { 1304 | initial_class: "kitty".to_string(), 1305 | class: "kitty".to_string(), 1306 | title: "kitty".to_string(), 1307 | initial_title: "kitty".to_string(), 1308 | is_active: false, 1309 | is_fullscreen: FullscreenMode::None, 1310 | matched_rule: Inactive(Class("kitty".to_string(), "term".to_string())), 1311 | is_dedup_inactive_fullscreen: false, 1312 | }, 1313 | AppClient { 1314 | initial_class: "kitty".to_string(), 1315 | class: "kitty".to_string(), 1316 | title: "kitty".to_string(), 1317 | initial_title: "kitty".to_string(), 1318 | is_active: false, 1319 | is_fullscreen: FullscreenMode::None, 1320 | matched_rule: Inactive(Class("kitty".to_string(), "term".to_string())), 1321 | is_dedup_inactive_fullscreen: false, 1322 | }, 1323 | AppClient { 1324 | initial_class: "kitty".to_string(), 1325 | class: "kitty".to_string(), 1326 | title: "kitty".to_string(), 1327 | initial_title: "kitty".to_string(), 1328 | is_active: false, 1329 | is_fullscreen: FullscreenMode::None, 1330 | matched_rule: Inactive(Class("kitty".to_string(), "term".to_string())), 1331 | is_dedup_inactive_fullscreen: false, 1332 | }, 1333 | AppClient { 1334 | initial_class: "kitty".to_string(), 1335 | class: "kitty".to_string(), 1336 | title: "kitty".to_string(), 1337 | initial_title: "kitty".to_string(), 1338 | is_active: false, 1339 | is_fullscreen: FullscreenMode::None, 1340 | matched_rule: Inactive(Class("kitty".to_string(), "term".to_string())), 1341 | is_dedup_inactive_fullscreen: false, 1342 | }, 1343 | AppClient { 1344 | initial_class: "kitty".to_string(), 1345 | class: "kitty".to_string(), 1346 | title: "kitty".to_string(), 1347 | initial_title: "kitty".to_string(), 1348 | is_active: false, 1349 | is_fullscreen: FullscreenMode::None, 1350 | matched_rule: Inactive(Class("kitty".to_string(), "term".to_string())), 1351 | is_dedup_inactive_fullscreen: false, 1352 | }, 1353 | ], 1354 | }], 1355 | &config, 1356 | ); 1357 | 1358 | assert_eq!(actual, expected); 1359 | } 1360 | 1361 | #[test] 1362 | fn test_dedup_focus_no_fullscreen_one_workspace_middle() { 1363 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1364 | config 1365 | .class 1366 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1367 | 1368 | config.format.dedup = true; 1369 | config.format.client_dup = "{icon}{counter}".to_string(); 1370 | config.format.client_active = "*{icon}*".to_string(); 1371 | config.format.client_dup_active = "{icon}{counter_unfocused}".to_string(); 1372 | 1373 | let renamer = Renamer::new( 1374 | Config { 1375 | cfg_path: None, 1376 | config: config.clone(), 1377 | }, 1378 | Args { 1379 | verbose: false, 1380 | debug: false, 1381 | dump: false, 1382 | migrate_config: false, 1383 | config: None, 1384 | }, 1385 | ); 1386 | 1387 | let expected = [(1, "*term* term4".to_string())].into_iter().collect(); 1388 | 1389 | let actual = renamer.generate_workspaces_string( 1390 | vec![AppWorkspace { 1391 | id: 1, 1392 | clients: vec![ 1393 | AppClient { 1394 | class: "kitty".to_string(), 1395 | initial_class: "kitty".to_string(), 1396 | title: "kitty".to_string(), 1397 | initial_title: "kitty".to_string(), 1398 | is_active: false, 1399 | is_fullscreen: FullscreenMode::None, 1400 | matched_rule: renamer.parse_icon( 1401 | "kitty".to_string(), 1402 | "kitty".to_string(), 1403 | "kitty".to_string(), 1404 | "kitty".to_string(), 1405 | false, 1406 | &config, 1407 | ), 1408 | is_dedup_inactive_fullscreen: false, 1409 | }, 1410 | AppClient { 1411 | class: "kitty".to_string(), 1412 | initial_class: "kitty".to_string(), 1413 | title: "kitty".to_string(), 1414 | initial_title: "kitty".to_string(), 1415 | is_active: false, 1416 | is_fullscreen: FullscreenMode::None, 1417 | matched_rule: renamer.parse_icon( 1418 | "kitty".to_string(), 1419 | "kitty".to_string(), 1420 | "kitty".to_string(), 1421 | "kitty".to_string(), 1422 | false, 1423 | &config, 1424 | ), 1425 | is_dedup_inactive_fullscreen: false, 1426 | }, 1427 | AppClient { 1428 | initial_class: "kitty".to_string(), 1429 | class: "kitty".to_string(), 1430 | title: "kitty".to_string(), 1431 | initial_title: "kitty".to_string(), 1432 | is_active: true, 1433 | is_fullscreen: FullscreenMode::None, 1434 | matched_rule: renamer.parse_icon( 1435 | "kitty".to_string(), 1436 | "kitty".to_string(), 1437 | "kitty".to_string(), 1438 | "kitty".to_string(), 1439 | true, 1440 | &config, 1441 | ), 1442 | is_dedup_inactive_fullscreen: false, 1443 | }, 1444 | AppClient { 1445 | initial_class: "kitty".to_string(), 1446 | class: "kitty".to_string(), 1447 | title: "kitty".to_string(), 1448 | initial_title: "kitty".to_string(), 1449 | is_active: false, 1450 | is_fullscreen: FullscreenMode::None, 1451 | matched_rule: renamer.parse_icon( 1452 | "kitty".to_string(), 1453 | "kitty".to_string(), 1454 | "kitty".to_string(), 1455 | "kitty".to_string(), 1456 | false, 1457 | &config, 1458 | ), 1459 | is_dedup_inactive_fullscreen: false, 1460 | }, 1461 | AppClient { 1462 | class: "kitty".to_string(), 1463 | initial_class: "kitty".to_string(), 1464 | title: "kitty".to_string(), 1465 | initial_title: "kitty".to_string(), 1466 | is_active: false, 1467 | is_fullscreen: FullscreenMode::None, 1468 | matched_rule: renamer.parse_icon( 1469 | "kitty".to_string(), 1470 | "kitty".to_string(), 1471 | "kitty".to_string(), 1472 | "kitty".to_string(), 1473 | false, 1474 | &config, 1475 | ), 1476 | is_dedup_inactive_fullscreen: false, 1477 | }, 1478 | ], 1479 | }], 1480 | &config, 1481 | ); 1482 | 1483 | assert_eq!(actual, expected); 1484 | } 1485 | 1486 | #[test] 1487 | fn test_dedup_no_focus_fullscreen_one_workspace_middle() { 1488 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1489 | config 1490 | .class 1491 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1492 | 1493 | config.format.dedup = true; 1494 | config.format.client_dup = "{icon}{counter}".to_string(); 1495 | config.format.client_dup_fullscreen = 1496 | "[{icon}]{delim}{icon}{counter_unfocused_sup}".to_string(); 1497 | 1498 | let renamer = Renamer::new( 1499 | Config { 1500 | cfg_path: None, 1501 | config: config.clone(), 1502 | }, 1503 | Args { 1504 | verbose: false, 1505 | debug: false, 1506 | config: None, 1507 | dump: false, 1508 | migrate_config: false, 1509 | }, 1510 | ); 1511 | 1512 | let expected = [(1, "[term] term4".to_string())].into_iter().collect(); 1513 | 1514 | let actual = renamer.generate_workspaces_string( 1515 | vec![AppWorkspace { 1516 | id: 1, 1517 | clients: vec![ 1518 | AppClient { 1519 | class: "kitty".to_string(), 1520 | initial_class: "kitty".to_string(), 1521 | title: "kitty".to_string(), 1522 | initial_title: "kitty".to_string(), 1523 | is_active: false, 1524 | is_fullscreen: FullscreenMode::None, 1525 | matched_rule: renamer.parse_icon( 1526 | "kitty".to_string(), 1527 | "kitty".to_string(), 1528 | "kitty".to_string(), 1529 | "kitty".to_string(), 1530 | false, 1531 | &config, 1532 | ), 1533 | is_dedup_inactive_fullscreen: false, 1534 | }, 1535 | AppClient { 1536 | class: "kitty".to_string(), 1537 | initial_class: "kitty".to_string(), 1538 | title: "kitty".to_string(), 1539 | initial_title: "kitty".to_string(), 1540 | is_active: false, 1541 | is_fullscreen: FullscreenMode::None, 1542 | matched_rule: renamer.parse_icon( 1543 | "kitty".to_string(), 1544 | "kitty".to_string(), 1545 | "kitty".to_string(), 1546 | "kitty".to_string(), 1547 | false, 1548 | &config, 1549 | ), 1550 | is_dedup_inactive_fullscreen: false, 1551 | }, 1552 | AppClient { 1553 | class: "kitty".to_string(), 1554 | initial_class: "kitty".to_string(), 1555 | title: "kitty".to_string(), 1556 | initial_title: "kitty".to_string(), 1557 | is_active: false, 1558 | is_fullscreen: FullscreenMode::Fullscreen, 1559 | matched_rule: renamer.parse_icon( 1560 | "kitty".to_string(), 1561 | "kitty".to_string(), 1562 | "kitty".to_string(), 1563 | "kitty".to_string(), 1564 | false, 1565 | &config, 1566 | ), 1567 | is_dedup_inactive_fullscreen: false, 1568 | }, 1569 | AppClient { 1570 | class: "kitty".to_string(), 1571 | initial_class: "kitty".to_string(), 1572 | title: "kitty".to_string(), 1573 | initial_title: "kitty".to_string(), 1574 | is_active: false, 1575 | is_fullscreen: FullscreenMode::None, 1576 | matched_rule: renamer.parse_icon( 1577 | "kitty".to_string(), 1578 | "kitty".to_string(), 1579 | "kitty".to_string(), 1580 | "kitty".to_string(), 1581 | false, 1582 | &config, 1583 | ), 1584 | is_dedup_inactive_fullscreen: false, 1585 | }, 1586 | AppClient { 1587 | class: "kitty".to_string(), 1588 | initial_class: "kitty".to_string(), 1589 | title: "kitty".to_string(), 1590 | initial_title: "kitty".to_string(), 1591 | is_active: false, 1592 | is_fullscreen: FullscreenMode::None, 1593 | matched_rule: renamer.parse_icon( 1594 | "kitty".to_string(), 1595 | "kitty".to_string(), 1596 | "kitty".to_string(), 1597 | "kitty".to_string(), 1598 | false, 1599 | &config, 1600 | ), 1601 | is_dedup_inactive_fullscreen: false, 1602 | }, 1603 | ], 1604 | }], 1605 | &config, 1606 | ); 1607 | 1608 | assert_eq!(actual, expected); 1609 | } 1610 | 1611 | #[test] 1612 | fn test_dedup_focus_fullscreen_one_workspace_middle() { 1613 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1614 | config 1615 | .class 1616 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 1617 | config.format.dedup = true; 1618 | config.format.client = "{icon}".to_string(); 1619 | config.format.client_active = "*{icon}*".to_string(); 1620 | config.format.client_fullscreen = "[{icon}]".to_string(); 1621 | config.format.client_dup = "{icon}{counter}".to_string(); 1622 | config.format.client_dup_fullscreen = 1623 | "[{icon}]{delim}{icon}{counter_unfocused}".to_string(); 1624 | config.format.client_dup_active = "*{icon}*{delim}{icon}{counter_unfocused}".to_string(); 1625 | 1626 | let renamer = Renamer::new( 1627 | Config { 1628 | cfg_path: None, 1629 | config: config.clone(), 1630 | }, 1631 | Args { 1632 | verbose: false, 1633 | debug: false, 1634 | config: None, 1635 | dump: false, 1636 | migrate_config: false, 1637 | }, 1638 | ); 1639 | 1640 | let expected = [(1, "[*term*] term4".to_string())].into_iter().collect(); 1641 | 1642 | let actual = renamer.generate_workspaces_string( 1643 | vec![AppWorkspace { 1644 | id: 1, 1645 | clients: vec![ 1646 | AppClient { 1647 | class: "kitty".to_string(), 1648 | initial_class: "kitty".to_string(), 1649 | title: "kitty".to_string(), 1650 | initial_title: "kitty".to_string(), 1651 | is_active: false, 1652 | is_fullscreen: FullscreenMode::None, 1653 | matched_rule: renamer.parse_icon( 1654 | "kitty".to_string(), 1655 | "kitty".to_string(), 1656 | "kitty".to_string(), 1657 | "kitty".to_string(), 1658 | false, 1659 | &config, 1660 | ), 1661 | is_dedup_inactive_fullscreen: false, 1662 | }, 1663 | AppClient { 1664 | class: "kitty".to_string(), 1665 | initial_class: "kitty".to_string(), 1666 | title: "kitty".to_string(), 1667 | initial_title: "kitty".to_string(), 1668 | is_active: false, 1669 | is_fullscreen: FullscreenMode::None, 1670 | matched_rule: renamer.parse_icon( 1671 | "kitty".to_string(), 1672 | "kitty".to_string(), 1673 | "kitty".to_string(), 1674 | "kitty".to_string(), 1675 | false, 1676 | &config, 1677 | ), 1678 | is_dedup_inactive_fullscreen: false, 1679 | }, 1680 | AppClient { 1681 | initial_class: "kitty".to_string(), 1682 | class: "kitty".to_string(), 1683 | title: "kitty".to_string(), 1684 | initial_title: "kitty".to_string(), 1685 | is_active: true, 1686 | is_fullscreen: FullscreenMode::Fullscreen, 1687 | matched_rule: renamer.parse_icon( 1688 | "kitty".to_string(), 1689 | "kitty".to_string(), 1690 | "kitty".to_string(), 1691 | "kitty".to_string(), 1692 | true, 1693 | &config, 1694 | ), 1695 | is_dedup_inactive_fullscreen: false, 1696 | }, 1697 | AppClient { 1698 | class: "kitty".to_string(), 1699 | initial_class: "kitty".to_string(), 1700 | title: "kitty".to_string(), 1701 | initial_title: "kitty".to_string(), 1702 | is_active: false, 1703 | is_fullscreen: FullscreenMode::None, 1704 | matched_rule: renamer.parse_icon( 1705 | "kitty".to_string(), 1706 | "kitty".to_string(), 1707 | "kitty".to_string(), 1708 | "kitty".to_string(), 1709 | false, 1710 | &config, 1711 | ), 1712 | is_dedup_inactive_fullscreen: false, 1713 | }, 1714 | AppClient { 1715 | initial_class: "kitty".to_string(), 1716 | class: "kitty".to_string(), 1717 | title: "kitty".to_string(), 1718 | initial_title: "kitty".to_string(), 1719 | is_active: false, 1720 | is_fullscreen: FullscreenMode::None, 1721 | matched_rule: renamer.parse_icon( 1722 | "kitty".to_string(), 1723 | "kitty".to_string(), 1724 | "kitty".to_string(), 1725 | "kitty".to_string(), 1726 | false, 1727 | &config, 1728 | ), 1729 | is_dedup_inactive_fullscreen: false, 1730 | }, 1731 | ], 1732 | }], 1733 | &config, 1734 | ); 1735 | 1736 | assert_eq!(actual, expected); 1737 | } 1738 | 1739 | #[test] 1740 | fn test_default_active_icon() { 1741 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1742 | config 1743 | .class 1744 | .push((Regex::new("kitty").unwrap(), "k".to_string())); 1745 | config 1746 | .class 1747 | .push((Regex::new("alacritty").unwrap(), "a".to_string())); 1748 | config 1749 | .class 1750 | .push((Regex::new("DEFAULT").unwrap(), "d".to_string())); 1751 | 1752 | config 1753 | .class_active 1754 | .push((Regex::new("kitty").unwrap(), "KKK".to_string())); 1755 | config 1756 | .class_active 1757 | .push((Regex::new("DEFAULT").unwrap(), "DDD".to_string())); 1758 | 1759 | config.format.client_active = "*{icon}*".to_string(); 1760 | 1761 | let renamer = Renamer::new( 1762 | Config { 1763 | cfg_path: None, 1764 | config: config.clone(), 1765 | }, 1766 | Args { 1767 | verbose: false, 1768 | debug: false, 1769 | config: None, 1770 | dump: false, 1771 | migrate_config: false, 1772 | }, 1773 | ); 1774 | 1775 | let expected = [(1, "KKK *a* DDD".to_string())].into_iter().collect(); 1776 | 1777 | let actual = renamer.generate_workspaces_string( 1778 | vec![AppWorkspace { 1779 | id: 1, 1780 | clients: vec![ 1781 | AppClient { 1782 | initial_class: "kitty".to_string(), 1783 | class: "kitty".to_string(), 1784 | title: "kitty".to_string(), 1785 | initial_title: "kitty".to_string(), 1786 | is_active: true, 1787 | is_fullscreen: FullscreenMode::None, 1788 | matched_rule: renamer.parse_icon( 1789 | "kitty".to_string(), 1790 | "kitty".to_string(), 1791 | "kitty".to_string(), 1792 | "kitty".to_string(), 1793 | true, 1794 | &config, 1795 | ), 1796 | is_dedup_inactive_fullscreen: false, 1797 | }, 1798 | AppClient { 1799 | class: "alacritty".to_string(), 1800 | initial_class: "alacritty".to_string(), 1801 | title: "alacritty".to_string(), 1802 | initial_title: "alacritty".to_string(), 1803 | is_active: true, 1804 | is_fullscreen: FullscreenMode::None, 1805 | matched_rule: renamer.parse_icon( 1806 | "alacritty".to_string(), 1807 | "alacritty".to_string(), 1808 | "alacritty".to_string(), 1809 | "alacritty".to_string(), 1810 | true, 1811 | &config, 1812 | ), 1813 | is_dedup_inactive_fullscreen: false, 1814 | }, 1815 | AppClient { 1816 | class: "qute".to_string(), 1817 | initial_class: "qute".to_string(), 1818 | title: "qute".to_string(), 1819 | initial_title: "qute".to_string(), 1820 | is_active: true, 1821 | is_fullscreen: FullscreenMode::None, 1822 | matched_rule: renamer.parse_icon( 1823 | "qute".to_string(), 1824 | "qute".to_string(), 1825 | "qute".to_string(), 1826 | "qute".to_string(), 1827 | true, 1828 | &config, 1829 | ), 1830 | is_dedup_inactive_fullscreen: false, 1831 | }, 1832 | ], 1833 | }], 1834 | &config, 1835 | ); 1836 | 1837 | assert_eq!(actual, expected); 1838 | } 1839 | 1840 | #[test] 1841 | fn test_no_class_but_title_icon() { 1842 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1843 | config.title_in_class.push(( 1844 | Regex::new("^$").unwrap(), 1845 | vec![(Regex::new("(?i)spotify").unwrap(), "spotify".to_string())], 1846 | )); 1847 | 1848 | let renamer = Renamer::new( 1849 | Config { 1850 | cfg_path: None, 1851 | config: config.clone(), 1852 | }, 1853 | Args { 1854 | verbose: false, 1855 | debug: false, 1856 | config: None, 1857 | dump: false, 1858 | migrate_config: false, 1859 | }, 1860 | ); 1861 | 1862 | let expected = [(1, "spotify".to_string())].into_iter().collect(); 1863 | 1864 | let actual = renamer.generate_workspaces_string( 1865 | vec![AppWorkspace { 1866 | id: 1, 1867 | clients: vec![AppClient { 1868 | initial_class: "".to_string(), 1869 | class: "".to_string(), 1870 | title: "spotify".to_string(), 1871 | initial_title: "spotify".to_string(), 1872 | is_active: false, 1873 | is_fullscreen: FullscreenMode::None, 1874 | matched_rule: renamer.parse_icon( 1875 | "".to_string(), 1876 | "".to_string(), 1877 | "spotify".to_string(), 1878 | "spotify".to_string(), 1879 | false, 1880 | &config, 1881 | ), 1882 | is_dedup_inactive_fullscreen: false, 1883 | }], 1884 | }], 1885 | &config, 1886 | ); 1887 | 1888 | assert_eq!(actual, expected); 1889 | } 1890 | 1891 | #[test] 1892 | fn test_class_with_exclam_mark() { 1893 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1894 | 1895 | config 1896 | .class 1897 | .push((Regex::new("osu!").unwrap(), "osu".to_string())); 1898 | 1899 | let renamer = Renamer::new( 1900 | Config { 1901 | cfg_path: None, 1902 | config: config.clone(), 1903 | }, 1904 | Args { 1905 | verbose: false, 1906 | debug: false, 1907 | config: None, 1908 | dump: false, 1909 | migrate_config: false, 1910 | }, 1911 | ); 1912 | 1913 | let expected = [(1, "osu".to_string())].into_iter().collect(); 1914 | 1915 | let actual = renamer.generate_workspaces_string( 1916 | vec![AppWorkspace { 1917 | id: 1, 1918 | clients: vec![AppClient { 1919 | initial_class: "osu!".to_string(), 1920 | class: "osu!".to_string(), 1921 | title: "osu!".to_string(), 1922 | initial_title: "osu!".to_string(), 1923 | is_active: false, 1924 | is_fullscreen: FullscreenMode::None, 1925 | matched_rule: renamer.parse_icon( 1926 | "osu!".to_string(), 1927 | "osu!".to_string(), 1928 | "osu!".to_string(), 1929 | "osu!".to_string(), 1930 | false, 1931 | &config, 1932 | ), 1933 | is_dedup_inactive_fullscreen: false, 1934 | }], 1935 | }], 1936 | &config, 1937 | ); 1938 | 1939 | assert_eq!(actual, expected); 1940 | } 1941 | 1942 | #[test] 1943 | fn test_no_default_class_active_fallback_to_formatted_default_class_inactive() { 1944 | // Test inactive default configuration 1945 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 1946 | 1947 | // Find and replace the DEFAULT entry 1948 | if let Some(idx) = config 1949 | .class 1950 | .iter() 1951 | .position(|(regex, _)| regex.as_str() == "DEFAULT") 1952 | { 1953 | config.class[idx] = ( 1954 | Regex::new("DEFAULT").unwrap(), 1955 | "default inactive".to_string(), 1956 | ); 1957 | } 1958 | 1959 | let renamer = Renamer::new( 1960 | Config { 1961 | cfg_path: None, 1962 | config: config.clone(), 1963 | }, 1964 | Args { 1965 | verbose: false, 1966 | debug: false, 1967 | config: None, 1968 | dump: false, 1969 | migrate_config: false, 1970 | }, 1971 | ); 1972 | 1973 | let expected = [(1, "*default inactive* default inactive".to_string())] 1974 | .into_iter() 1975 | .collect(); 1976 | 1977 | let actual = renamer.generate_workspaces_string( 1978 | vec![AppWorkspace { 1979 | id: 1, 1980 | clients: vec![ 1981 | AppClient { 1982 | initial_class: "fake-app-unknown".to_string(), 1983 | class: "fake-app-unknown".to_string(), 1984 | title: "~".to_string(), 1985 | initial_title: "zsh".to_string(), 1986 | is_active: true, 1987 | is_fullscreen: FullscreenMode::None, 1988 | matched_rule: renamer.parse_icon( 1989 | "fake-app-unknown".to_string(), 1990 | "fake-app-unknown".to_string(), 1991 | "zsh".to_string(), 1992 | "~".to_string(), 1993 | true, 1994 | &config, 1995 | ), 1996 | is_dedup_inactive_fullscreen: false, 1997 | }, 1998 | AppClient { 1999 | initial_class: "fake-app-unknown".to_string(), 2000 | class: "fake-app-unknown".to_string(), 2001 | title: "~".to_string(), 2002 | initial_title: "zsh".to_string(), 2003 | is_active: false, 2004 | is_fullscreen: FullscreenMode::None, 2005 | matched_rule: renamer.parse_icon( 2006 | "fake-app-unknown".to_string(), 2007 | "fake-app-unknown".to_string(), 2008 | "zsh".to_string(), 2009 | "~".to_string(), 2010 | true, 2011 | &config, 2012 | ), 2013 | is_dedup_inactive_fullscreen: false, 2014 | }, 2015 | ], 2016 | }], 2017 | &config, 2018 | ); 2019 | 2020 | assert_eq!(actual, expected); 2021 | } 2022 | 2023 | #[test] 2024 | fn test_no_default_class_active_fallback_to_class_default() { 2025 | // Test active default configuration 2026 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 2027 | 2028 | config 2029 | .class_active 2030 | .push((Regex::new("DEFAULT").unwrap(), "default active".to_string())); 2031 | 2032 | let renamer = Renamer::new( 2033 | Config { 2034 | cfg_path: None, 2035 | config: config.clone(), 2036 | }, 2037 | Args { 2038 | verbose: false, 2039 | debug: false, 2040 | config: None, 2041 | dump: false, 2042 | migrate_config: false, 2043 | }, 2044 | ); 2045 | 2046 | let expected = [(1, "default active".to_string())].into_iter().collect(); 2047 | 2048 | let actual = renamer.generate_workspaces_string( 2049 | vec![AppWorkspace { 2050 | id: 1, 2051 | clients: vec![AppClient { 2052 | initial_class: "kitty".to_string(), 2053 | class: "kitty".to_string(), 2054 | title: "~".to_string(), 2055 | initial_title: "zsh".to_string(), 2056 | is_active: true, 2057 | is_fullscreen: FullscreenMode::None, 2058 | matched_rule: renamer.parse_icon( 2059 | "kitty".to_string(), 2060 | "kitty".to_string(), 2061 | "zsh".to_string(), 2062 | "~".to_string(), 2063 | true, 2064 | &config, 2065 | ), 2066 | is_dedup_inactive_fullscreen: false, 2067 | }], 2068 | }], 2069 | &config, 2070 | ); 2071 | 2072 | assert_eq!(actual, expected); 2073 | 2074 | // Test no active default configuration 2075 | let config = crate::config::read_config_file(None, false, false).unwrap(); 2076 | 2077 | let renamer = Renamer::new( 2078 | Config { 2079 | cfg_path: None, 2080 | config: config.clone(), 2081 | }, 2082 | Args { 2083 | verbose: false, 2084 | debug: false, 2085 | config: None, 2086 | dump: false, 2087 | migrate_config: false, 2088 | }, 2089 | ); 2090 | 2091 | let actual = renamer.generate_workspaces_string( 2092 | vec![AppWorkspace { 2093 | id: 1, 2094 | clients: vec![AppClient { 2095 | initial_class: "kitty".to_string(), 2096 | class: "kitty".to_string(), 2097 | initial_title: "zsh".to_string(), 2098 | title: "~".to_string(), 2099 | is_active: true, 2100 | is_fullscreen: FullscreenMode::None, 2101 | matched_rule: renamer.parse_icon( 2102 | "kitty".to_string(), 2103 | "kitty".to_string(), 2104 | "zsh".to_string(), 2105 | "~".to_string(), 2106 | true, 2107 | &config, 2108 | ), 2109 | is_dedup_inactive_fullscreen: false, 2110 | }], 2111 | }], 2112 | &config, 2113 | ); 2114 | 2115 | // When no active default is configured, the inactive default is used 2116 | // and run through the same formatter as a normal inactive client. 2117 | let expected = [(1, "*\u{f059} kitty*".to_string())].into_iter().collect(); 2118 | 2119 | assert_eq!(actual, expected); 2120 | } 2121 | 2122 | #[test] 2123 | fn test_initial_title_in_initial_class_combos() { 2124 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 2125 | 2126 | config 2127 | .class 2128 | .push((Regex::new("kitty").unwrap(), "term0".to_string())); 2129 | 2130 | config.title_in_class.push(( 2131 | Regex::new("kitty").unwrap(), 2132 | vec![(Regex::new("~").unwrap(), "term1".to_string())], 2133 | )); 2134 | 2135 | config.title_in_initial_class.push(( 2136 | Regex::new("kitty").unwrap(), 2137 | vec![(Regex::new("~").unwrap(), "term2".to_string())], 2138 | )); 2139 | 2140 | let renamer = Renamer::new( 2141 | Config { 2142 | cfg_path: None, 2143 | config: config.clone(), 2144 | }, 2145 | Args { 2146 | verbose: false, 2147 | debug: false, 2148 | config: None, 2149 | dump: false, 2150 | migrate_config: false, 2151 | }, 2152 | ); 2153 | 2154 | let expected = [(1, "term2".to_string())].into_iter().collect(); 2155 | 2156 | let actual = renamer.generate_workspaces_string( 2157 | vec![AppWorkspace { 2158 | id: 1, 2159 | clients: vec![AppClient { 2160 | initial_class: "kitty".to_string(), 2161 | class: "kitty".to_string(), 2162 | title: "~".to_string(), 2163 | initial_title: "zsh".to_string(), 2164 | is_active: false, 2165 | is_fullscreen: FullscreenMode::None, 2166 | is_dedup_inactive_fullscreen: false, 2167 | matched_rule: renamer.parse_icon( 2168 | "kitty".to_string(), 2169 | "kitty".to_string(), 2170 | "zsh".to_string(), 2171 | "~".to_string(), 2172 | false, 2173 | &config, 2174 | ), 2175 | }], 2176 | }], 2177 | &config, 2178 | ); 2179 | 2180 | assert_eq!(actual, expected); 2181 | 2182 | config.initial_title_in_class.push(( 2183 | Regex::new("kitty").unwrap(), 2184 | vec![(Regex::new("(?i)zsh").unwrap(), "term3".to_string())], 2185 | )); 2186 | 2187 | let renamer = Renamer::new( 2188 | Config { 2189 | cfg_path: None, 2190 | config: config.clone(), 2191 | }, 2192 | Args { 2193 | verbose: false, 2194 | debug: false, 2195 | config: None, 2196 | dump: false, 2197 | migrate_config: false, 2198 | }, 2199 | ); 2200 | 2201 | let actual = renamer.generate_workspaces_string( 2202 | vec![AppWorkspace { 2203 | id: 1, 2204 | clients: vec![AppClient { 2205 | initial_class: "kitty".to_string(), 2206 | class: "kitty".to_string(), 2207 | initial_title: "zsh".to_string(), 2208 | title: "~".to_string(), 2209 | is_active: false, 2210 | is_fullscreen: FullscreenMode::None, 2211 | matched_rule: renamer.parse_icon( 2212 | "kitty".to_string(), 2213 | "kitty".to_string(), 2214 | "zsh".to_string(), 2215 | "~".to_string(), 2216 | false, 2217 | &config, 2218 | ), 2219 | is_dedup_inactive_fullscreen: false, 2220 | }], 2221 | }], 2222 | &config, 2223 | ); 2224 | 2225 | let expected = [(1, "term3".to_string())].into_iter().collect(); 2226 | 2227 | assert_eq!(actual, expected); 2228 | 2229 | config.initial_title_in_initial_class.push(( 2230 | Regex::new("kitty").unwrap(), 2231 | vec![(Regex::new("(?i)zsh").unwrap(), "term4".to_string())], 2232 | )); 2233 | 2234 | let renamer = Renamer::new( 2235 | Config { 2236 | cfg_path: None, 2237 | config: config.clone(), 2238 | }, 2239 | Args { 2240 | verbose: false, 2241 | debug: false, 2242 | config: None, 2243 | dump: false, 2244 | migrate_config: false, 2245 | }, 2246 | ); 2247 | 2248 | let actual = renamer.generate_workspaces_string( 2249 | vec![AppWorkspace { 2250 | id: 1, 2251 | clients: vec![AppClient { 2252 | initial_class: "kitty".to_string(), 2253 | class: "kitty".to_string(), 2254 | initial_title: "zsh".to_string(), 2255 | title: "~".to_string(), 2256 | is_active: false, 2257 | is_fullscreen: FullscreenMode::None, 2258 | matched_rule: renamer.parse_icon( 2259 | "kitty".to_string(), 2260 | "kitty".to_string(), 2261 | "zsh".to_string(), 2262 | "~".to_string(), 2263 | false, 2264 | &config, 2265 | ), 2266 | is_dedup_inactive_fullscreen: false, 2267 | }], 2268 | }], 2269 | &config, 2270 | ); 2271 | 2272 | let expected = [(1, "term4".to_string())].into_iter().collect(); 2273 | 2274 | assert_eq!(actual, expected); 2275 | } 2276 | 2277 | #[test] 2278 | fn test_workspace_cache() { 2279 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 2280 | config 2281 | .class 2282 | .push((Regex::new("kitty").unwrap(), "term".to_string())); 2283 | 2284 | let renamer = Renamer::new( 2285 | Config { 2286 | cfg_path: None, 2287 | config: config.clone(), 2288 | }, 2289 | Args { 2290 | verbose: false, 2291 | debug: false, 2292 | config: None, 2293 | dump: false, 2294 | migrate_config: false, 2295 | }, 2296 | ); 2297 | 2298 | // Initial state - cache should be empty 2299 | assert_eq!(renamer.workspace_strings_cache.lock().unwrap().len(), 0); 2300 | 2301 | let mut app_workspaces = vec![ 2302 | AppWorkspace { 2303 | id: 1, 2304 | clients: vec![AppClient { 2305 | initial_class: "kitty".to_string(), 2306 | class: "kitty".to_string(), 2307 | title: "term1".to_string(), 2308 | initial_title: "term1".to_string(), 2309 | is_active: false, 2310 | is_fullscreen: FullscreenMode::None, 2311 | matched_rule: renamer.parse_icon( 2312 | "kitty".to_string(), 2313 | "kitty".to_string(), 2314 | "term1".to_string(), 2315 | "term1".to_string(), 2316 | false, 2317 | &config, 2318 | ), 2319 | is_dedup_inactive_fullscreen: false, 2320 | }], 2321 | }, 2322 | AppWorkspace { 2323 | id: 2, 2324 | clients: vec![AppClient { 2325 | initial_class: "kitty".to_string(), 2326 | class: "kitty".to_string(), 2327 | title: "term2".to_string(), 2328 | initial_title: "term2".to_string(), 2329 | is_active: false, 2330 | is_fullscreen: FullscreenMode::None, 2331 | matched_rule: renamer.parse_icon( 2332 | "kitty".to_string(), 2333 | "kitty".to_string(), 2334 | "term2".to_string(), 2335 | "term2".to_string(), 2336 | false, 2337 | &config, 2338 | ), 2339 | is_dedup_inactive_fullscreen: false, 2340 | }], 2341 | }, 2342 | ]; 2343 | 2344 | let strings = renamer.generate_workspaces_string(app_workspaces.clone(), &config); 2345 | // Update cache and rename workspaces 2346 | let altered_strings = renamer.get_altered_workspaces(&strings).unwrap(); 2347 | assert_eq!(strings, altered_strings); 2348 | 2349 | let workspace_ids: HashSet<_> = app_workspaces.iter().map(|w| w.id).collect(); 2350 | renamer 2351 | .update_cache(&altered_strings, &workspace_ids) 2352 | .unwrap(); 2353 | // Cache should now contain entries for all workspaces 2354 | { 2355 | let cache = renamer.workspace_strings_cache.lock().unwrap(); 2356 | assert_eq!(cache.len(), app_workspaces.len()); 2357 | assert_eq!(cache.get(&1), strings.get(&1)); 2358 | assert_eq!(cache.get(&2), strings.get(&2)); 2359 | } 2360 | 2361 | // Generate same workspaces again - nothing should be altered 2362 | let altered_strings2 = renamer.get_altered_workspaces(&strings).unwrap(); 2363 | assert!(altered_strings2.is_empty()); 2364 | 2365 | app_workspaces.push(AppWorkspace { 2366 | id: 3, 2367 | clients: vec![AppClient { 2368 | initial_class: "kitty".to_string(), 2369 | class: "kitty".to_string(), 2370 | title: "term3".to_string(), 2371 | initial_title: "term3".to_string(), 2372 | is_active: false, 2373 | is_fullscreen: FullscreenMode::None, 2374 | matched_rule: renamer.parse_icon( 2375 | "kitty".to_string(), 2376 | "kitty".to_string(), 2377 | "term3".to_string(), 2378 | "term3".to_string(), 2379 | false, 2380 | &config, 2381 | ), 2382 | is_dedup_inactive_fullscreen: false, 2383 | }], 2384 | }); 2385 | 2386 | let strings3 = renamer.generate_workspaces_string(app_workspaces.clone(), &config); 2387 | let altered_strings3 = renamer.get_altered_workspaces(&strings3).unwrap(); 2388 | 2389 | // Only the new workspace should be altered 2390 | assert_eq!(altered_strings3.len(), 1); 2391 | assert_eq!(altered_strings3.get(&3), strings3.get(&3)); 2392 | 2393 | let workspace_ids: HashSet<_> = app_workspaces.iter().map(|w| w.id).collect(); 2394 | renamer 2395 | .update_cache(&altered_strings3, &workspace_ids) 2396 | .unwrap(); 2397 | 2398 | // Generate different workspace set - should update cache 2399 | let app_workspaces2 = vec![AppWorkspace { 2400 | id: 4, 2401 | clients: vec![AppClient { 2402 | initial_class: "kitty".to_string(), 2403 | class: "kitty".to_string(), 2404 | title: "term3".to_string(), // Different title 2405 | initial_title: "term3".to_string(), 2406 | is_active: false, 2407 | is_fullscreen: FullscreenMode::None, 2408 | matched_rule: renamer.parse_icon( 2409 | "kitty".to_string(), 2410 | "kitty".to_string(), 2411 | "term3".to_string(), 2412 | "term3".to_string(), 2413 | false, 2414 | &config, 2415 | ), 2416 | is_dedup_inactive_fullscreen: false, 2417 | }], 2418 | }]; 2419 | 2420 | let strings3 = renamer.generate_workspaces_string(app_workspaces2.clone(), &config); 2421 | let altered_strings3 = renamer.get_altered_workspaces(&strings3).unwrap(); 2422 | assert_eq!(strings3, altered_strings3); 2423 | 2424 | let workspace_ids: HashSet<_> = app_workspaces2.iter().map(|w| w.id).collect(); 2425 | renamer 2426 | .update_cache(&altered_strings3, &workspace_ids) 2427 | .unwrap(); 2428 | 2429 | // Cache should be updated - workspace 2 removed, workspace 1 updated 2430 | { 2431 | let cache = renamer.workspace_strings_cache.lock().unwrap(); 2432 | assert_eq!(cache.len(), 1); 2433 | assert_eq!(cache.get(&1), strings3.get(&1)); 2434 | assert_eq!(cache.get(&2), None); 2435 | } 2436 | 2437 | // Test cache reset 2438 | renamer.reset_workspaces(config.clone()).unwrap(); 2439 | assert_eq!(renamer.workspace_strings_cache.lock().unwrap().len(), 0); 2440 | } 2441 | 2442 | #[test] 2443 | fn test_regex_capture_support() { 2444 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 2445 | 2446 | config.title_in_class.push(( 2447 | Regex::new("(?i)foot").unwrap(), 2448 | vec![( 2449 | Regex::new("emerge: (.+?/.+?)-.*").unwrap(), 2450 | "test {match1}".to_string(), 2451 | )], 2452 | )); 2453 | config.title_in_class.push(( 2454 | Regex::new("(?i)foot").unwrap(), 2455 | vec![( 2456 | Regex::new("pacman: (.+?/.+?)-(.*)").unwrap(), 2457 | "test {match1} test2 {match2}".to_string(), 2458 | )], 2459 | )); 2460 | config.title_in_class_active.push(( 2461 | Regex::new("(?i)foot").unwrap(), 2462 | vec![( 2463 | Regex::new("pacman: (.+?/.+?)-(.*)").unwrap(), 2464 | "*#test{match1}#between#{match2}endtest#*".to_string(), 2465 | )], 2466 | )); 2467 | 2468 | config.format.client_active = "*{icon}*".to_string(); 2469 | 2470 | let renamer = Renamer::new( 2471 | Config { 2472 | cfg_path: None, 2473 | config: config.clone(), 2474 | }, 2475 | Args { 2476 | verbose: false, 2477 | debug: false, 2478 | config: None, 2479 | dump: false, 2480 | migrate_config: false, 2481 | }, 2482 | ); 2483 | 2484 | let mut expected = [(1, "test (13 of 20) dev-lang/rust".to_string())] 2485 | .into_iter() 2486 | .collect(); 2487 | 2488 | let mut actual = renamer.generate_workspaces_string( 2489 | vec![AppWorkspace { 2490 | id: 1, 2491 | clients: vec![AppClient { 2492 | initial_class: "foot".to_string(), 2493 | class: "foot".to_string(), 2494 | initial_title: "zsh".to_string(), 2495 | title: "emerge: (13 of 20) dev-lang/rust-1.69.0-r1 Compile:".to_string(), 2496 | is_active: false, 2497 | is_fullscreen: FullscreenMode::None, 2498 | matched_rule: renamer.parse_icon( 2499 | "foot".to_string(), 2500 | "foot".to_string(), 2501 | "zsh".to_string(), 2502 | "emerge: (13 of 20) dev-lang/rust-1.69.0-r1 Compile:".to_string(), 2503 | false, 2504 | &config, 2505 | ), 2506 | is_dedup_inactive_fullscreen: false, 2507 | }], 2508 | }], 2509 | &config, 2510 | ); 2511 | 2512 | assert_eq!(actual, expected); 2513 | 2514 | expected = [( 2515 | 1, 2516 | "*#test(14 of 20) dev-lang/rust#between#1.69.0-r1 Compile:endtest#*".to_string(), 2517 | )] 2518 | .into_iter() 2519 | .collect(); 2520 | 2521 | actual = renamer.generate_workspaces_string( 2522 | vec![AppWorkspace { 2523 | id: 1, 2524 | clients: vec![AppClient { 2525 | initial_class: "foot".to_string(), 2526 | class: "foot".to_string(), 2527 | initial_title: "zsh".to_string(), 2528 | title: "pacman: (14 of 20) dev-lang/rust-1.69.0-r1 Compile:".to_string(), 2529 | is_active: true, 2530 | is_fullscreen: FullscreenMode::None, 2531 | matched_rule: renamer.parse_icon( 2532 | "foot".to_string(), 2533 | "foot".to_string(), 2534 | "zsh".to_string(), 2535 | "pacman: (14 of 20) dev-lang/rust-1.69.0-r1 Compile:".to_string(), 2536 | true, 2537 | &config, 2538 | ), 2539 | is_dedup_inactive_fullscreen: false, 2540 | }], 2541 | }], 2542 | &config, 2543 | ); 2544 | 2545 | assert_eq!(actual, expected); 2546 | } 2547 | 2548 | #[test] 2549 | fn test_workspaces_name_config() { 2550 | let mut config = crate::config::read_config_file(None, false, false).unwrap(); 2551 | 2552 | config 2553 | .workspaces_name 2554 | .push(("0".to_string(), "zero".to_string())); 2555 | 2556 | config 2557 | .workspaces_name 2558 | .push(("1".to_string(), "one".to_string())); 2559 | 2560 | let expected = "zero".to_string(); 2561 | let actual = get_workspace_name(0, &config.workspaces_name); 2562 | 2563 | assert_eq!(actual, expected); 2564 | 2565 | let expected = "one".to_string(); 2566 | let actual = get_workspace_name(1, &config.workspaces_name); 2567 | 2568 | assert_eq!(actual, expected); 2569 | 2570 | let expected = "3".to_string(); 2571 | let actual = get_workspace_name(3, &config.workspaces_name); 2572 | 2573 | assert_eq!(actual, expected); 2574 | } 2575 | } 2576 | --------------------------------------------------------------------------------