├── .gitignore ├── SECURITY.md ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── Cargo.toml ├── pull_request_template.md ├── LICENSE ├── configuration.md ├── src ├── single.rs ├── options.rs ├── main.rs └── hyprland_event.rs ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | ## Reporting a Vulnerability 5 | 6 | Email me i8ehkvien@mozmail.com 7 | -------------------------------------------------------------------------------- /.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 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Linux Distro (please complete the following information):** 24 | - [e.g. Arch] 25 | 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyprland-per-window-layout" 3 | description = "Per window keyboard layout (language) for Hyprland wayland compositor" 4 | version = "0.2.17" 5 | edition = "2021" 6 | exclude = ["target", "Cargo.lock"] 7 | readme = "README.md" 8 | repository = "https://github.com/coffebar/hyprland-per-window-layout" 9 | keywords = ["cli", "wayland", "linux", "hyprland", "daemon"] 10 | license="MIT" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | serde = { version = "1.0", features = ["derive"] } 16 | env_logger = "0.10.0" 17 | lazy_static = "1.4.0" 18 | log = "0.4.17" 19 | serde_json = "1.0.93" 20 | nix = "0.23.0" 21 | dirs = "5.0.1" 22 | toml = "0.8.1" 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MahouShoujoMivutilde 4 | Copyright (c) 2023 coffebar 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Reason to have configuration 4 | 5 | This program can be used without a configuration file. But you may want to have a configuration file to: 6 | 7 | - Set up a keyboard layout for a specific window classes 8 | 9 | ## Configuration file 10 | 11 | Create a file 12 | ~/.config/hyprland-per-window-layout/options.toml 13 | 14 | Example configuration file: 15 | 16 | ```toml 17 | # list of keyboards to operate on 18 | # use `hyprctl devices -j` to list all keyboards 19 | keyboards = [ 20 | "lenovo-keyboard", 21 | ] 22 | 23 | # layout_index => window classes list 24 | # use `hyprctl clients` to get class names 25 | [[default_layouts]] 26 | 1 = [ 27 | "org.telegram.desktop", 28 | ] 29 | ``` 30 | 31 | This example will set your second layout for the Telegram by default. 32 | 33 | 1 - is a layout index. In case of this input configuration: 34 | ``` 35 | input { 36 | kb_layout = us,es,de 37 | ... 38 | ``` 39 | *us* index is 0, *es* index is 1, *de* index is 2. 40 | 41 | Note, *keyboards* section is required for default_layouts feature. 42 | 43 | Here is more complex example if you have 3 layouts and 2 keyboards: 44 | 45 | ```toml 46 | # list of keyboards to operate on 47 | # use `hyprctl devices -j` to list all keyboards 48 | keyboards = [ 49 | "apple-magic-keyboard", 50 | "lenovo-keyboard", 51 | ] 52 | 53 | # layout_index => window classes list 54 | # use `hyprctl clients` to get class names 55 | [[default_layouts]] 56 | 1 = [ 57 | "org.telegram.desktop", 58 | "discord", 59 | ] 60 | 2 = [ 61 | "firefox", 62 | ] 63 | ``` 64 | -------------------------------------------------------------------------------- /src/single.rs: -------------------------------------------------------------------------------- 1 | extern crate nix; 2 | 3 | pub use self::inner::*; 4 | 5 | mod inner { 6 | use nix::sys::socket::{self, UnixAddr}; 7 | use nix::unistd; 8 | use nix::Result; 9 | use std::os::unix::prelude::RawFd; 10 | 11 | /// A struct representing one running instance. 12 | pub struct SingleInstance { 13 | maybe_sock: Option, 14 | } 15 | 16 | impl SingleInstance { 17 | /// Returns a new SingleInstance object. 18 | pub fn new(name: &str) -> Result { 19 | let addr = UnixAddr::new_abstract(name.as_bytes())?; 20 | let sock = socket::socket( 21 | socket::AddressFamily::Unix, 22 | socket::SockType::Stream, 23 | socket::SockFlag::empty(), 24 | None, 25 | )?; 26 | 27 | let maybe_sock = match socket::bind(sock, &socket::SockAddr::Unix(addr)) { 28 | Ok(()) => Some(sock), 29 | Err(nix::errno::Errno::EADDRINUSE) => None, 30 | Err(e) => return Err(e), 31 | }; 32 | 33 | Ok(Self { maybe_sock }) 34 | } 35 | 36 | /// Returns whether this instance is single. 37 | pub fn is_single(&self) -> bool { 38 | self.maybe_sock.is_some() 39 | } 40 | } 41 | 42 | impl Drop for SingleInstance { 43 | fn drop(&mut self) { 44 | if let Some(sock) = self.maybe_sock { 45 | // Intentionally discard any close errors. 46 | let _ = unistd::close(sock); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to hyprland-per-window-layout 2 | 3 | Thank you for your interest in contributing! ❤️ 4 | 5 | ## Development Setup 6 | 7 | ### Prerequisites 8 | 9 | - Rust (stable channel) 10 | - Hyprland compositor running 11 | - At least 2 keyboard layouts configured in hyprland.conf 12 | 13 | ### Building 14 | 15 | ```bash 16 | git clone https://github.com/coffebar/hyprland-per-window-layout.git 17 | cd hyprland-per-window-layout 18 | cargo build --release 19 | ``` 20 | 21 | ### Running Locally 22 | 23 | ```bash 24 | # Stop any existing instance 25 | pkill hyprland-per-window-layout 26 | 27 | # Run your development build 28 | RUST_LOG=debug ./target/release/hyprland-per-window-layout 29 | ``` 30 | 31 | ## Code Style 32 | 33 | - Run `cargo fmt` before committing 34 | - Run `cargo clippy` and fix any warnings 35 | - Follow existing code patterns and conventions 36 | 37 | ## Testing Your Changes 38 | 39 | 1. Build the project with your changes 40 | 2. Run with `RUST_LOG=debug` to see debug output 41 | 3. Test switching between windows and verify layouts change correctly 42 | 4. Test with different applications (terminal, browser, chat apps) 43 | 5. Verify the daemon handles Hyprland restarts gracefully 44 | 45 | ## Submitting Changes 46 | 47 | ### Commit Messages 48 | 49 | Use clear, descriptive commit messages: 50 | - `fix: ` for bug fixes 51 | - `feat: ` for new features 52 | - `refactor: ` for code improvements 53 | - `docs: ` for documentation changes 54 | 55 | ### Pull Request Process 56 | 57 | 1. Fork the repository 58 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 59 | 3. Make your changes and commit 60 | 4. Push to your fork 61 | 5. Open a Pull Request with: 62 | - Clear description of changes 63 | - How you tested the changes 64 | - Any relevant issue numbers 65 | 66 | ## Types of Contributions 67 | 68 | ### Bug Reports 69 | 70 | - Check if the issue already exists 71 | - Include Hyprland version (`hyprctl version`) 72 | - Include debug logs (`RUST_LOG=debug`) 73 | - Describe steps to reproduce 74 | 75 | ### Feature Requests 76 | 77 | - Explain the use case 78 | - Describe expected behavior 79 | - Consider if it fits the project's "zero configuration" philosophy 80 | 81 | ### Code Contributions 82 | 83 | - Bug fixes are always welcome 84 | - New features should be discussed first (open an issue) 85 | - Performance improvements are appreciated 86 | - Code cleanup/refactoring is welcome if it improves maintainability 87 | 88 | ## Questions? 89 | 90 | Open an issue for any questions about contributing. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) 2 | 3 | # Hyprland Per-Window Keyboard Layout Manager 4 | 5 | ![](https://img.shields.io/crates/d/hyprland-per-window-layout) 6 | ![](https://img.shields.io/github/issues-raw/coffebar/hyprland-per-window-layout) 7 | ![](https://img.shields.io/github/stars/coffebar/hyprland-per-window-layout) 8 | ![](https://img.shields.io/aur/version/hyprland-per-window-layout) 9 | ![](https://img.shields.io/crates/v/hyprland-per-window-layout) 10 | 11 | Automatic keyboard layout switching for Hyprland - each window remembers its own keyboard layout. 12 | 13 | ## Features 14 | 15 | - 🚀 **Zero configuration** - works out of the box 16 | - 🧠 **Per-window memory** - each window maintains its layout 17 | - ⚡ **Lightweight** - minimal resource usage (Rust) 18 | - 🔧 **Optional configuration** - set default layouts per application 19 | 20 | ## Use Cases 21 | 22 | - **Developers**: Code in English, chat in native language 23 | - **Multilingual users**: Seamless switching between languages 24 | - **Power users**: Consistent layouts across applications 25 | 26 | **Requirements**: At least 2 keyboard layouts in hyprland.conf 27 | 28 | ## Installation 29 | 30 | ### From [AUR](https://aur.archlinux.org/packages/hyprland-per-window-layout) (Arch Linux) 31 | 32 | ```bash 33 | # e.g. 34 | yay -Sy && yay -S hyprland-per-window-layout 35 | ``` 36 | 37 | Add to hyprland.conf: 38 | ``` 39 | exec-once = /usr/bin/hyprland-per-window-layout 40 | ``` 41 | 42 | ### From Cargo 43 | 44 | ```bash 45 | cargo install hyprland-per-window-layout 46 | ``` 47 | 48 | Add to hyprland.conf: 49 | ``` 50 | exec-once = ~/.cargo/bin/hyprland-per-window-layout 51 | ``` 52 | 53 | ### Gentoo 54 | 55 | Activate wayland overlay as described in [README](https://github.com/bsd-ac/wayland-desktop#activate-overlay-via-eselect-repository), allow **~amd64** keyword and then install it: 56 | 57 | ```bash 58 | # emerge --ask gui-apps/hyprland-per-window-layout 59 | ``` 60 | 61 | ### From Source 62 | 63 | ```bash 64 | git clone https://github.com/coffebar/hyprland-per-window-layout.git 65 | cd hyprland-per-window-layout 66 | cargo build --release 67 | mkdir -p ~/.local/bin/ 68 | cp target/release/hyprland-per-window-layout ~/.local/bin/ 69 | ``` 70 | 71 | Add to hyprland.conf: 72 | ``` 73 | exec-once = ~/.local/bin/hyprland-per-window-layout 74 | ``` 75 | 76 | ## Configuration 77 | 78 | Optional. See [configuration.md](configuration.md) for setting default layouts per application. 79 | 80 | ## Contributing 81 | 82 | Bug reports and PRs are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 83 | 84 | Tested on Hyprland v0.50. 85 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | // read and represent the options file 2 | // located at ~/.config/hyprland-per-window-layout/options.toml 3 | 4 | use std::fs::File; 5 | 6 | use serde::Deserialize; 7 | use std::collections::HashMap; 8 | use toml::Table; 9 | 10 | #[derive(Deserialize, Debug)] 11 | pub struct Options { 12 | pub keyboards: Vec, // list of keyboards to switch layouts on 13 | pub default_layouts: HashMap>, // default layouts for window classes 14 | } 15 | 16 | // function to read the options file toml 17 | pub fn read_options() -> Options { 18 | // get the path to the options file 19 | // in $HOME/.config/hyprland-per-window-layout/options.toml 20 | let options_path = dirs::config_dir() 21 | .unwrap() 22 | .join("hyprland-per-window-layout") 23 | .join("options.toml"); 24 | // read the file contents if it exists 25 | // ignore if it doesn't exist 26 | match File::open(&options_path) { 27 | Ok(_file) => { 28 | // read the file contents 29 | let file_content = match std::fs::read_to_string(&options_path) { 30 | Ok(content) => content, 31 | Err(e) => { 32 | println!("Error reading options.toml: {e}"); 33 | return Options { 34 | keyboards: Vec::new(), 35 | default_layouts: HashMap::new(), 36 | }; 37 | } 38 | }; 39 | let _t = match file_content.parse::() { 40 | Ok(table) => table, 41 | Err(e) => { 42 | println!("Error parsing options.toml: {e}"); 43 | return Options { 44 | keyboards: Vec::new(), 45 | default_layouts: HashMap::new(), 46 | }; 47 | } 48 | }; 49 | let mut map = HashMap::new(); 50 | let mut keyboards = Vec::new(); 51 | if let Some(_default_layouts) = _t.get("default_layouts") { 52 | if let Some(default_layouts_array) = _default_layouts.as_array() { 53 | if let Some(first_layout) = default_layouts_array.first() { 54 | if let Some(layout_table) = first_layout.as_table() { 55 | for (key, value) in layout_table.iter() { 56 | if let Ok(key_num) = key.parse::() { 57 | if let Some(value_array) = value.as_array() { 58 | let layout_vec: Vec = value_array 59 | .iter() 60 | .filter_map(|x| x.as_str().map(|s| s.to_string())) 61 | .collect(); 62 | map.insert(key_num, layout_vec); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | if let Some(_keyboards) = _t.get("keyboards") { 71 | if let Some(keyboards_array) = _keyboards.as_array() { 72 | for keyboard in keyboards_array.iter() { 73 | if let Some(keyboard_str) = keyboard.as_str() { 74 | keyboards.push(keyboard_str.to_string()); 75 | } 76 | } 77 | } 78 | } 79 | return Options { 80 | keyboards, 81 | default_layouts: map, 82 | }; 83 | } 84 | Err(_) => { 85 | println!("options.toml not found, using defaults"); 86 | } 87 | }; 88 | Options { 89 | keyboards: Vec::new(), 90 | default_layouts: HashMap::new(), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; // read env variables 2 | use std::io::BufRead; // read unix socket 3 | use std::io::BufReader; // read unix socket 4 | use std::os::unix::net::UnixStream; 5 | 6 | mod hyprland_event; // work with message from socket 7 | use hyprland_event::{event, fullfill_keyboards_list, fullfill_layouts_list, hyprctl}; 8 | 9 | mod options; // read options.toml 10 | use options::read_options; 11 | 12 | mod single; // a struct representing one running instance 13 | use single::SingleInstance; 14 | 15 | use serde_json::Value; // json parsed 16 | 17 | // listen Hyprland socket 18 | fn listen(socket_addr: String) -> std::io::Result<()> { 19 | let stream = match UnixStream::connect(socket_addr) { 20 | Ok(stream) => stream, 21 | Err(e) => { 22 | println!("Couldn't connect: {e:?}"); 23 | return Err(e); 24 | } 25 | }; 26 | let mut reader = BufReader::new(stream); 27 | let opt = read_options(); 28 | if !opt.keyboards.is_empty() { 29 | for keyboard in opt.keyboards.iter() { 30 | fullfill_keyboards_list(keyboard.to_string()); 31 | log::debug!("Keyboard added: {}", keyboard); 32 | } 33 | } 34 | loop { 35 | // read message from socket 36 | let mut buf: Vec = vec![]; 37 | let readed = match reader.read_until(b'\n', &mut buf) { 38 | Ok(size) => size, 39 | Err(e) => { 40 | log::warn!("Error reading from socket: {}", e); 41 | break Err(e); 42 | } 43 | }; 44 | if readed == 0 { 45 | break Ok(()); 46 | } 47 | let data = String::from_utf8_lossy(&buf); 48 | let data_parts: Vec<&str> = data.trim().split(">>").collect(); 49 | if data_parts.len() > 1 { 50 | event(data_parts[0], data_parts[1], &opt) 51 | } 52 | } 53 | } 54 | 55 | // get keyboards count listed in hyprland conf file (input section) 56 | // return -1 if failed 57 | fn get_kb_layouts_count() -> i16 { 58 | // get layouts list from hyprctl cli call 59 | match hyprctl(["getoption", "input:kb_layout", "-j"].to_vec()) { 60 | Ok(output) => { 61 | log::debug!("input:kb_layout: {}", output); 62 | // parse the string from stdin into serde_json::Value 63 | let json: Value = match serde_json::from_str(&output) { 64 | Ok(json) => json, 65 | Err(e) => { 66 | log::warn!("Failed to parse JSON: {}", e); 67 | return -1; 68 | } 69 | }; 70 | if json.is_null() || json["str"].is_null() { 71 | return -1; 72 | } 73 | let kb_layout = str::replace(json["str"].to_string().trim(), "\"", ""); 74 | 75 | if !kb_layout.is_empty() { 76 | let items: Vec<&str> = kb_layout.split(",").collect(); 77 | items.len() as i16 78 | } else { 79 | 0 80 | } 81 | } 82 | Err(_e) => { 83 | println!("Failed to get option from hyprctl"); 84 | 0 85 | } 86 | } 87 | } 88 | 89 | // try to get kb layouts count 5 times with 1 sec delay 90 | fn get_kb_layouts_count_retry() -> i16 { 91 | let mut count = 0; 92 | loop { 93 | let layouts_found = get_kb_layouts_count(); 94 | if layouts_found > -1 { 95 | return layouts_found; 96 | } 97 | count += 1; 98 | if count > 5 { 99 | return -1; 100 | } 101 | std::thread::sleep(std::time::Duration::from_secs(1)); 102 | } 103 | } 104 | 105 | // check kb_file option is set in hyprland conf file 106 | fn kb_file_isset() -> bool { 107 | // get layouts list from hyprctl cli call 108 | match hyprctl(["getoption", "input:kb_file", "-j"].to_vec()) { 109 | Ok(output) => { 110 | log::debug!("input:kb_file: {}", output); 111 | // parse the string from stdin into serde_json::Value 112 | let json: Value = match serde_json::from_str(&output) { 113 | Ok(json) => json, 114 | Err(e) => { 115 | log::warn!("Failed to parse JSON: {}", e); 116 | return false; 117 | } 118 | }; 119 | if json["str"].is_null() { 120 | return false; 121 | } 122 | let value = str::replace(json["str"].to_string().trim(), "\"", ""); 123 | value != "[[EMPTY]]" 124 | } 125 | Err(_e) => { 126 | println!("Failed to get option from hyprctl"); 127 | false 128 | } 129 | } 130 | } 131 | 132 | // get default layout from cli command "hyprctl devices -j" 133 | // value of ['keyboards'][0]['active_keymap'] 134 | fn get_default_layout_name() -> bool { 135 | match hyprctl(["devices", "-j"].to_vec()) { 136 | Ok(output) => { 137 | // parse the string from stdin into serde_json::Value 138 | let json: Value = match serde_json::from_str(&output) { 139 | Ok(json) => json, 140 | Err(e) => { 141 | log::warn!("Failed to parse JSON: {}", e); 142 | return false; 143 | } 144 | }; 145 | let keyboards = &json["keyboards"]; 146 | log::debug!("keyboards: {}", keyboards); 147 | if keyboards.is_null() { 148 | log::warn!("No keyboards found"); 149 | return false; 150 | } 151 | let keyboards_array = match keyboards.as_array() { 152 | Some(arr) => arr, 153 | None => { 154 | log::warn!("Keyboards is not an array"); 155 | return false; 156 | } 157 | }; 158 | if keyboards_array.is_empty() { 159 | log::warn!("No keyboards found"); 160 | return false; 161 | } 162 | let kb_layout = str::replace( 163 | keyboards_array[0]["active_keymap"].to_string().trim(), 164 | "\"", 165 | "", 166 | ); 167 | if !kb_layout.is_empty() { 168 | fullfill_layouts_list(kb_layout.to_string()); 169 | true 170 | } else { 171 | log::warn!("Keyboard layouts not found"); 172 | false 173 | } 174 | } 175 | Err(_e) => { 176 | println!("Failed to get devices from hyprctl"); 177 | false 178 | } 179 | } 180 | } 181 | 182 | // read env variables and listen Hyprland unix socket 183 | fn main() { 184 | // to see logs in output: add env RUST_LOG='debug' 185 | env_logger::init(); 186 | let instance_sock = SingleInstance::new("hyprland-per-window-layout").unwrap(); 187 | if !instance_sock.is_single() { 188 | println!("Another instance is running."); 189 | std::process::exit(1); 190 | } 191 | // this program make sense if you have 2+ layouts 192 | let layouts_found = get_kb_layouts_count_retry(); 193 | 194 | if layouts_found < 2 && !kb_file_isset() { 195 | println!("Fatal error: You need to configure layouts on Hyprland"); 196 | println!("Add kb_layout option to input group in your hyprland.conf"); 197 | println!("You don't need this program if you have only 1 keyboard layout"); 198 | std::process::exit(1); 199 | } 200 | let mut attempts = 0; 201 | const MAX_ATTEMPTS: u32 = 30; // 30 second timeout 202 | while !get_default_layout_name() { 203 | // repeat until success 204 | attempts += 1; 205 | if attempts >= MAX_ATTEMPTS { 206 | println!("Timeout: Could not get default layout after {MAX_ATTEMPTS} seconds"); 207 | std::process::exit(1); 208 | } 209 | std::thread::sleep(std::time::Duration::from_secs(1)); 210 | } 211 | 212 | match env::var("HYPRLAND_INSTANCE_SIGNATURE") { 213 | Ok(hypr_inst) => { 214 | let default_socket = format!("/tmp/hypr/{hypr_inst}/.socket2.sock"); // for backawards compatibility 215 | let socket = match env::var("XDG_RUNTIME_DIR") { 216 | Ok(runtime_dir) => { 217 | match std::fs::metadata(format!("{runtime_dir}/hypr/{hypr_inst}/.socket2.sock")) 218 | { 219 | Ok(_) => format!("{runtime_dir}/hypr/{hypr_inst}/.socket2.sock"), 220 | Err(..) => default_socket, 221 | } 222 | } 223 | Err(..) => default_socket, 224 | }; 225 | // listen Hyprland socket 226 | match listen(socket) { 227 | Ok(()) => {} 228 | Err(e) => log::warn!("Error {e}"), 229 | } 230 | } 231 | Err(e) => println!("Fatal Error: Hyprland is not run. {e}"), 232 | } 233 | std::process::exit(1); 234 | } 235 | -------------------------------------------------------------------------------- /src/hyprland_event.rs: -------------------------------------------------------------------------------- 1 | // logging 2 | 3 | // options struct 4 | use crate::options::Options; 5 | 6 | // std lib 7 | use std::fmt; 8 | 9 | // system cmd 10 | use std::process::Command; 11 | 12 | // global hashmap with Mutex 13 | use lazy_static::lazy_static; 14 | use std::collections::HashMap; 15 | use std::sync::Mutex; 16 | lazy_static! { 17 | // hashmap to store windows and thier layouts 18 | static ref HASHMAP: Mutex> = Mutex::new(HashMap::new()); 19 | // vec to store layouts (long names) 20 | pub static ref LAYOUTS: Mutex> = Mutex::new(Vec::new()); 21 | // vec to store keyboard names 22 | pub static ref KEYBOARDS: Mutex> = Mutex::new(Vec::new()); 23 | // last active window address 24 | static ref ACTIVE_WINDOW: Mutex = Mutex::new(String::new()); 25 | // last active window class 26 | static ref ACTIVE_CLASS: Mutex = Mutex::new(String::new()); 27 | // current active layout index 28 | static ref ACTIVE_LAYOUT: Mutex = Mutex::new(0); 29 | } 30 | 31 | // work with messages from hyprland socket 32 | pub fn event(name: &str, data: &str, options: &Options) { 33 | log::debug!("E:'{}' D:'{}'", name, data); 34 | 35 | if name == "activewindow" { 36 | // save only all before first comma 37 | let data = data.split(",").next().unwrap_or("").to_string(); 38 | if let Ok(mut active_class) = ACTIVE_CLASS.lock() { 39 | *active_class = data; 40 | } 41 | return; 42 | } 43 | 44 | if name == "activewindowv2" { 45 | if data.is_empty() { 46 | log::debug!("No active window (empty workspace), maintaining current layout"); 47 | return; 48 | } 49 | let addr = format!("0x{data}"); 50 | if let Ok(mut active_window) = ACTIVE_WINDOW.lock() { 51 | *active_window = addr.clone(); 52 | } 53 | let map = match HASHMAP.lock() { 54 | Ok(map) => map, 55 | Err(_) => return, 56 | }; 57 | match map.get(&addr) { 58 | Some(index) => { 59 | log::debug!("{}: {}", addr, index); 60 | // only change layout if it's different from current 61 | let current_layout = match ACTIVE_LAYOUT.lock() { 62 | Ok(layout) => *layout, 63 | Err(_) => return, 64 | }; 65 | if current_layout != *index { 66 | change_layout(*index); 67 | } else { 68 | log::debug!("Layout {} already active, skipping change", index); 69 | } 70 | } 71 | None => { 72 | drop(map); 73 | log::debug!("added addr: {}", addr); 74 | // check if we have default layout for this window 75 | let default_layouts = &options.default_layouts; 76 | 77 | for (index, window_classes) in default_layouts.iter() { 78 | for window_class in window_classes.iter() { 79 | if let Ok(active_class) = ACTIVE_CLASS.lock() { 80 | for window_active_class in active_class.split(",") { 81 | if window_active_class.eq(window_class) { 82 | log::debug!( 83 | "Found default layout {} for window {}", 84 | index, 85 | window_active_class 86 | ); 87 | // Drop active_class before acquiring new mutex 88 | std::mem::drop(active_class); 89 | if let Ok(mut map) = HASHMAP.lock() { 90 | map.insert(addr.clone(), *index); 91 | // map will be dropped automatically 92 | } 93 | // only change layout if it's different from current 94 | let current_layout = match ACTIVE_LAYOUT.lock() { 95 | Ok(layout) => *layout, 96 | Err(_) => return, 97 | }; 98 | if current_layout != *index { 99 | change_layout(*index); 100 | } else { 101 | log::debug!( 102 | "Layout {} already active, skipping change", 103 | index 104 | ); 105 | } 106 | return; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | // set layout to default one (index 0) 113 | if let Ok(mut map) = HASHMAP.lock() { 114 | map.insert(addr, 0); 115 | // map will be dropped automatically 116 | } 117 | // only change layout if it's different from current 118 | let current_layout = match ACTIVE_LAYOUT.lock() { 119 | Ok(layout) => *layout, 120 | Err(_) => return, 121 | }; 122 | if current_layout != 0 { 123 | change_layout(0); 124 | } else { 125 | log::debug!("Layout 0 already active, skipping change"); 126 | } 127 | } 128 | } 129 | return; 130 | } 131 | 132 | if name == "closewindow" { 133 | let addr = format!("0x{data}"); 134 | if let Ok(mut map) = HASHMAP.lock() { 135 | map.remove(&addr); 136 | } 137 | return; 138 | } 139 | 140 | if name == "activelayout" { 141 | // params ex: keychron-keychron-k2,English (US) 142 | // params ex with variant: at-translated-set-2-keyboard,English (US, intl., with dead keys) 143 | if let Some((param_keyboard, param_layout)) = data.split_once(',') { 144 | if param_keyboard.contains("wlr_virtual_keyboard_v") { 145 | log::debug!("Skip virtual keyboard {}", param_keyboard); 146 | return; 147 | } 148 | log::debug!( 149 | "Catch layout changed event on {} with {}", 150 | param_keyboard, 151 | param_layout 152 | ); 153 | fullfill_keyboards_list(param_keyboard.to_string()); 154 | fullfill_layouts_list(param_layout.to_string()); 155 | 156 | let layout_vec = match LAYOUTS.lock() { 157 | Ok(vec) => vec, 158 | Err(_) => return, 159 | }; 160 | for (index, layout) in layout_vec.iter().enumerate() { 161 | let index = index as u16; 162 | if param_layout.eq(layout) { 163 | let active_layout: u16 = match ACTIVE_LAYOUT.lock() { 164 | Ok(layout) => *layout, 165 | Err(_) => return, 166 | }; 167 | if active_layout == index { 168 | log::debug!("Layout {} is current", layout); 169 | return; 170 | } 171 | if let Ok(mut active_layout_ref) = ACTIVE_LAYOUT.lock() { 172 | *active_layout_ref = index; 173 | } 174 | let addr = match ACTIVE_WINDOW.lock() { 175 | Ok(window) => window.clone(), 176 | Err(_) => return, 177 | }; 178 | 179 | if let Ok(mut map) = HASHMAP.lock() { 180 | map.insert(addr.clone(), index); 181 | log::debug!( 182 | "Saved layout {} with index {} on addr {}", 183 | layout, 184 | index, 185 | addr 186 | ); 187 | } 188 | 189 | return; 190 | } 191 | } 192 | } else { 193 | log::warn!("Bad 'activelayout' format: {}", data) 194 | } 195 | } 196 | } 197 | #[derive(Debug)] 198 | pub struct CommandFailed {} 199 | impl fmt::Display for CommandFailed { 200 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 201 | write!(f, "Command returned error") 202 | } 203 | } 204 | 205 | // run cli command 'hyprctl' with given args 206 | pub fn hyprctl(argv: Vec<&str>) -> Result { 207 | let output = Command::new("hyprctl") 208 | .args(argv) 209 | .output() 210 | .expect("failed to execute process"); 211 | match output.status.code() { 212 | Some(code) => { 213 | log::debug!("Status code is {}", code); 214 | Ok(String::from_utf8_lossy(&output.stdout).to_string()) 215 | } 216 | None => Err(CommandFailed {}), 217 | } 218 | } 219 | 220 | // updates layout on all active keyboards 221 | // Note: you need to manualy change layout on keyboard to add it into this list 222 | fn change_layout(index: u16) { 223 | let mut keyboards = match KEYBOARDS.lock() { 224 | Ok(kb) => kb, 225 | Err(_) => return, 226 | }; 227 | if keyboards.is_empty() { 228 | log::debug!("layout change interrupt: no keyboard added"); 229 | return; 230 | } 231 | log::debug!("layout change {}", index); 232 | if let Ok(mut active_layout) = ACTIVE_LAYOUT.lock() { 233 | *active_layout = index; 234 | } 235 | let mut kb_index = 0; 236 | let mut trash: Vec = Vec::new(); 237 | for kb in keyboards.iter() { 238 | if kb.contains("yubikey") { 239 | // skip yubikey 240 | kb_index += 1; 241 | continue; 242 | } 243 | let new_index = &index.to_string(); 244 | let e = hyprctl(["switchxkblayout", "--", kb.as_str(), new_index].to_vec()); 245 | match e { 246 | Ok(code) => { 247 | log::debug!( 248 | "Layout changed kb:{} index:{} exit_code:{}", 249 | kb, 250 | new_index, 251 | code 252 | ); 253 | } 254 | Err(_e) => { 255 | log::warn!("Keyboard removed from list: {}", kb); 256 | trash.push(kb_index); 257 | } 258 | } 259 | kb_index += 1; 260 | } 261 | // Remove elements in reverse order to avoid index misalignment 262 | for kb_index in trash.iter().rev() { 263 | keyboards.remove(*kb_index); 264 | } 265 | } 266 | 267 | // we have to fill this layouts list on go 268 | pub fn fullfill_layouts_list(long_name: String) { 269 | // add kb long name to LAYOUTS if not there 270 | let mut layout_vec = match LAYOUTS.lock() { 271 | Ok(vec) => vec, 272 | Err(_) => return, 273 | }; 274 | 275 | // skip blacklisted layouts 276 | let blacklisted_layouts = ["wvkbd"]; 277 | for layout in blacklisted_layouts.iter() { 278 | if layout.eq(&long_name) { 279 | log::debug!("Layout blacklisted: {}", long_name); 280 | return; 281 | } 282 | } 283 | 284 | if !layout_vec.contains(&long_name) { 285 | layout_vec.push(long_name.clone()); 286 | log::debug!("Layout stored: {}", long_name); 287 | } 288 | } 289 | 290 | pub fn fullfill_keyboards_list(name: String) { 291 | if let Ok(mut keyboards) = KEYBOARDS.lock() { 292 | if !keyboards.contains(&name) { 293 | keyboards.push(name); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "2.4.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 31 | 32 | [[package]] 33 | name = "cc" 34 | version = "1.0.79" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 37 | 38 | [[package]] 39 | name = "cfg-if" 40 | version = "1.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 43 | 44 | [[package]] 45 | name = "dirs" 46 | version = "5.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 49 | dependencies = [ 50 | "dirs-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "dirs-sys" 55 | version = "0.4.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 58 | dependencies = [ 59 | "libc", 60 | "option-ext", 61 | "redox_users", 62 | "windows-sys", 63 | ] 64 | 65 | [[package]] 66 | name = "env_logger" 67 | version = "0.10.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 70 | dependencies = [ 71 | "humantime", 72 | "is-terminal", 73 | "log", 74 | "regex", 75 | "termcolor", 76 | ] 77 | 78 | [[package]] 79 | name = "equivalent" 80 | version = "1.0.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 83 | 84 | [[package]] 85 | name = "errno" 86 | version = "0.3.1" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 89 | dependencies = [ 90 | "errno-dragonfly", 91 | "libc", 92 | "windows-sys", 93 | ] 94 | 95 | [[package]] 96 | name = "errno-dragonfly" 97 | version = "0.1.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 100 | dependencies = [ 101 | "cc", 102 | "libc", 103 | ] 104 | 105 | [[package]] 106 | name = "getrandom" 107 | version = "0.2.11" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 110 | dependencies = [ 111 | "cfg-if", 112 | "libc", 113 | "wasi", 114 | ] 115 | 116 | [[package]] 117 | name = "hashbrown" 118 | version = "0.14.3" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 121 | 122 | [[package]] 123 | name = "hermit-abi" 124 | version = "0.3.1" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 127 | 128 | [[package]] 129 | name = "humantime" 130 | version = "2.1.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 133 | 134 | [[package]] 135 | name = "hyprland-per-window-layout" 136 | version = "0.2.17" 137 | dependencies = [ 138 | "dirs", 139 | "env_logger", 140 | "lazy_static", 141 | "log", 142 | "nix", 143 | "serde", 144 | "serde_json", 145 | "toml", 146 | ] 147 | 148 | [[package]] 149 | name = "indexmap" 150 | version = "2.1.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 153 | dependencies = [ 154 | "equivalent", 155 | "hashbrown", 156 | ] 157 | 158 | [[package]] 159 | name = "io-lifetimes" 160 | version = "1.0.11" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 163 | dependencies = [ 164 | "hermit-abi", 165 | "libc", 166 | "windows-sys", 167 | ] 168 | 169 | [[package]] 170 | name = "is-terminal" 171 | version = "0.4.7" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 174 | dependencies = [ 175 | "hermit-abi", 176 | "io-lifetimes", 177 | "rustix", 178 | "windows-sys", 179 | ] 180 | 181 | [[package]] 182 | name = "itoa" 183 | version = "1.0.6" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 186 | 187 | [[package]] 188 | name = "lazy_static" 189 | version = "1.4.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 192 | 193 | [[package]] 194 | name = "libc" 195 | version = "0.2.152" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 198 | 199 | [[package]] 200 | name = "libredox" 201 | version = "0.0.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 204 | dependencies = [ 205 | "bitflags 2.4.1", 206 | "libc", 207 | "redox_syscall", 208 | ] 209 | 210 | [[package]] 211 | name = "linux-raw-sys" 212 | version = "0.3.8" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 215 | 216 | [[package]] 217 | name = "log" 218 | version = "0.4.18" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" 221 | 222 | [[package]] 223 | name = "memchr" 224 | version = "2.5.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 227 | 228 | [[package]] 229 | name = "memoffset" 230 | version = "0.6.5" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 233 | dependencies = [ 234 | "autocfg", 235 | ] 236 | 237 | [[package]] 238 | name = "nix" 239 | version = "0.23.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" 242 | dependencies = [ 243 | "bitflags 1.3.2", 244 | "cc", 245 | "cfg-if", 246 | "libc", 247 | "memoffset", 248 | ] 249 | 250 | [[package]] 251 | name = "option-ext" 252 | version = "0.2.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 255 | 256 | [[package]] 257 | name = "proc-macro2" 258 | version = "1.0.76" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" 261 | dependencies = [ 262 | "unicode-ident", 263 | ] 264 | 265 | [[package]] 266 | name = "quote" 267 | version = "1.0.35" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 270 | dependencies = [ 271 | "proc-macro2", 272 | ] 273 | 274 | [[package]] 275 | name = "redox_syscall" 276 | version = "0.4.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 279 | dependencies = [ 280 | "bitflags 1.3.2", 281 | ] 282 | 283 | [[package]] 284 | name = "redox_users" 285 | version = "0.4.4" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 288 | dependencies = [ 289 | "getrandom", 290 | "libredox", 291 | "thiserror", 292 | ] 293 | 294 | [[package]] 295 | name = "regex" 296 | version = "1.8.3" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" 299 | dependencies = [ 300 | "aho-corasick", 301 | "memchr", 302 | "regex-syntax", 303 | ] 304 | 305 | [[package]] 306 | name = "regex-syntax" 307 | version = "0.7.2" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" 310 | 311 | [[package]] 312 | name = "rustix" 313 | version = "0.37.19" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" 316 | dependencies = [ 317 | "bitflags 1.3.2", 318 | "errno", 319 | "io-lifetimes", 320 | "libc", 321 | "linux-raw-sys", 322 | "windows-sys", 323 | ] 324 | 325 | [[package]] 326 | name = "ryu" 327 | version = "1.0.13" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 330 | 331 | [[package]] 332 | name = "serde" 333 | version = "1.0.163" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" 336 | dependencies = [ 337 | "serde_derive", 338 | ] 339 | 340 | [[package]] 341 | name = "serde_derive" 342 | version = "1.0.163" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "syn", 349 | ] 350 | 351 | [[package]] 352 | name = "serde_json" 353 | version = "1.0.96" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" 356 | dependencies = [ 357 | "itoa", 358 | "ryu", 359 | "serde", 360 | ] 361 | 362 | [[package]] 363 | name = "serde_spanned" 364 | version = "0.6.5" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 367 | dependencies = [ 368 | "serde", 369 | ] 370 | 371 | [[package]] 372 | name = "syn" 373 | version = "2.0.48" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 376 | dependencies = [ 377 | "proc-macro2", 378 | "quote", 379 | "unicode-ident", 380 | ] 381 | 382 | [[package]] 383 | name = "termcolor" 384 | version = "1.2.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 387 | dependencies = [ 388 | "winapi-util", 389 | ] 390 | 391 | [[package]] 392 | name = "thiserror" 393 | version = "1.0.56" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 396 | dependencies = [ 397 | "thiserror-impl", 398 | ] 399 | 400 | [[package]] 401 | name = "thiserror-impl" 402 | version = "1.0.56" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 405 | dependencies = [ 406 | "proc-macro2", 407 | "quote", 408 | "syn", 409 | ] 410 | 411 | [[package]] 412 | name = "toml" 413 | version = "0.8.8" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 416 | dependencies = [ 417 | "serde", 418 | "serde_spanned", 419 | "toml_datetime", 420 | "toml_edit", 421 | ] 422 | 423 | [[package]] 424 | name = "toml_datetime" 425 | version = "0.6.5" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 428 | dependencies = [ 429 | "serde", 430 | ] 431 | 432 | [[package]] 433 | name = "toml_edit" 434 | version = "0.21.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 437 | dependencies = [ 438 | "indexmap", 439 | "serde", 440 | "serde_spanned", 441 | "toml_datetime", 442 | "winnow", 443 | ] 444 | 445 | [[package]] 446 | name = "unicode-ident" 447 | version = "1.0.9" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 450 | 451 | [[package]] 452 | name = "wasi" 453 | version = "0.11.0+wasi-snapshot-preview1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 456 | 457 | [[package]] 458 | name = "winapi" 459 | version = "0.3.9" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 462 | dependencies = [ 463 | "winapi-i686-pc-windows-gnu", 464 | "winapi-x86_64-pc-windows-gnu", 465 | ] 466 | 467 | [[package]] 468 | name = "winapi-i686-pc-windows-gnu" 469 | version = "0.4.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 472 | 473 | [[package]] 474 | name = "winapi-util" 475 | version = "0.1.5" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 478 | dependencies = [ 479 | "winapi", 480 | ] 481 | 482 | [[package]] 483 | name = "winapi-x86_64-pc-windows-gnu" 484 | version = "0.4.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 487 | 488 | [[package]] 489 | name = "windows-sys" 490 | version = "0.48.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 493 | dependencies = [ 494 | "windows-targets", 495 | ] 496 | 497 | [[package]] 498 | name = "windows-targets" 499 | version = "0.48.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 502 | dependencies = [ 503 | "windows_aarch64_gnullvm", 504 | "windows_aarch64_msvc", 505 | "windows_i686_gnu", 506 | "windows_i686_msvc", 507 | "windows_x86_64_gnu", 508 | "windows_x86_64_gnullvm", 509 | "windows_x86_64_msvc", 510 | ] 511 | 512 | [[package]] 513 | name = "windows_aarch64_gnullvm" 514 | version = "0.48.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 517 | 518 | [[package]] 519 | name = "windows_aarch64_msvc" 520 | version = "0.48.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 523 | 524 | [[package]] 525 | name = "windows_i686_gnu" 526 | version = "0.48.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 529 | 530 | [[package]] 531 | name = "windows_i686_msvc" 532 | version = "0.48.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 535 | 536 | [[package]] 537 | name = "windows_x86_64_gnu" 538 | version = "0.48.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 541 | 542 | [[package]] 543 | name = "windows_x86_64_gnullvm" 544 | version = "0.48.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 547 | 548 | [[package]] 549 | name = "windows_x86_64_msvc" 550 | version = "0.48.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 553 | 554 | [[package]] 555 | name = "winnow" 556 | version = "0.5.33" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" 559 | dependencies = [ 560 | "memchr", 561 | ] 562 | --------------------------------------------------------------------------------