├── .gitignore ├── Cargo.toml ├── flake.nix ├── .github └── workflows │ └── release.yml ├── README.md ├── flake.lock ├── src └── main.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raise" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [profile.release] 7 | strip = true 8 | 9 | [dependencies] 10 | argh = "0.1" 11 | anyhow = "1.0" 12 | miniserde = "0.1" 13 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | naersk-package = { 6 | url = "github:nix-community/naersk"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = { self, nixpkgs, flake-utils, naersk-package, ... }: 12 | flake-utils.lib.eachDefaultSystem (system: let 13 | pkgs = import nixpkgs {inherit system;}; 14 | naersk = pkgs.callPackage naersk-package {}; 15 | in { 16 | defaultPackage = naersk.buildPackage { 17 | src = ./.; 18 | }; 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: taiki-e/create-gh-release-action@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | upload-assets: 18 | strategy: 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: taiki-e/upload-rust-binary-action@v1 28 | with: 29 | bin: raise 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raise 2 | 3 | Run or raise implemented for Hyprland. It will raise window if it exists, 4 | or cycle to next window if current window matches class to focus. Otherwise 5 | it will launch new window. 6 | 7 | ``` 8 | $ raise 9 | Usage: raise -c -e 10 | 11 | Raise window if it exists, otherwise launch new window. 12 | 13 | Options: 14 | -c, --class class to focus 15 | -e, --launch command to launch 16 | --help display usage information 17 | ``` 18 | 19 | ## Install `raise` 20 | 21 | There are multiple ways to install this: 22 | 23 | 1. Go to [releases](https://github.com/svelterust/raise/releases) 24 | 2. `cargo install --git https://github.com/svelterust/raise` 25 | 3. Add `github:svelterust/raise` as a flake to your NixOS configuration 26 | 27 | For NixOS, add raise to your flake inputs: 28 | 29 | ```nix 30 | inputs = { 31 | raise.url = "github:svelterust/raise"; 32 | }; 33 | ``` 34 | 35 | Then add it to your system, for instance: `environment.systemPackages = [raise.defaultPackage.x86_64-linux];` 36 | 37 | ## Example configuration 38 | 39 | I like having Super + `` bound to run or raise, and Super + Shift + `` to launch application regularly. 40 | 41 | ``` 42 | bind = SUPER, V, exec, raise --class "Alacritty" --launch "alacritty" 43 | bind = SUPER_SHIFT, V, exec, alacritty 44 | bind = SUPER, C, exec, raise --class "firefox" --launch "firefox" 45 | bind = SUPER_SHIFT, C, exec, firefox 46 | bind = SUPER, F, exec, raise --class "emacs" --launch "emacsclient --create-frame" 47 | bind = SUPER_SHIFT, F, exec, emacsclient --create-frame 48 | ``` 49 | 50 | ## How to find class? 51 | 52 | Run `hyprctl clients` while window is open, and look for `class: `. 53 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1692799911, 9 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk-package": { 22 | "inputs": { 23 | "nixpkgs": [ 24 | "nixpkgs" 25 | ] 26 | }, 27 | "locked": { 28 | "lastModified": 1692351612, 29 | "narHash": "sha256-KTGonidcdaLadRnv9KFgwSMh1ZbXoR/OBmPjeNMhFwU=", 30 | "owner": "nix-community", 31 | "repo": "naersk", 32 | "rev": "78789c30d64dea2396c9da516bbcc8db3a475207", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "nix-community", 37 | "repo": "naersk", 38 | "type": "github" 39 | } 40 | }, 41 | "nixpkgs": { 42 | "locked": { 43 | "lastModified": 1693158576, 44 | "narHash": "sha256-aRTTXkYvhXosGx535iAFUaoFboUrZSYb1Ooih/auGp0=", 45 | "owner": "NixOS", 46 | "repo": "nixpkgs", 47 | "rev": "a999c1cc0c9eb2095729d5aa03e0d8f7ed256780", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "NixOS", 52 | "ref": "nixos-unstable", 53 | "repo": "nixpkgs", 54 | "type": "github" 55 | } 56 | }, 57 | "root": { 58 | "inputs": { 59 | "flake-utils": "flake-utils", 60 | "naersk-package": "naersk-package", 61 | "nixpkgs": "nixpkgs" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, bail}; 2 | use argh::FromArgs; 3 | use miniserde::{json, Deserialize}; 4 | use std::process::{Child, Command}; 5 | 6 | #[derive(FromArgs)] 7 | /// Raise window if it exists, otherwise launch new window. 8 | struct Args { 9 | /// class to focus 10 | #[argh(option, short = 'c')] 11 | class: String, 12 | 13 | /// command to launch 14 | #[argh(option, short = 'e')] 15 | launch: String, 16 | } 17 | 18 | #[derive(Deserialize, Debug)] 19 | struct Client { 20 | class: String, 21 | address: String, 22 | } 23 | 24 | fn launch_command(args: &Args) -> std::io::Result { 25 | Command::new("hyprctl") 26 | .arg("keyword") 27 | .arg("exec") 28 | .arg(&args.launch) 29 | .spawn() 30 | } 31 | 32 | fn focus_window(address: &str) -> std::io::Result { 33 | Command::new("hyprctl") 34 | .arg("dispatch") 35 | .arg("focuswindow") 36 | .arg(format!("address:{address}")) 37 | .spawn() 38 | } 39 | 40 | fn get_current_matching_window(class: &str) -> Result { 41 | let output = Command::new("hyprctl") 42 | .arg("activewindow") 43 | .arg("-j") 44 | .output()?; 45 | let stdout = String::from_utf8(output.stdout) 46 | .context("Reading `hyprctl currentwindow -j` to string failed")?; 47 | let client = json::from_str::(&stdout)?; 48 | if class == &client.class { 49 | Ok(client) 50 | } else { 51 | bail!("Current window is not of same class") 52 | } 53 | } 54 | 55 | fn main() -> Result<()> { 56 | // Get arguments 57 | let args: Args = argh::from_env(); 58 | 59 | // Launch hyprctl 60 | let json = Command::new("hyprctl").arg("clients").arg("-j").output(); 61 | match json { 62 | Ok(output) if output.status.success() => { 63 | // Deserialize output 64 | let stdout = String::from_utf8(output.stdout) 65 | .context("Reading `hyprctl clients -j` to string failed")?; 66 | let clients = json::from_str::>(&stdout) 67 | .context("Failed to parse `hyprctl clients -j`")?; 68 | 69 | // Filter matching clients 70 | let candidates = clients 71 | .iter() 72 | .filter(|client| client.class == args.class) 73 | .collect::>(); 74 | 75 | // Are we currently focusing a window of this class? 76 | if let Ok(Client { address, .. }) = get_current_matching_window(&args.class) { 77 | // Focus next window based on first 78 | if let Some(index) = candidates.iter().position(|client| client.address == address) { 79 | if let Some(next_client) = candidates.iter().cycle().skip(index + 1).next() { 80 | focus_window(&next_client.address)?; 81 | } 82 | } 83 | } else { 84 | // Focus first window, otherwise launch command 85 | match candidates.first() { 86 | Some(Client { address, .. }) => focus_window(address)?, 87 | _ => launch_command(&args)?, 88 | }; 89 | } 90 | } 91 | // If hyprctl fails, just launch it 92 | _ => { 93 | launch_command(&args)?; 94 | } 95 | } 96 | 97 | // Success 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /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 = "anyhow" 7 | version = "1.0.75" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 10 | 11 | [[package]] 12 | name = "argh" 13 | version = "0.1.12" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" 16 | dependencies = [ 17 | "argh_derive", 18 | "argh_shared", 19 | ] 20 | 21 | [[package]] 22 | name = "argh_derive" 23 | version = "0.1.12" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" 26 | dependencies = [ 27 | "argh_shared", 28 | "proc-macro2", 29 | "quote", 30 | "syn", 31 | ] 32 | 33 | [[package]] 34 | name = "argh_shared" 35 | version = "0.1.12" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" 38 | dependencies = [ 39 | "serde", 40 | ] 41 | 42 | [[package]] 43 | name = "itoa" 44 | version = "1.0.9" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 47 | 48 | [[package]] 49 | name = "mini-internal" 50 | version = "0.1.34" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "4bb752642ceeaad440a788ea0ee23c0317117068f49dc1f25fd23b26146a225d" 53 | dependencies = [ 54 | "proc-macro2", 55 | "quote", 56 | "syn", 57 | ] 58 | 59 | [[package]] 60 | name = "miniserde" 61 | version = "0.1.34" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "c007c51ce98f5f76964762e92fe98967f9fa65943415fd0d0dc6b6bf6bd3da0d" 64 | dependencies = [ 65 | "itoa", 66 | "mini-internal", 67 | "ryu", 68 | ] 69 | 70 | [[package]] 71 | name = "proc-macro2" 72 | version = "1.0.66" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 75 | dependencies = [ 76 | "unicode-ident", 77 | ] 78 | 79 | [[package]] 80 | name = "quote" 81 | version = "1.0.33" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 84 | dependencies = [ 85 | "proc-macro2", 86 | ] 87 | 88 | [[package]] 89 | name = "raise" 90 | version = "0.1.0" 91 | dependencies = [ 92 | "anyhow", 93 | "argh", 94 | "miniserde", 95 | ] 96 | 97 | [[package]] 98 | name = "ryu" 99 | version = "1.0.15" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 102 | 103 | [[package]] 104 | name = "serde" 105 | version = "1.0.188" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 108 | dependencies = [ 109 | "serde_derive", 110 | ] 111 | 112 | [[package]] 113 | name = "serde_derive" 114 | version = "1.0.188" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 117 | dependencies = [ 118 | "proc-macro2", 119 | "quote", 120 | "syn", 121 | ] 122 | 123 | [[package]] 124 | name = "syn" 125 | version = "2.0.29" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" 128 | dependencies = [ 129 | "proc-macro2", 130 | "quote", 131 | "unicode-ident", 132 | ] 133 | 134 | [[package]] 135 | name = "unicode-ident" 136 | version = "1.0.11" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 139 | --------------------------------------------------------------------------------