├── .github └── workflows │ └── on-push.yml ├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── book ├── .gitignore ├── book.toml └── src │ ├── README.md │ ├── SUMMARY.md │ ├── check.svg │ ├── configuration │ ├── README.md │ ├── bar.md │ ├── block.md │ ├── command.md │ ├── concepts.md │ ├── cookbook │ │ ├── README.md │ │ ├── advanced.md │ │ ├── appearance.md │ │ ├── border-lines.png │ │ ├── custom-clock.png │ │ ├── custom-clock.py │ │ ├── data.md │ │ ├── empty-space.png │ │ ├── icon.png │ │ ├── menu-closed.png │ │ ├── menu-open.png │ │ ├── no-space.png │ │ ├── panel_0.png │ │ ├── panel_1.png │ │ ├── panel_2.png │ │ ├── pango.png │ │ ├── partial-bar.png │ │ ├── popup-edge.png │ │ └── separator.png │ ├── img │ │ └── separators.png │ └── variable.md │ ├── installation.md │ ├── links.md │ ├── main.png │ ├── new-setup.png │ ├── panel-sample-left.png │ └── panel-sample-right.png ├── data └── default_config.toml ├── panel-sample-left.png ├── panel-sample-right.png └── src ├── bar.rs ├── cli.rs ├── config.rs ├── desktop.rs ├── drawing.rs ├── engine.rs ├── ipc.rs ├── ipcserver.rs ├── keyboard.rs ├── main.rs ├── parse.rs ├── process.rs ├── protocol.rs ├── source.rs ├── state.rs ├── stats.rs ├── thread.rs ├── timer.rs ├── wait.rs ├── window.rs ├── wmready.rs ├── xrandr.rs └── xutils.rs /.github/workflows/on-push.yml: -------------------------------------------------------------------------------- 1 | name: On Push 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build-and-test: 15 | #runs-on: ubuntu-latest 16 | runs-on: ubuntu-24.04 # Temporary set this because it is newer than -latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v2 20 | 21 | - name: Install Ubuntu tools 22 | run: sudo apt-get update && sudo apt-get install -y libcairo2-dev libpango1.0-dev libx11-xcb-dev 23 | 24 | - name: Install stable toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | components: clippy 31 | 32 | - name: Build 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: build 36 | 37 | - name: Test 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | 42 | - name: Clippy check 43 | uses: actions-rs/clippy-check@v1 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | args: --all-features 47 | deploy-book: 48 | runs-on: ubuntu-latest 49 | permissions: 50 | contents: write # To push a branch 51 | pull-requests: write # To create a PR from that branch 52 | steps: 53 | - uses: actions/checkout@v4 54 | with: 55 | fetch-depth: 0 56 | - name: Install latest mdbook 57 | run: | 58 | tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') 59 | url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" 60 | mkdir mdbook 61 | curl -sSL $url | tar -xz --directory=./mdbook 62 | echo `pwd`/mdbook >> $GITHUB_PATH 63 | tag=$(curl 'https://api.github.com/repos/badboy/mdbook-toc/releases/latest' | jq -r '.tag_name') 64 | url="https://github.com/badboy/mdbook-toc/releases/download/${tag}/mdbook-toc-${tag}-x86_64-unknown-linux-gnu.tar.gz" 65 | mkdir mdbook-toc 66 | curl -sSL $url | tar -xz --directory=./mdbook-toc 67 | echo `pwd`/mdbook-toc >> $GITHUB_PATH 68 | - name: Build Book 69 | run: | 70 | # This assumes your book is in the root of your repository. 71 | # Just add a `cd` here if you need to change to another directory. 72 | cd book 73 | mdbook build 74 | git worktree add gh-pages 75 | git config user.name "Deploy from CI" 76 | git config user.email "" 77 | cd gh-pages 78 | # Delete the ref to avoid keeping history. 79 | git update-ref -d refs/heads/gh-pages 80 | rm -rf * 81 | mv ../book/* . 82 | echo oatbar.app > ./CNAME 83 | git add . 84 | git commit -m "Deploy $GITHUB_SHA to gh-pages" 85 | git push --force --set-upstream origin gh-pages 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.core 3 | flamegraph.svg 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of `oatbar` authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | Google LLC 7 | Igor Petruk 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *Igor Petruk *, the 73 | Project Steward(s) for *Oatbar*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows [Google's Open Source Community 24 | Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Oatbar Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "oatbar" 17 | version = "0.1.0" 18 | edition = "2021" 19 | description = "Powerful and customizable DE and WM status bar" 20 | keywords = ["x11", "wm", "bar"] 21 | categories = ["graphics", "visualization"] 22 | license = "Apache-2.0" 23 | documentation = "https://github.com/igor-petruk/oatbar" 24 | repository = "https://github.com/igor-petruk/oatbar" 25 | homepage = "https://oatbar.app" 26 | default-run="oatbar" 27 | 28 | [[bin]] 29 | name="oatbar-keyboard" 30 | path="src/keyboard.rs" 31 | 32 | [[bin]] 33 | name="oatbar-desktop" 34 | path="src/desktop.rs" 35 | 36 | [[bin]] 37 | name="oatbar-stats" 38 | path="src/stats.rs" 39 | 40 | [[bin]] 41 | name="oatctl" 42 | path="src/cli.rs" 43 | 44 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 45 | 46 | [features] 47 | default=["bmp", "ico", "image", "jpeg", "png", "webp", "svg", "raster"] 48 | image=[] 49 | raster=["dep:image"] 50 | bmp=["image", "raster", "image/bmp"] 51 | ico=["image", "raster", "image/ico"] 52 | jpeg=["image", "raster", "image/jpeg"] 53 | png=["image", "raster", "image/png"] 54 | webp=["image", "raster", "image/webp"] 55 | svg=["image", "dep:resvg"] 56 | profile = ["dep:pprof"] 57 | 58 | [dependencies] 59 | pprof = { version = "0.13", features = ["flamegraph"], optional = true } 60 | anyhow = "1" 61 | bytesize = "1.1" 62 | cairo-rs = { version = "0.19", features = ["xcb"], default-features=false } 63 | derive_builder = "0.20" 64 | dirs = "5" 65 | pangocairo = "0.19" 66 | pango = {version="0.19", features=["v1_52"]} 67 | regex = "1" 68 | serde = { version = "1", features = ["derive"] } 69 | serde_json = "1" 70 | serde_regex = "1.1.0" 71 | serde_with = { version = "3", default-features = false, features = ["macros", "std"] } 72 | toml = "0.8" 73 | tracing = "0.1" 74 | tracing-subscriber = "0.3" 75 | xcb = { version = "1.4", features = ["randr", "xkb", "xlib_xcb", "xinput"] } 76 | itertools = "0.13" 77 | thiserror = "1.0.40" 78 | macro_rules_attribute = "0.2.0" 79 | crossbeam-channel = "0.5.8" 80 | clap = {version="4.4.8", features=["derive"]} 81 | fork = "0.1.22" 82 | libc = "0.2.150" 83 | systemstat = "0.2.3" 84 | nix = { version = "0.29", features = ["net"] } 85 | image = { version = "0.25.1", optional=true, default-features = false } 86 | resvg = {version = "0.42.0", optional=true} 87 | 88 | 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oatbar 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/oatbar.svg)](https://crates.io/crates/oatbar) 4 | ![Latest Build](https://img.shields.io/github/actions/workflow/status/igor-petruk/oatbar/on-push.yml) 5 | ![Crates.io License](https://img.shields.io/crates/l/oatbar) 6 | ![GitHub top language](https://img.shields.io/github/languages/top/igor-petruk/oatbar) 7 | ![Crates.io](https://img.shields.io/crates/d/oatbar?label=Cargo.io%20downloads) 8 | 9 | [![Packaging status](https://repology.org/badge/vertical-allrepos/oatbar.svg)](https://repology.org/project/oatbar/versions) 10 | 11 | Oatbar is a standalone desktop bar that can be used with various WMs and DEs. This bar aims to become one of the most full-featured bars available. 12 | 13 | ## Installation and User Guide 14 | 15 | Please proceed to the [oatbar.app](https://oatbar.app) 16 | 17 | ## Ideas 18 | 19 | I truly aspire to build something unique, what other status bars lack. Do you have a cool feature you'd like to see in oatbar? 20 | 21 | Join the disussion https://github.com/igor-petruk/oatbar/discussions 22 | 23 | ## Disclaimer 24 | 25 | This is not an officially supported Google product. 26 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Igor Petruk"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Oatbar" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/igor-petruk/oatbar/tree/main" 10 | edit-url-template = "https://github.com/igor-petruk/oatbar/edit/main/book/{path}" 11 | 12 | [preprocessor.toc] 13 | command = "mdbook-toc" 14 | renderer = ["html"] 15 | -------------------------------------------------------------------------------- /book/src/README.md: -------------------------------------------------------------------------------- 1 | # Oatbar - standalone desktop bar 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/oatbar.svg)](https://crates.io/crates/oatbar) 4 | ![Latest Build](https://img.shields.io/github/actions/workflow/status/igor-petruk/oatbar/on-push.yml) 5 | ![Crates.io License](https://img.shields.io/crates/l/oatbar) 6 | ![GitHub top language](https://img.shields.io/github/languages/top/igor-petruk/oatbar) 7 | ![Crates.io](https://img.shields.io/crates/d/oatbar?label=Cargo.io%20downloads) 8 | 9 | ![Panel Left](panel-sample-left.png) 10 | ![Panel Right](panel-sample-right.png) 11 | [![Screenshot](main.png)](main.png) 12 | 13 | The motivation for creation of `oatbar` was to extend on the idea of Unix-way for toolbars. 14 | Inspired by `i3bar` which consumes a JSON stream that controls it's appearance, we take this 15 | idea much further without becoming a DIY widget toolkit. 16 | 17 | JSON or plain text streams can be turned into text panels, progress bars or even images without 18 | any significant coding effort. 19 | 20 | | Feature | **`oatbar`** | Basic bars | Bars with built-in plugins | DIY toolbar kits | 21 | |---------|:-------:|:-----:|:----:|:------:| 22 | | [Built-in system data](configuration/cookbook/data.md) | **✅** | **✅** | **✅** | - | 23 | | [Text widgets from custom data](configuration/block.md#text-block) | **✅** | **✅** | **✅** | **✅** | 24 | | [Advanced widgets](configuration/block.md) | **✅** | **✅** | - | **✅** | 25 | | [Advanced widgets with data from a custom script](configuration/block.md) | **✅** | - | - | **✅** | 26 | | [Display image files from a custom script](configuration/block.md#image-block) | **✅** | - | - | **✅** | 27 | | [Generate images dynamically in a custom script](configuration/cookbook/advanced.md#dynamic-image-block) | **✅** | - | - | **✅** | 28 | | [Control all appearance from a custom script](cookbook/appearance.md) | **✅** | - | - | **✅** | 29 | | [Minimal coding](#example) | **✅** | **✅** | **✅** | - | 30 | | [Built-in plugins have no appearance advantage over custom scripts](configuration/cookbook/data.md#common-blocks) | **✅** | - | - | **✅** | 31 | | [Unix-way input via pipes means customization programming language is not forced upon you](configuration/command) | **✅** | **✅** | **✅** | - | 32 | | [Consume data from *other* ecosystems like community scripts for `polybar`, `i3blocks`, `i3status`, `conky`](configuration/cookbook/data.md#third-party-sources) | **✅**[^other_eco] | - | - | **✅** | 33 | 34 | [^other_eco]: In can be partial, like the lack of support for `polybar` formatting, but a lot of scripts are useful. 35 | 36 | ## Example 37 | 38 | ```toml 39 | [[bar]] 40 | height=32 41 | blocks_left=["workspace"] 42 | blocks_right=["clock"] 43 | 44 | [[command]] 45 | name="clock" 46 | command="date '+%a %b %e %H:%M:%S'" 47 | interval=1 48 | 49 | [[command]] 50 | name="desktop" 51 | command="oatbar-desktop" 52 | 53 | [[block]] 54 | name = 'workspace' 55 | type = 'enum' 56 | active = '${desktop:workspace.active}' 57 | variants = '${desktop:workspace.variants}' 58 | on_mouse_left = "oatbar-desktop $BLOCK_INDEX" 59 | 60 | [[block]] 61 | name = 'clock' 62 | type = 'text' 63 | value = '${clock:value}' 64 | ``` 65 | 66 | Here `clock` command sends plain text, but `desktop` streams 67 | structured data in JSON. Each is connected to text and enum selector 68 | widgets respectively. `oatbar-desktop` ships with `oatbar`, but it is an external tool 69 | to a bar, as can be replaced your own script. 70 | 71 | Feel free to run `oatbar-desktop` and investigate it's output. `oatbar` consumes 72 | [multiple text formats](configuration/command.md#formats) and this data can be 73 | displayed with minimal configuration on widgets called [blocks](configuration/block.md). 74 | 75 | ## Ideas 76 | 77 | I truly aspire to build something unique with `oatbar`, something what other status bars lack. Do you have a cool or unconventional feature you'd like to see in `oatbar`? 78 | 79 | [Join the disussion and brainstorm!](https://github.com/igor-petruk/oatbar/discussions) 80 | 81 | ## Next Steps 82 | 83 | * [Installation](./installation.md) 84 | 85 | * [Configuration](./configuration) 86 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./README.md) 4 | - [Installation](./installation.md) 5 | 6 | # Configuration 7 | 8 | - [Overview](./configuration/README.md) 9 | - [Concepts](./configuration/concepts.md) 10 | - [Bar](./configuration/bar.md) 11 | - [Command](./configuration/command.md) 12 | - [Block](./configuration/block.md) 13 | - [Variable](./configuration/variable.md) 14 | - [Cookbook](./configuration/cookbook/README.md) 15 | - [Data](./configuration/cookbook/data.md) 16 | - [Appearance](./configuration/cookbook/appearance.md) 17 | - [Advanced](./configuration/cookbook/advanced.md) 18 | 19 | # Other 20 | 21 | - [Links](./links.md) 22 | -------------------------------------------------------------------------------- /book/src/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /book/src/configuration/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The configuration for the `oatbar` is located at `~/.config/oatbar.toml`. If you do 4 | not have this file, it would be generated with reasonable defaults. 5 | 6 | ![New setup](../new-setup.png) 7 | 8 | Proceed to [concepts](./concepts.md) to learn basic building blocks 9 | of `oatbar` configuration. 10 | 11 | Proceed to [cookbook](./cookbook/) if you are familiar with concepts and you are looking for a _recipe_ to solve a particular problem, proceed to 12 | the particular problem. 13 | 14 |
15 | 16 | The configuration is unstable. 17 | 18 | Until 1.0 release, the configuration change is unstable and 19 | can change between minor releases. Community feedback is needed 20 | to make sure that when configuration stabilizes, it is the best 21 | possible. 22 | 23 |
24 | -------------------------------------------------------------------------------- /book/src/configuration/bar.md: -------------------------------------------------------------------------------- 1 | # Bar 2 | 3 | Bar is a single panel that is positioned at the top or the bottom of a screen. 4 | 5 | Here are all the properties it can have: 6 | 7 | ```toml 8 | [[bar]] 9 | # Blocks to show at different parts of the bar, can be empty. 10 | # Currently it is user's responsibility is to make sure they don't overlap. 11 | blocks_left=["block1", "block2"] 12 | blocks_center=["block3"] 13 | blocks_right=["block4"] 14 | 15 | # Monitor to use as listed by `xrandr` command. 16 | # If unspecified, the primary is used. 17 | monitor="DP-6.8" 18 | 19 | # Height of the bar. 20 | height=32 21 | 22 | # "bottom" is a default value. 23 | position="top" 24 | 25 | # Empty space between the blocks and the bar edges. 26 | margin=5 27 | 28 | # A bar is normally hidden, unless pops up. 29 | popup=true 30 | 31 | # Make a popup bar pop up when the mouse is near the edge of the screen. 32 | popup_at_edge=true 33 | 34 | # List of pairs [expression, regex]. 35 | # Show the block only if all expressions match respective regexes. 36 | show_if_matches=[["${clock:value}",'.+']] 37 | ``` 38 | -------------------------------------------------------------------------------- /book/src/configuration/block.md: -------------------------------------------------------------------------------- 1 | # Block 2 | 3 | Blocks are the widgets displaying pieces of information on the bar. 4 | 5 | `oatbar` provides a lot of hidden power via these widgets, as they can provide 6 | more than they initially seem. Consider that the most of the string properties 7 | of blocks support variable substitution, directly controlled by your scripts. 8 | 9 | The reference below explains properties of these blocks and the 10 | [Cookbook](./cookbook/) shows how to use them in a very clever way. 11 | 12 | 13 | 14 | ## Common properties 15 | 16 | ```toml 17 | [[block]] 18 | # Name used as a reference 19 | name="block-name" 20 | 21 | # Main input for the block. 22 | value="Clock: ${clock:value}" 23 | 24 | # A series of regex replacements that 25 | # are applied to `value`. 26 | # See https://docs.rs/regex/latest/regex/ 27 | replace=[ 28 | ["^1$","www"], 29 | ["^2$","term"] 30 | ] 31 | # If true, stop applying replaces after one row matches. 32 | # If false, keep applying replaces to the end. 33 | replace_first_match=false 34 | 35 | # If set, formats the final value contents after all processing and transformations 36 | # like regex replacements or progress bar rendering. 37 | # Not supported by image blocks. 38 | output_format="cpu: ${value}" 39 | 40 | # If true (default), full Pango markup is supported. 41 | # https://docs.gtk.org/Pango/pango_markup.html 42 | # It may be desirable to turn it off if input has 43 | # HTML-like text to be displayed. 44 | pango_markup=true 45 | 46 | # List of pairs [expression, regex]. 47 | # Show the block only if all expressions match respective regexes. 48 | show_if_matches=[["${clock:value}",'.+']] 49 | 50 | # If set, and the bar has popup=true, then this block 51 | # can pop up. 52 | # - block - the block itself pops up 53 | # - partial_bar - the partial bar pops up 54 | # - bar - the entire bar pops up. 55 | # The surrounding separators will appear as appropriate. 56 | popup="partial_bar" 57 | # If unset, the popup is triggered by any property change. 58 | # If set, the popup is triggered by change of this property. 59 | popup_value="${clock:value}" 60 | 61 | # Font and size of the text in the block. 62 | font="Iosevka 14" 63 | 64 | # Base RGBA colors of the blocks. 65 | background="#101010bb" 66 | foreground="#ffff00" 67 | 68 | # Properties of lines around the block, if set. 69 | overline_color="#..." 70 | underline_color="#..." 71 | edgeline_color="#..." 72 | line_width=0.4 73 | 74 | # Margin and padding of the block within a bar. 75 | margin=3.0 76 | padding=5.0 77 | 78 | # A command to run on a particular mouse event. 79 | # It is run with `sh -c "..."` and the process will be detached from oatbar. 80 | # BLOCK_NAME and BLOCK_VALUE environment variables are set. 81 | # For `enum` blocks, BLOCK_INDEX is set too. 82 | on_mouse_left = 'chrome' 83 | on_mouse_middle = 'chrome' 84 | on_mouse_right = 'chrome' 85 | on_scroll_up = 'chrome' 86 | on_scroll_down = 'chrome' 87 | ``` 88 | 89 | To avoid repetition, consider using `default_block`, that 90 | supports all common properties. 91 | 92 | ```toml 93 | [[default_block]] 94 | background="#202020" 95 | ``` 96 | 97 | Multiple named `default_block` sections can be used. 98 | 99 | ```toml 100 | [[default_block]] 101 | name="ws1_widgets" 102 | 103 | [[block]] 104 | inherit="ws1_widgets" 105 | ``` 106 | 107 | ## Text block 108 | 109 | ```toml 110 | [[block]] 111 | type="text" 112 | ``` 113 | 114 | Text blocks include all common properties, which should be enough to show 115 | basic text or icons using [Pango markup](https://docs.gtk.org/Pango/pango_markup.html), 116 | icon fonts such as [Font Awesome](https://fontawesome.com/), 117 | [Nerd Fonts](https://www.nerdfonts.com/), [IcoMoon](https://icomoon.io/) or emojis. 118 | 119 | In addition, text blocks are used as separators to create **partial bars**. 120 | They are smaller bars within a bar that groups multiple blocks together. 121 | 122 | ![Separators](img/separators.png) 123 | 124 | ```toml 125 | [[bar]] 126 | blocks_right=["L", "music", "R", "E", "L", "layout", "S", "clock", "R"] 127 | 128 | [[block]] 129 | name="music" 130 | ... 131 | show_if_matches = [['${player:now_playing.full_text}', '.+']] 132 | popup = "partial_bar" 133 | 134 | [[block]] 135 | name="S" 136 | type = "text" 137 | separator_type = "gap" 138 | value = "|" 139 | 140 | [[block]] 141 | name="E" 142 | type = "text" 143 | separator_type = "gap" 144 | value = " " 145 | background = "#00000000" 146 | 147 | [[block]] 148 | name="L" 149 | type = "text" 150 | separator_type = "left" 151 | separator_radius = 8.0 152 | 153 | [[block]] 154 | name="R" 155 | type = "text" 156 | separator_type = "right" 157 | separator_radius = 8.0 158 | ``` 159 | 160 | `separator_type` gives a hint on where partial bars are located. 161 | This helps when `popup="partial_bar"`. It also helps to collapse 162 | unnecessary separators when normal blocks around them are hidden. 163 | 164 | ## Number block 165 | 166 | ```toml 167 | [[block]] 168 | type="number" 169 | ``` 170 | 171 | Number can be displayed as text on the text block. But the real 172 | value comes when the bar understands that the data is a number. 173 | 174 | In addition to common properties, the number blocks 175 | support unit conversions and alternative forms of display, 176 | such as progress bars. 177 | 178 | ```toml 179 | # Min/max values are used in progress bars. 180 | # They are set as string because they support 181 | # variable substituion and can be specified in units. 182 | min_value="0" 183 | max_value="1000" 184 | 185 | # A number type that input represents. 186 | # - number - a number from min to max 187 | # - percent - a number from 0 to 100, '%' is ommitted from the input when parsing. 188 | # - bytes - a number that supports byte unit suffixes, e.g. "GB", "kb", 189 | # - See https://docs.rs/bytesize/latest/bytesize/ 190 | number_type="percent" 191 | 192 | # A sorted list of ramp formats. If set, prior to wrapping with `output_format`, 193 | # wrap to the format from the entry larger than `value`. 194 | ramp = [ 195 | ["80%", "${value}"], 196 | ["90%", "${value}"], 197 | ] 198 | ``` 199 | 200 | `number_display` can be used to select the widget that is going to display your 201 | number on the block. 202 | 203 | ### Number as text 204 | 205 | You can display the number as text as you would have with a `text` block, but there 206 | is a benefit of additional functionality, such as unit conversions and ramp 207 | functionality. 208 | 209 | 210 | ```toml 211 | [[block]] 212 | type="number" 213 | name="cpu" 214 | ... 215 | number_display="progress_bar" 216 | ``` 217 | 218 | ### Progress bar 219 | 220 | ```toml 221 | [[block]] 222 | type="number" 223 | name="cpu" 224 | ... 225 | number_display="progress_bar" 226 | # How many characters to use for the progress bar. 227 | progress_bar_size=10 228 | # Progress bar characters. In this example would render: "━━━━雷 " 229 | empty=" " 230 | fill="━" 231 | indicator="雷" 232 | # Each of the above can be a ramp 233 | # fill = [ 234 | # ["", "━"], 235 | # ["60%", ""], 236 | # ["90%", ""], 237 | # ] 238 | ``` 239 | 240 | ## Enum block 241 | 242 | ```toml 243 | [[block]] 244 | type="enum" 245 | ``` 246 | 247 | Enum is different from text block as it renders multiple child text blocks called 248 | `variants`, only one of which is `active`. *Example:* keyboard layout switch. 249 | 250 | Almost every common property related to block display has an `active_` counterpart 251 | to configure an active block. For example `background` and `active_background`. 252 | 253 | ```toml 254 | # A separated list of variants, e.g. "ua,se,us". 255 | variants = '${keyboard:layout.variants}' 256 | # An index of the item that is to be active starting from 0. 257 | active = '${keyboard:layout.active}' 258 | # A separator for the variants list. Default: ",". 259 | enum_separator="|" 260 | ``` 261 | 262 | Text processing via `replace` is done per item of the `variants` separately, 263 | not together. If an variant becomes empty as a result of processing, it will 264 | not be displayed, but it won't impact the meaning of `active` index. 265 | 266 | `BLOCK_INDEX` environment variable set for `on_mouse_left` command is set to 267 | the index of the variant that was clicked on. 268 | 269 | ## Image block 270 | 271 | ```toml 272 | [[block]] 273 | type="image" 274 | ``` 275 | 276 | In image blocks, the `value` property is interpreted as and image file name to be 277 | rendered. Supported formats: BMP, ICO, JPEG, PNG, SVG, WEBP. 278 | 279 | ``` 280 | # If set, can shrink the image smaller than automatically determined size. 281 | max_image_height=20 282 | # If this value is set and changed, then image caching gets disabled and 283 | # image is reloaded from the filesystem even if the filename stayed the same. 284 | # It can be used by a command to generate dynamic images under the same filename. 285 | updater_value="${image_generator:timestamp}" 286 | ``` 287 | 288 | The block offers rich possibilities, provided you can generate your own 289 | images or download them from the Internet on flight in the command that generates 290 | a filename. 291 | -------------------------------------------------------------------------------- /book/src/configuration/command.md: -------------------------------------------------------------------------------- 1 | # Command 2 | 3 | Command is an external program that provides data to `oatbar`. 4 | 5 | ```toml 6 | # Runs periodically 7 | [[command]] 8 | name="disk_free" 9 | command="df -h / | tail -1 | awk '{print $5}'" 10 | interval=60 # Default is 10. 11 | 12 | # Runs once 13 | [[command]] 14 | name="uname" 15 | command="uname -a" 16 | once=true # Default is false. 17 | 18 | # Streams continiously 19 | [[command]] 20 | name="desktop" 21 | command="oatbar-desktop" 22 | # format="i3bar" 23 | ``` 24 | 25 | `oatbar` will run each command as `sh -c "command"` to support basic shell 26 | substitutions. 27 | 28 | ### Formats 29 | 30 | Formats are usually auto-detected and there is no need to set `format` explicitly. 31 | 32 | #### `plain` 33 | 34 | Plain text format is just text printed to `stdout`. 35 | 36 | ```toml 37 | name="hello" 38 | command="echo Hello world" 39 | ``` 40 | 41 | This will set `${hello:value}` [variable](./variable.md) to be used 42 | by [blocks](./block.md). If the command outputs multiple lines, each print 43 | will set this variable to a new value. If the command runs indefinitely, the 44 | pauses between prints can be used to only update the variable when necessary. 45 | When the command is complete, it will be restarted after `interval` 46 | seconds (the default is `10`). 47 | 48 | If `line_names` are set, then the output is expected in groups of 49 | multiple lines, each will set it's own variable, like `${hello:first_name}` and 50 | `${hello:last_name}` in the following example: 51 | 52 | ```toml 53 | name="hello" 54 | line_names=["first_name", "last_name"] 55 | ``` 56 | 57 | Many [`polybar` scripts](https://github.com/polybar/polybar-scripts) can be 58 | used via the `plain` format, as soon as they don't use polybar specific 59 | formatting. 60 | 61 | [`i3blocks` raw format](https://vivien.github.io/i3blocks/#_format) plugins 62 | can be consumed too by means of the `line_names` set to standard names for 63 | `i3blocks`. 64 | 65 | #### `i3bar` 66 | 67 | [`i3bar` format](https://oatbar.app/index.html) is the richest supported format. 68 | It supports multiple streams of data across multiple "instances" of these streams. 69 | In [`i3wm`](i3wm.org) this format fully controls the display of `i3bar`, where 70 | for `oatbar` it is a yet another data source that needs to be explicitly 71 | connected to properties of the blocks. For example instead of coloring 72 | the block, you can choose to color the entire bar. Or you can use color `red` coming 73 | from an `i3bar` plugin as a signal to show a hidden block. 74 | 75 | Plugins that `oatbar` ships with use this format. 76 | 77 | ```toml 78 | [[command]] 79 | name="desktop" 80 | command="oatbar-desktop" 81 | ``` 82 | 83 | The command `output-desktop` outputs: 84 | 85 | ```json 86 | {"version":1} 87 | [ 88 | [{"full_text":"workspace: 1","name":"workspace","active":0,"value":"1","variants":"1,2,3"}, 89 | {"full_text":"window: Alacritty","name":"window_title","value":"Alacritty"}], 90 | ... 91 | ``` 92 | 93 | This command is named `desktop` in the config. 94 | 95 | Each entry is groups variables under a different `name` that 96 | represents a purpose of the data stream, in this case: `workspace` 97 | and `window_title`. Multiple entries with the same `name`, but different 98 | `instance` field to represent further breakdown (e.g. names of 99 | network interfaces from a network plugin). 100 | 101 | The output from above will set 102 | the following variables, run `oatbar` and see them in real-time 103 | 104 | ```shell 105 | $ oatctl var ls 106 | desktop:workspace.active=0 107 | desktop:workspace.value=1 108 | desktop:workspace.variants=1,2,3 109 | desktop:workspace.full_text=workspace: 1 110 | desktop:window_title.value=Alacritty 111 | desktop:window_title.full_text=window: Alacritty 112 | ``` 113 | 114 | If `instance` is present in the entry, then the name of the variable is 115 | `command_name:name.instance.variable`. 116 | -------------------------------------------------------------------------------- /book/src/configuration/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | [**Command**](./command.md) is a source of data that streams [**variables**](variable.md). Variables are 4 | used in properties of [**blocks**](block.md). [**Bars**](bar.md) are made of blocks. All you have to learn. 5 | 6 | ```toml 7 | [[bar]] 8 | blocks_right=["clock"] 9 | 10 | [[block]] 11 | name = 'clock' 12 | type = 'text' 13 | value = '${clock:value}' 14 | 15 | [[command]] 16 | name="clock" 17 | command="date '+%a %b %e %H:%M:%S'" 18 | ``` 19 | 20 | The `oatctl` tool can be used to interact with `oatbar`. For more info: 21 | 22 | ```console 23 | oatctl help 24 | ``` 25 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/README.md: -------------------------------------------------------------------------------- 1 | # Cookbook 2 | 3 | `oatbar` tries to keep pre-packaged plugins or modules to a minimum. 4 | Instead it offers easy ways of achieving the same by integrating 5 | external data sources, tools or modules from ecosystems of other bars. 6 | 7 | ## Next 8 | 9 | 1. Learn where to get [**data**](./data.md) to show it on the bar. 10 | 2. If you have the data, proceed learning how to [**display**](./appearance.md) it. 11 | 3. If you know the basic, see some [**advanced applications**](./advanced.md) of `oatbar` concepts. 12 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced examples 2 | 3 | Let's build something complex with what `oatbar` has to offer. 4 | 5 | 6 | 7 | Combination of variables, visibility control and programmatic access to variables via `oatctl var` provides 8 | tremendous power. 9 | 10 | In these examples `oatctl var` is often called from `on_mouse_left` handlers, but you 11 | can use it in your WM keybindings too. 12 | 13 | ## Workspace customizations 14 | 15 | If you have enabled `oatbar-desktop` command, you should have access to the `${desktop:workspace.value}` 16 | variable. 17 | 18 | ```toml 19 | [[command]] 20 | name="desktop" 21 | command="oatbar-desktop" 22 | ``` 23 | 24 | See which values it can have via `oatctl var ls | grep desktop` when running `oatbar`. You can use 25 | this to set any properties of your block, including appearance and visibility. 26 | 27 | ### Appearance 28 | 29 | In this example, the bar on workspace `two` is a bit more red than usual. 30 | 31 | ```toml 32 | [[var]] 33 | name="default_block_bg" 34 | value="${desktop:workspace.value}" 35 | replace_first_match=true 36 | replace=[ 37 | ["^two$","#301919e6"], 38 | [".*", "#191919e6"], 39 | ] 40 | 41 | [[default_block]] 42 | background="${default_block_bg}" 43 | ``` 44 | 45 | ### Visibility 46 | 47 | This block shown only on workspace `three`. 48 | 49 | ```toml 50 | [[block]] 51 | show_if_matches=[["${desktop:workspace.value}", "^three$"]] 52 | ``` 53 | 54 | ## Menu 55 | 56 | `oatbar` does not know anything about menus, but let's build one. 57 | 58 | ![Menu Closed](menu-closed.png) 59 | 60 | ![Menu Open](menu-open.png) 61 | 62 | ```toml 63 | [[bar]] 64 | blocks_left=["L", "menu", "launch_chrome", "launch_terminal", "R"] 65 | 66 | [[default_block]] 67 | background="#191919e6" 68 | 69 | [[default_block]] 70 | name="menu_child" 71 | background="#111111e6" 72 | line_width=3 73 | overline_color="#191919e6" 74 | underline_color="#191919e6" 75 | show_if_matches=[["${show_menu}","show"]] 76 | 77 | [[block]] 78 | name='menu' 79 | type = 'text' 80 | value = "${show_menu}" 81 | replace = [ 82 | ["^$", "circle-right"], 83 | ["show", "circle-left"], 84 | ["(.+)","$1"], 85 | ] 86 | on_mouse_left = "oatctl var rotate show_menu right '' show" 87 | 88 | [[block]] 89 | name='launch_chrome' 90 | type = 'text' 91 | inherit="menu_child" 92 | value = " " 93 | on_mouse_left = "oatctl var set show_menu ''; chrome" 94 | 95 | [[block]] 96 | name='launch_terminal' 97 | type = 'text' 98 | inherit="menu_child" 99 | value = " " 100 | on_mouse_left = "oatctl var set show_menu ''; alacritty" 101 | ``` 102 | 103 | Let's take a closer look: 104 | 105 | 1. We create a `show_menu` variable that can be empty or set to `show` 106 | 1. In `menu` block all regexes apply in sequence. 107 | 1. The first two replace it with icon names. 108 | 1. The last one wraps the icon name into the final Pango markup. 109 | 1. The `on_mouse_left` rotates the values of `show_menu` between empty and `show`, effectively toggling it. 110 | 1. Blocks are only displayed if `show_menu` is set. 111 | 1. Blocks clear `show_menu` before launching the app to hide the menu. 112 | 1. A small cosmetic effect is achieved by inheriting a `default_block` with a different style. 113 | 114 | This example can be extended to build more layers of nesting by introducing additional variables. 115 | 116 | ## Rotating menu 117 | 118 | It sometimes useful to always show the main panel, but have an occasional access to additional 119 | information. A great idea would be to build a rotating menu. 120 | 121 | ![Panel 0](panel_0.png)   122 | ![Panel 1](panel_1.png)   123 | ![Panel 2](panel_2.png) 124 | 125 | ```toml 126 | [[bar]] 127 | blocks_left=["L", "rotate_left", "panel_0", "panel_1", "panel_2", "rotate_right", "R"] 128 | 129 | [[block]] 130 | name='rotate_left' 131 | type = 'text' 132 | value = "circle-left" 133 | on_mouse_left = "oatctl var rotate rotation_idx left '' 1 2" 134 | 135 | [[block]] 136 | name='rotate_right' 137 | type = 'text' 138 | value = "circle-right" 139 | on_mouse_left = "oatctl var rotate rotation_idx right '' 1 2" 140 | 141 | [[block]] 142 | name='panel_0' 143 | type = 'text' 144 | value = "Panel 0" 145 | show_if_matches=[["${rotation_idx}", "^$"]] 146 | 147 | [[block]] 148 | name='panel_1' 149 | type = 'text' 150 | value = "Panel 1" 151 | show_if_matches=[["${rotation_idx}", "1"]] 152 | 153 | [[block]] 154 | name='panel_2' 155 | type = 'text' 156 | value = "Panel 2" 157 | show_if_matches=[["${rotation_idx}", "2"]] 158 | ``` 159 | 160 | ## Dynamic image block 161 | 162 | ![Custom clock](custom-clock.png) 163 | 164 | This looks like a normal clock, but it is actually loaded from the PNG, 165 | that was generated by a custom command. You can display **arbitrary graphics**! 166 | 167 | A program to generate an image: 168 | 169 | ```python 170 | {{#include custom-clock.py}} 171 | ``` 172 | 173 | If run, it would write `/tmp/custom-clock.png` and print text like this 174 | 175 | ```shell 176 | $ python3 ./book/src/configuration/cookbook/custom-clock.py 177 | /tmp/custom-clock.png 178 | 2024-06-30 21:09:22.165792 179 | /tmp/custom-clock.png 180 | 2024-06-30 21:09:23.185004 181 | /tmp/custom-clock.png 182 | 2024-06-30 21:09:24.195003 183 | ``` 184 | 185 | ```toml 186 | [[command]] 187 | name="img-clock" 188 | command="python3 /home/user/Project/oatbar/clock.py" 189 | line_names=["file_name", "ts"] 190 | 191 | [[block]] 192 | name = 'image' 193 | type = 'image' 194 | value = '${img-clock:file_name}' 195 | updater_value = '${img-clock:ts}' 196 | ``` 197 | 198 | The value of `img-clock:ts` is not important, but because it 199 | changes, the image is not cached. It get's reloaded from 200 | the disk on each update. 201 | 202 |
203 | 204 | This is not the most efficient way to build custom 205 | widgets. 206 | 207 | It involves writing/reading files from the 208 | disk on each update. Building animations is possible, but 209 | not efficient, which can matter for you if you are on 210 | laptop battery. You can use `tmpfs` to save on disk writes, 211 | but not so much on CPU cycles. 212 | 213 |
214 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/appearance.md: -------------------------------------------------------------------------------- 1 | # Appearance 2 | 3 | This section focuses on recipes on how to display the data from your sources. 4 | 5 | 6 | 7 | ## Separators 8 | 9 | ### Simple separator 10 | 11 | ![Separator](separator.png) 12 | 13 | Separator is just a text block. 14 | 15 | ```toml 16 | [[bar]] 17 | blocks_left=["foo", "S", "bar", "S", "baz"] 18 | 19 | [[block]] 20 | name="S" 21 | type = 'text' 22 | separator_type = 'gap' 23 | value = '|' 24 | foreground = "#53e2ae" 25 | ``` 26 | 27 | This approach offers maximum flexibility: 28 | * Multiple separator types and styles 29 | * Dynamically separators based on conditions 30 | * Disappearing separators via `show_if_matches` 31 | 32 | Specifying `separator_type = "gap"` is recommended. It gives `oatbar` a hint that the block is 33 | a separator. For example multiple separators in a row do not make sense and 34 | they will collapse if real blocks between them become hidden. 35 | 36 | ### Empty space around bar 37 | 38 | By default `oatbar` looks more traditional. 39 | 40 | ![No Space](no-space.png) 41 | 42 | You can apply margins and border-lines to achieve some empty space around your bar. 43 | 44 | ![Empty Space](empty-space.png) 45 | 46 | ```toml 47 | [[bar]] 48 | blocks_right=["L", "layout", "S", "clock", "R"] 49 | # `top` may need a value or must be zero depening on your WM. 50 | margin={left=8, right=8, top=0, bottom=8} 51 | # Alpha channels is zero, so the bar is transparent unless there is has a block. 52 | background="#00000000" 53 | 54 | [[default_block]] 55 | # The actual block color. 56 | background="#191919e6" 57 | 58 | [[block]] 59 | name='L' 60 | type = 'text' 61 | separator_type = 'left' 62 | separator_radius = 8.0 63 | 64 | [[block]] 65 | name='R' 66 | type = 'text' 67 | separator_type = 'right' 68 | separator_radius = 8.0 69 | ``` 70 | 71 | ### Border lines 72 | 73 | ![Border Lines](border-lines.png) 74 | 75 | ```toml 76 | # Or per [[block]] separately. 77 | [[default_block]] 78 | edgeline_color = "#53e2ae" 79 | overline_color = "#53e2ae" 80 | underline_color = "#53e2ae" 81 | 82 | # edgeline applies to `left` and `right` blocks. 83 | [[block]] 84 | name='L' 85 | type = 'text' 86 | separator_type = 'left' 87 | separator_radius = 8.0 88 | 89 | [[block]] 90 | name='R' 91 | type = 'text' 92 | separator_type = 'right' 93 | separator_radius = 8.0 94 | ``` 95 | 96 | 97 | ### Partial bar 98 | 99 | Bars of `oatbar` can be further separated to small partial bars. It is possible 100 | to by further use of `L` and `R` bars and addition of completely transparent `E` block. 101 | 102 | ![Partial Bar](partial-bar.png) 103 | 104 | ```toml 105 | [[bar]] 106 | blocks_left=["L", "workspace", "R", "E", "L", "active_window", "R"] 107 | 108 | [[block]] 109 | name="E" 110 | type = 'text' 111 | separator_type = 'gap' 112 | value = ' ' 113 | background = "#00000000" 114 | # If you have set these in [[default_block]], reset them back. 115 | edgeline_color = "" 116 | overline_color = "" 117 | underline_color = "" 118 | ``` 119 | 120 | Setting `separator_type` correctly for all separators will make partial panel 121 | disappearing if all real blocks are hidden via `show_if_matches`. 122 | 123 | ## Blocks 124 | 125 | ### Pango markup 126 | 127 | `oatbar` supports full [Pango Markup](https://docs.gtk.org/Pango/pango_markup.html) 128 | as a main tool to format the block content. Command can emit Pango markup too, controlling 129 | the appearance of the blocks. 130 | 131 | ![Pango](pango.png) 132 | 133 | ```toml 134 | [[block]] 135 | name='pango' 136 | type = 'text' 137 | value = "hello World" 138 | ``` 139 | 140 | Font names to be used in Pango can be looked up via the `fc-list` command. 141 | 142 | ### Icons 143 | 144 | Use icon fonts such as [Font Awesome](https://fontawesome.com/), 145 | [Nerd Fonts](https://www.nerdfonts.com/), [IcoMoon](https://icomoon.io/) or emojis. 146 | 147 | ![Icon](icon.png) 148 | 149 | ```toml 150 | [[block]] 151 | name='pango' 152 | type = 'text' 153 | value = " Symbolico - I'm free" 154 | 155 | [[var]] 156 | name="green_icon" 157 | value="font='Font Awesome 6 Free 13' foreground='#53e2ae'" 158 | ``` 159 | 160 | Some icon fonts use ligatures instead of emojis, replacing words with icons like this: 161 | 162 | ```toml 163 | value = "music Symbolico - I'm free" 164 | ``` 165 | 166 | If your icon does not perfectly vertically align with your text, experiment with font size and `rise` Pango parameter. 167 | 168 | ### Visibility 169 | 170 | `show_if_matches` combined with a powerful tool to build dynamic bars. 171 | Here it is used to only show the block if the value is not empty. 172 | 173 | ```toml 174 | [block] 175 | value = '${desktop:window_title.value}' 176 | show_if_matches = [['${desktop:window_title.value}', '.+']] 177 | ``` 178 | 179 | Custom variables, not only those coming from commands can be used. They can be 180 | set via `oatctl var set`, opening a huge number of possibilities. See some examples in 181 | the [Advanced](./advanced.md) chapter. 182 | 183 | If you are not an expert in regular expressions, here are some useful ones: 184 | 185 | | Regex | Meaning | 186 | |---------|-----------| 187 | | `foo` | Contains `foo` | 188 | | `^foo` | Starts with `foo` | 189 | | `foo$` | Ends with `foo` | 190 | | `^foo$` | Exactly `foo` | 191 | | `^$` | Empty string | 192 | | `.+` | Non empty string | 193 | | (foo|bar|baz) | Contains one of those words | 194 | | ^(foo|bar|baz)$ | Exactly one of those words | 195 | 196 | In the examples above `foo` works because it only contains alpha-numeric characters, 197 | but be careful with including characters that have special meaning in regular expressions. 198 | For more info read `regex` crate [documentation](https://docs.rs/regex/latest/regex/#syntax). 199 | 200 | ## Popup bars 201 | 202 | A bar can be hidden and appear when certain conditions are met. 203 | 204 | ```toml 205 | [[bar]] 206 | popup=true 207 | ``` 208 | 209 | Popup bars appear on the top of the windows, unlike normal bars that 210 | allocate dedicated space on the screen. 211 | 212 | ![Popup Edge](popup-edge.png) 213 | 214 | ### Popup at cursor 215 | 216 | Popup bar can be shown when the cursor approaches the screen edge where the bar is located. 217 | 218 | ```toml 219 | [[bar]] 220 | popup=true 221 | popup_at_edge=true 222 | ``` 223 | 224 | ### Temporary popup on block change 225 | 226 | When *any property* of the block changes you can make it appear. Depending on a `popup` 227 | value you can show enclosing partial or entire bar. 228 | 229 | ```toml 230 | [[bar]] 231 | popup="bar" 232 | #popup="partial_bar" 233 | #popup="block" 234 | ``` 235 | 236 | If you won't want to popup on any property change, you can limit it to one expression. 237 | 238 | ```toml 239 | [[bar]] 240 | popup_value="${foo}" 241 | ``` 242 | 243 | Example layout switcher that appears in the middle of the screen when you change your layout. 244 | 245 | ```toml 246 | [[bar]] 247 | blocks_center=["L", "layout_enum_center", "R"] 248 | background="#00000000" 249 | popup=true 250 | position="center" 251 | 252 | [[block]] 253 | name = 'layout_enum_center' 254 | type = 'enum' 255 | active = '${keyboard:layout.active}' 256 | variants = '${keyboard:layout.variants}' 257 | popup = "block" 258 | ``` 259 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/border-lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/border-lines.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/custom-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/custom-clock.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/custom-clock.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import cairo 3 | import os 4 | import time 5 | import sys 6 | 7 | while True: 8 | WIDTH, HEIGHT = 270, 28 9 | sfc = cairo.ImageSurface(cairo.Format.ARGB32, WIDTH, HEIGHT) 10 | ctx = cairo.Context(sfc) 11 | ctx.set_font_size(16) 12 | ctx.select_font_face("Courier", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 13 | ctx.move_to(3, 20) 14 | ctx.set_source_rgb(1.0, 1.0, 1.0) 15 | time_str="%s" % datetime.datetime.now() 16 | ctx.show_text(time_str) 17 | ctx.fill() 18 | 19 | output_filename = '/tmp/custom-clock.png' 20 | sfc.write_to_png(output_filename) 21 | 22 | print(output_filename) 23 | print(time_str) 24 | sys.stdout.flush() 25 | time.sleep(1) 26 | 27 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/data.md: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | This chapter contains examples of common sources and methods of ingesting 4 | data for your blocks. 5 | 6 | 7 | 8 | ## Common Blocks 9 | 10 | ### App Launcher 11 | 12 | ```toml 13 | [[block]] 14 | name='browser' 15 | type = 'text' 16 | value = " " 17 | on_mouse_left = 'chrome' 18 | ``` 19 | 20 | ### Clock 21 | 22 | ```toml 23 | [[command]] 24 | name="clock" 25 | command="date '+%a %b %e %H:%M:%S'" 26 | interval=1 27 | 28 | [[block]] 29 | name = 'clock' 30 | type = 'text' 31 | value = '${clock:value}' 32 | ``` 33 | 34 | If you do not need to show seconds, you can make `interval` smaller. 35 | 36 | ### Keyboard 37 | 38 | `oatbar` ships with keyboard status utility that streams keyboard layouts 39 | and indicator values in `i3bar` format. 40 | 41 | If you run it, you will see 42 | 43 | ```json 44 | ❯ oatbar-keyboard 45 | {"version":1} 46 | [ 47 | [{"full_text":"layout: us","name":"layout","active":0,"value":"us","variants":"us,ua"}, 48 | {"full_text":"caps_lock:off","name":"indicator","instance":"caps_lock","value":"off"}, 49 | ...], 50 | ``` 51 | 52 | Enable it with 53 | 54 | ```toml 55 | [[command]] 56 | name="keyboard" 57 | command="oatbar-keyboard" 58 | ``` 59 | 60 | #### Layout 61 | 62 | `oatbar-keyboard` is designed to work with `setxkbmap`. 63 | For example you set up your layouts on each WM start like this: 64 | 65 | ```shell 66 | setxkbmap -layout us,ua -option grp:alt_space_toggle 67 | ``` 68 | 69 | Enable `oatbar-keyboard` and use an `enum` block: 70 | 71 | ```toml 72 | [[block]] 73 | name = 'layout' 74 | type = 'enum' 75 | active = '${keyboard:layout.active}' 76 | variants = '${keyboard:layout.variants}' 77 | on_mouse_left = "oatbar-keyboard layout set $BLOCK_INDEX" 78 | ``` 79 | 80 | #### Indicators 81 | 82 | Show indicators, such as `caps_lock`, `scroll_lock` and `num_lock` 83 | as follows: 84 | 85 | ```toml 86 | [[block]] 87 | name = 'caps_lock' 88 | type = 'text' 89 | value = '${keyboard:indicator.caps_lock.full_text}' 90 | ``` 91 | 92 | ### Active workspaces and windows 93 | 94 | ```oatbar-desktop``` talks to your WM via EWMH protocol to obtain 95 | the information about active workspaces and windows. 96 | 97 | ```json 98 | ❯ oatbar-desktop 99 | {"version":1} 100 | [ 101 | [{"full_text":"workspace: 1","name":"workspace","active":0,"value":"1","variants":"1,2,3"}, 102 | {"full_text":"window: Alacritty","name":"window_title","value":"Alacritty"}], 103 | ``` 104 | 105 | ```toml 106 | [[command]] 107 | name="desktop" 108 | command="oatbar-desktop" 109 | 110 | [[block]] 111 | name = 'workspace' 112 | type = 'enum' 113 | active = '${desktop:workspace.active}' 114 | variants = '${desktop:workspace.variants}' 115 | # Optional replacement with icons 116 | replace = [ 117 | ["1",""], 118 | ["2",""], 119 | ["3",""] 120 | ] 121 | font="Font Awesome 6 Free 13" 122 | on_mouse_left = "oatbar-desktop $BLOCK_INDEX" 123 | 124 | [[block]] 125 | name='window' 126 | type = 'text' 127 | value = '${desktop:window_title.value|max:100}' 128 | pango_markup = false # Window title can happen to have HTML. 129 | ``` 130 | 131 | ### System stats 132 | 133 | `oatbar` ships with a `oatbar-stats` utility that streams system stats in the `i3bar` 134 | format: 135 | 136 | * CPU 137 | * Memory 138 | * Network 139 | * Interface names 140 | * Running status 141 | * Address 142 | * Download and upload rates 143 | 144 | There is a lot of data you can display on your `blocks`. Enable `oatbar-stats` 145 | like this: 146 | 147 | ```toml 148 | [[command]] 149 | name="stats" 150 | command="oatbar-stats" 151 | ``` 152 | 153 | Restart `oatbar` and examine the new variables. 154 | 155 | ```shell 156 | oatctl var ls | grep '^stats:' 157 | ``` 158 | 159 | The example output below. 160 | 161 | ```ini 162 | stats:cpu.full_text=cpu: 2% 163 | stats:cpu.percent=2 164 | stats:memory.free=8744980480 165 | stats:memory.full_text=mem: 73% 32.9 GB 166 | stats:memory.percent=73 167 | stats:memory.total=32915705856 168 | stats:memory.used=24170725376 169 | stats:net.igc0.full_text=igc0: 192.168.0.160 170 | stats:net.igc0.ipv4_0_addr=192.168.0.160 171 | stats:net.igc0.ipv4_0_broadcast=192.168.0.255 172 | stats:net.igc0.ipv4_0_run=true 173 | stats:net.igc0.ipv4_0_up=true 174 | stats:net.igc0.mac_0_addr=48:21:0b:35:ca:08 175 | stats:net.igc0.mac_0_run=true 176 | stats:net.igc0.mac_0_up=true 177 | stats:net.igc0.rx_per_sec=1704 178 | stats:net.igc0.tx_per_sec=1110 179 | ... 180 | ``` 181 | 182 | Entries with `full_text` are a good start to display directly, 183 | if you do not need more fine grained customizations. 184 | 185 | ```toml 186 | [[block]] 187 | name="ethernet" 188 | type="text" 189 | value="${stats:net.igc0.full_text}" 190 | ``` 191 | 192 | ### Disk space 193 | 194 | Example for `/home` directory partition: 195 | 196 | ```toml 197 | [[command]] 198 | name="home_free" 199 | command="df -h /home | tail -1 | awk '{print $5}'" 200 | interval=60 201 | 202 | [[block]] 203 | name='home_free' 204 | type = 'text' 205 | value = '/home ${home_free:value}' 206 | ``` 207 | 208 | ## Third-party sources 209 | 210 | Existing bar ecosystems already can provide large mount of useful information. 211 | `oatbar` by-design focuses on making it possible to adapt third-party data sources. 212 | 213 | ### `i3status` 214 | 215 | `i3status` is a great cross-platform source of information about the system. It supports: 216 | 217 | * CPU 218 | * Memory 219 | * Network 220 | * Battery 221 | * Volume 222 | 223 | `i3status` is designed to be used by `i3bar`, but `oatbar` supports this format natively. 224 | Enable it in `~/.i3status.conf` or in `~/.config/i3status/config`: 225 | 226 | ```conf 227 | general { 228 | output_format = "i3bar" 229 | } 230 | ``` 231 | 232 | Add you plugin as described in the [`i3status` documentation](https://i3wm.org/docs/i3status.html). 233 | Prefer simple output format, as you can format it on the `oatbar` side. Example: 234 | 235 | ``` 236 | order += cpu_usage 237 | 238 | cpu_usage { 239 | format = "%usage" 240 | } 241 | ``` 242 | 243 | If you run `i3status` you will now see 244 | 245 | ```json 246 | ❯ i3status 247 | {"version":1} 248 | [ 249 | [{"name":"cpu_usage","markup":"none","full_text":"00%"}] 250 | ,[{"name":"cpu_usage","markup":"none","full_text":"02%"}] 251 | ``` 252 | 253 | In `oatbar` config: 254 | 255 | ```toml 256 | [[block]] 257 | name='cpu' 258 | type = 'number' 259 | value = "${i3status:cpu_usage.full_text}" 260 | number_type = "percent" 261 | output_format="CPU:${value}" 262 | number_display="text" 263 | ``` 264 | 265 | If you prefer a progress bar: 266 | 267 | ```toml 268 | number_display="progress_bar" 269 | ``` 270 | 271 | ### `conky` 272 | 273 | As `i3status`, `conky` can also be a great source of system data. 274 | 275 | `conky` can print it's [variables](https://conky.sourceforge.net/variables.html) as plain text 276 | and `oatbar` can consume it as multi-line plain text. Example `~/.oatconkyrc`: 277 | 278 | ```lua 279 | conky.config = { 280 | out_to_console = true, 281 | out_to_x = false, 282 | update_interval = 1.0, 283 | } 284 | 285 | conky.text = [[ 286 | $memperc% 287 | $cpu% 288 | ]] 289 | ``` 290 | 291 | If you run `conky -c ~/.oatconkyrc` you will see repeating groups of numbers: 292 | 293 | ``` 294 | 2% 295 | 10% 296 | 5% 297 | 10% 298 | ``` 299 | 300 | In `oatbar` config: 301 | 302 | ```toml 303 | [[command]] 304 | name="conky" 305 | command="conky -c ~/.oatconkyrc" 306 | line_names=["mem","cpu"] 307 | 308 | [[block]] 309 | name='cpu' 310 | type = 'number' 311 | value = "${conky:cpu}" 312 | number_type = "percent" 313 | 314 | [block.number_display] 315 | type="text" 316 | output_format="CPU:${value}" 317 | 318 | [[block]] 319 | name='mem' 320 | type = 'number' 321 | value = "${conky:mem}" 322 | number_type = "percent" 323 | 324 | [block.number_display] 325 | type="text" 326 | output_format="MEM:${value}" 327 | ``` 328 | 329 | ### `i3blocks` 330 | 331 | `i3blocks` in a drop-in replacement for `i3status` to be used in 332 | `i3bar`. If you have existingi `i3blocks` configs, feel free to plug it 333 | directly into `oatbar`: 334 | 335 | ```toml 336 | [[command]] 337 | name="i3blocks" 338 | command="i3blocks" 339 | ``` 340 | 341 | You can check which `oatbar` variables it makes available by running 342 | `i3blocks` in your console. 343 | 344 | The indirection between the script, `i3blocks` and `oatbar` is not required. 345 | You can connect any plugin from the [`i3block-contrib`](https://github.com/vivien/i3blocks-contrib) 346 | excellent collection directly into `oatbar`. 347 | 348 | For example: 349 | 350 | ```console 351 | $ git clone https://github.com/vivien/i3blocks-contrib 352 | $ cd ./i3blocks-contrib/cpu_usage2 353 | $ make 354 | $ ./cpu_usage2 -l "cpu: " 355 | cpu: 39.79% 356 | cpu: 47.06% 357 | ``` 358 | 359 | As you can see, it inputs only one line of data each interval, 360 | so setting `line_names` is not necessary, however always check for it. 361 | 362 | ```toml 363 | [[command]] 364 | name="cpu_usage2" 365 | command="/path/to/cpu_usage2 -l 'cpu: '" 366 | 367 | [[block]] 368 | name="cpu_usage2" 369 | type="text" 370 | value="${cpu_usage2:value}" 371 | ``` 372 | 373 | ### HTTP APIs 374 | 375 | HTTP JSON APIs that do not require complicated login are extremely 376 | easy to integrate using `curl` and `jq`. 377 | 378 | Explore your JSON first 379 | 380 | ```console 381 | $ curl 'https://api.ipify.org?format=json' 382 | {"ip":"1.2.3.4"} 383 | ``` 384 | 385 | `jq -r` let's you extract a value from a JSON object. Add the command 386 | to `oatbar` config, but make sure to set a sufficient `interval` not to get 387 | banned. 388 | 389 | ```toml 390 | [[command]] 391 | name="ip" 392 | command="curl 'https://api.ipify.org?format=json | jq -r .ip" 393 | interval=1800 394 | 395 | [[block]] 396 | name="ip" 397 | type="text" 398 | value="my ip: ${ip:value}" 399 | ``` 400 | 401 | ### File 402 | 403 | You can use file watching utils to output file contents on any file change. 404 | For example for Linux you can use `fswatch`. 405 | 406 | ```toml 407 | [[command]] 408 | command="cat ./file; fswatch --event Updated ./file | xargs -I {} cat {}" 409 | ``` 410 | 411 | ### Socket 412 | 413 | Use `socat` to read from sockets. TCP socket: 414 | 415 | ```toml 416 | [[command]] 417 | command="socat TCP:localhost:7777 -" 418 | ``` 419 | 420 | SSL socket: 421 | 422 | ```toml 423 | [[command]] 424 | command="socat OPENSSL:localhost:7777 -" 425 | ``` 426 | 427 | For Unix socket: 428 | 429 | ```toml 430 | [[command]] 431 | command="socat UNIX-CONNECT:/path/to/socket -" 432 | ``` 433 | -------------------------------------------------------------------------------- /book/src/configuration/cookbook/empty-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/empty-space.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/icon.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/menu-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/menu-closed.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/menu-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/menu-open.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/no-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/no-space.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/panel_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/panel_0.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/panel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/panel_1.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/panel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/panel_2.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/pango.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/pango.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/partial-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/partial-bar.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/popup-edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/popup-edge.png -------------------------------------------------------------------------------- /book/src/configuration/cookbook/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/cookbook/separator.png -------------------------------------------------------------------------------- /book/src/configuration/img/separators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/configuration/img/separators.png -------------------------------------------------------------------------------- /book/src/configuration/variable.md: -------------------------------------------------------------------------------- 1 | # Varable 2 | 3 | 4 | 5 | Variables are at length described in the [Command](./command.md) section 6 | where they are produced and in the [Block](./block.md) section where they 7 | are consumed. 8 | 9 | If `oatbar` is running and you have added a few commands, you can see all available variables 10 | using `oatctl` command: 11 | 12 | ``` 13 | $ oatctl var ls 14 | clock:value=Mon Jul 8 00:40:19 15 | desktop:window_title.full_text=window: Alacritty 16 | desktop:window_title.value=Alacritty 17 | desktop:workspace.active=0 18 | desktop:workspace.full_text=workspace: 1 19 | desktop:workspace.value=1 20 | desktop:workspace.variants=1,2 21 | ... 22 | ``` 23 | 24 | More info on how to get and set variables programmatically: 25 | 26 | ``` 27 | $ oatctl var help 28 | ```` 29 | 30 | # Standalone variables 31 | 32 | You can declare your additional variables that do not come from commands. This is useful to 33 | pre-process data with [`replace` and `replace_first_match`](./block.md#common-properties) to be used in multiple blocks. 34 | 35 | ```toml 36 | [[var]] 37 | name="clock_color_attr" 38 | value = '${clock:color}' 39 | replace = [["(.+)","foreground='$1'"]] 40 | 41 | [[block]] 42 | value = "${clock:value}" 43 | ``` 44 | 45 | Standalone variables can use each other only in the order they are declared in the file, 46 | otherwise the result is undefined. 47 | 48 | ## Filters 49 | 50 | Filters are additional functions you can apply to values inside of the `${...}` expressions. Example: 51 | 52 | ```toml 53 | value = '${desktop:window_title.value|def:(no selected window)|max:100}' 54 | ``` 55 | 56 | Supported filters: 57 | 58 | * `def` sets the default value if the input variable is empty 59 | * `max` limits the length of the input. If it is larger, it is shortened with ellipsis (`...`) 60 | * `align` aligns the text to occupy fixed width if it is shorter than a certain length 61 | * First character is the filler 62 | * Second character is an alignment: `<`, `^` (center) or `>` 63 | * Min width 64 | * Example: 65 | * `hello` passed via `align:_>10` will be `_____hello` 66 | -------------------------------------------------------------------------------- /book/src/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Please install `cargo` via the package manager or [rustup.rs](http://rustup.rs). 4 | 5 | ### Dependencies 6 | 7 | #### ArchLinux 8 | 9 | ```sh 10 | pacman -Sy pango cairo libxcb pkgconf 11 | ``` 12 | 13 | #### Ubuntu/Debian 14 | 15 | ```sh 16 | apt-get install -y build-essential pkg-config \ 17 | libcairo2-dev libpango1.0-dev libx11-xcb-dev 18 | ``` 19 | 20 | #### Other 21 | 22 | Install the development packages for the following libraries 23 | 24 | * Cairo 25 | * Pango 26 | * x11-xcb 27 | 28 | ### Install 29 | 30 | ```sh 31 | cargo install oatbar 32 | ``` 33 | 34 | During the first launch the bar will create a default config at 35 | `~/.config/oatbar.toml` that should work on most machines. Run: 36 | 37 | ```sh 38 | oatbar 39 | ``` 40 | 41 | And you should see: 42 | 43 | ![New setup](new-setup.png) 44 | 45 | ### NetBSD 46 | 47 | On NetBSD, a package is available from the official repositories. 48 | To install it, simply run: 49 | 50 | ``` 51 | pkgin install oatbar 52 | ``` 53 | 54 | ### Next 55 | 56 | [Configuration](./configuration) 57 | -------------------------------------------------------------------------------- /book/src/links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | * [Crates.io](https://crates.io/crates/oatbar) 4 | * [Repository](https://github.com/igor-petruk/oatbar) 5 | -------------------------------------------------------------------------------- /book/src/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/main.png -------------------------------------------------------------------------------- /book/src/new-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/new-setup.png -------------------------------------------------------------------------------- /book/src/panel-sample-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/panel-sample-left.png -------------------------------------------------------------------------------- /book/src/panel-sample-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/book/src/panel-sample-right.png -------------------------------------------------------------------------------- /data/default_config.toml: -------------------------------------------------------------------------------- 1 | [[bar]] 2 | height=32 3 | blocks_left=[ "launcher", "workspace", "active_window"] 4 | blocks_right=["layout_enum", "sep", "disk_free", "sep", "clock"] 5 | background="#191919dd" 6 | 7 | [[var]] 8 | name="theme_color" 9 | value="#c7f2bb" 10 | #value="#facd5a" 11 | #value="#f9dbff" 12 | #value="#9cf7ff" 13 | 14 | [[default_block]] 15 | background="#191919dd" 16 | active_background="#333333dd" 17 | active_overline_color = "${theme_color}" 18 | active_foreground = "${theme_color}" 19 | active_output_format = '${value}' 20 | 21 | [[command]] 22 | name="disk_free" 23 | command="df -h / | tail -1 | awk '{print $5}'" 24 | interval=60 25 | 26 | [[command]] 27 | name="clock" 28 | command="date '+%a %b %e %H:%M:%S'" 29 | interval=1 30 | 31 | [[command]] 32 | name="desktop" 33 | command="oatbar-desktop" 34 | 35 | [[command]] 36 | name="keyboard" 37 | command="oatbar-keyboard" 38 | 39 | [[block]] 40 | name="launcher" 41 | type="text" 42 | value="Run" 43 | on_mouse_left="rofi -show drun" 44 | 45 | [[block]] 46 | name = 'workspace' 47 | type = 'enum' 48 | active = '${desktop:workspace.active}' 49 | variants = '${desktop:workspace.variants}' 50 | on_mouse_left = "oatbar-desktop $BLOCK_INDEX" 51 | 52 | [[block]] 53 | name = 'layout_enum' 54 | type = 'enum' 55 | active = '${keyboard:layout.active}' 56 | variants = '${keyboard:layout.variants}' 57 | on_mouse_left = "oatbar-keyboard layout set $BLOCK_INDEX" 58 | 59 | [[block]] 60 | name='disk_free' 61 | type = 'text' 62 | value = '/ ${disk_free:value}' 63 | 64 | [[block]] 65 | name = 'clock' 66 | type = 'text' 67 | value = '${clock:value}' 68 | 69 | [[block]] 70 | name='active_window' 71 | type = 'text' 72 | value = ':: ${desktop:window_title.value} ::' 73 | show_if_matches = [['${desktop:window_title.value}','.+']] 74 | pango_markup = false 75 | on_mouse_left="rofi -show window" 76 | 77 | [[block]] 78 | name='sep' 79 | type = 'text' 80 | value = '|' 81 | padding = 0.0 82 | foreground = "#777777" 83 | 84 | -------------------------------------------------------------------------------- /panel-sample-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/panel-sample-left.png -------------------------------------------------------------------------------- /panel-sample-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-petruk/oatbar/75bed5f34f4f5751a6f0a79c90da518ac5f11096/panel-sample-right.png -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use clap::{Parser, Subcommand, ValueEnum}; 3 | 4 | #[allow(unused)] 5 | mod ipc; 6 | 7 | #[derive(Parser)] 8 | #[command( 9 | author, version, 10 | about = "A cli tool to interact with oatbar.", 11 | long_about = None)] 12 | #[command(propagate_version = true)] 13 | struct Cli { 14 | #[command(subcommand)] 15 | command: Commands, 16 | /// Unique name of the oatbar server instance. 17 | #[arg(long, default_value = "oatbar")] 18 | instance_name: String, 19 | } 20 | 21 | #[derive(Clone, ValueEnum)] 22 | enum Direction { 23 | Right, 24 | Left, 25 | } 26 | 27 | #[derive(Subcommand)] 28 | enum VarSubcommand { 29 | /// Set a variable value. 30 | Set { 31 | /// Variable name. 32 | name: String, 33 | /// New variable value. 34 | value: String, 35 | }, 36 | /// Get a current variable value. 37 | Get { 38 | /// Variable name. 39 | name: String, 40 | }, 41 | /// Rotate a variable value through a list of values. 42 | /// 43 | /// If a current value is not in the list, the variable is set 44 | /// to the first value if direction is right, and to the last 45 | /// value if the direction is left. 46 | Rotate { 47 | /// Variable name. 48 | name: String, 49 | /// Rotation direction in the list. When going off-limits, appears from the other side. 50 | direction: Direction, 51 | /// List of values. 52 | values: Vec, 53 | }, 54 | /// List all variables and their values. Useful for troubleshooting. 55 | #[command(name = "ls")] 56 | List {}, 57 | } 58 | 59 | #[derive(Subcommand)] 60 | enum Commands { 61 | /// Interrupt waiting on all pending command `intervals`, 62 | /// forcing immediate restart. 63 | Poke, 64 | /// Work with oatbar variables. 65 | Var { 66 | #[clap(subcommand)] 67 | var: VarSubcommand, 68 | }, 69 | } 70 | 71 | fn var_rotate( 72 | client: &ipc::Client, 73 | name: String, 74 | direction: Direction, 75 | values: Vec, 76 | ) -> anyhow::Result { 77 | if values.is_empty() { 78 | return Err(anyhow::anyhow!("Values list must be not empty")); 79 | } 80 | let response = client.send_command(ipc::Command::GetVar { name: name.clone() })?; 81 | if let Some(error) = response.error { 82 | return Err(anyhow!("{}", error)); 83 | } 84 | let value = match response.data { 85 | Some(ipc::ResponseData::Value(value)) => value, 86 | x => return Err(anyhow!("Unexpected response: {:?}", x)), 87 | }; 88 | let position = values 89 | .iter() 90 | .enumerate() 91 | .find(|(_, v)| *v == &value) 92 | .map(|(idx, _)| idx); 93 | let last_idx = values.len() - 1; 94 | use Direction::*; 95 | let new_idx = match (direction, position) { 96 | (Left, None) => last_idx, 97 | (Left, Some(0)) => last_idx, 98 | (Left, Some(l)) => l - 1, 99 | (Right, None) => 0, 100 | (Right, Some(x)) if x == last_idx => 0, 101 | (Right, Some(l)) => l + 1, 102 | }; 103 | let new_value = values.get(new_idx).expect("new_idx should be within range"); 104 | client.send_command(ipc::Command::SetVar { 105 | name, 106 | value: new_value.clone(), 107 | }) 108 | } 109 | 110 | fn main() -> anyhow::Result<()> { 111 | let cli = Cli::parse(); 112 | let client = ipc::Client::new(&cli.instance_name)?; 113 | let response = match cli.command { 114 | Commands::Poke => client.send_command(ipc::Command::Poke), 115 | Commands::Var { var } => match var { 116 | VarSubcommand::Set { name, value } => { 117 | client.send_command(ipc::Command::SetVar { name, value }) 118 | } 119 | VarSubcommand::Get { name } => client.send_command(ipc::Command::GetVar { name }), 120 | VarSubcommand::Rotate { 121 | name, 122 | direction, 123 | values, 124 | } => var_rotate(&client, name, direction, values), 125 | VarSubcommand::List {} => client.send_command(ipc::Command::ListVars {}), 126 | }, 127 | }?; 128 | if let Some(error) = response.error { 129 | return Err(anyhow!("{}", error)); 130 | } 131 | if let Some(response_data) = response.data { 132 | match response_data { 133 | ipc::ResponseData::Value(value) => println!("{}", value), 134 | ipc::ResponseData::Vars(vars) => { 135 | for (k, v) in vars { 136 | println!("{}={}", k, v); 137 | } 138 | } 139 | } 140 | } 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /src/desktop.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod protocol; 16 | #[allow(unused)] 17 | mod xutils; 18 | 19 | use anyhow::{anyhow, Context}; 20 | use protocol::i3bar; 21 | use std::collections::BTreeMap; 22 | use xcb::x::{self, Atom, Window}; 23 | use xcb::Xid; 24 | use xutils::get_atom; 25 | 26 | use tracing::*; 27 | 28 | #[derive(Debug)] 29 | struct Workspaces { 30 | current: usize, 31 | names: Vec, 32 | } 33 | 34 | fn print_update(workspaces: &Workspaces, title: &str) -> anyhow::Result<()> { 35 | let workspace_value = workspaces 36 | .names 37 | .get(workspaces.current) 38 | .unwrap_or(&"?".to_string()) 39 | .to_string(); 40 | let mut other = BTreeMap::new(); 41 | other.insert("active".into(), workspaces.current.into()); 42 | other.insert("variants".into(), workspaces.names.join(",").into()); 43 | other.insert("value".into(), workspace_value.clone().into()); 44 | let mut title_other = BTreeMap::new(); 45 | title_other.insert("value".into(), title.into()); 46 | let blocks = vec![ 47 | i3bar::Block { 48 | full_text: format!("workspace: {}", workspace_value), 49 | name: Some("workspace".into()), 50 | instance: None, 51 | other, 52 | }, 53 | i3bar::Block { 54 | name: Some("window_title".into()), 55 | full_text: format!("window: {}", title), 56 | other: title_other, 57 | ..Default::default() 58 | }, 59 | ]; 60 | println!("{},", serde_json::to_string(&blocks)?); 61 | Ok(()) 62 | } 63 | 64 | fn get_workspaces( 65 | root: Window, 66 | conn: &xcb::Connection, 67 | current: &Atom, 68 | names: &Atom, 69 | ) -> anyhow::Result { 70 | let reply = xutils::get_property(conn, root, *current, x::ATOM_CARDINAL, 1)?; 71 | let current: u32 = *reply 72 | .value() 73 | .first() 74 | .ok_or_else(|| anyhow!("Empty reply"))?; 75 | let reply = xutils::get_property(conn, root, *names, x::ATOM_ANY, 1024)?; 76 | let buf: &[u8] = reply.value(); 77 | let bufs = buf.split(|f| *f == 0); 78 | let utf8 = bufs 79 | .map(|buf| String::from_utf8_lossy(buf).into_owned()) 80 | .filter(|s| !s.is_empty()); 81 | Ok(Workspaces { 82 | current: current as usize, 83 | names: utf8.collect(), 84 | }) 85 | } 86 | 87 | fn set_current_workspace( 88 | root: Window, 89 | conn: &xcb::Connection, 90 | current: &Atom, 91 | current_value: u32, 92 | ) -> anyhow::Result<()> { 93 | xutils::send( 94 | conn, 95 | &x::SendEvent { 96 | propagate: false, 97 | destination: x::SendEventDest::Window(root), 98 | event_mask: x::EventMask::all(), 99 | event: &x::ClientMessageEvent::new( 100 | root, 101 | *current, 102 | x::ClientMessageData::Data32([current_value, 0, 0, 0, 0]), 103 | ), 104 | }, 105 | )?; 106 | Ok(()) 107 | } 108 | 109 | fn get_active_window_title( 110 | conn: &xcb::Connection, 111 | root: Window, 112 | active_window: &Atom, 113 | window_name: &Atom, 114 | ) -> anyhow::Result { 115 | let reply = xutils::get_property(conn, root, *active_window, x::ATOM_WINDOW, 1) 116 | .context("Getting active window")?; 117 | let window: Option<&Window> = reply.value().first(); 118 | if window.is_none() { 119 | tracing::warn!( 120 | "Unable to get active window (maybe temporarily): {:?}", 121 | reply 122 | ); 123 | return Ok("".into()); 124 | } 125 | let window = *window.unwrap(); 126 | if window.resource_id() == 0 || window.resource_id() == u32::MAX { 127 | return Ok("".into()); 128 | } 129 | // TODO: fix a negligible memory leak monitoring all windows ever active. 130 | // There is a finite number of them possible. 131 | xutils::send( 132 | conn, 133 | &x::ChangeWindowAttributes { 134 | window, 135 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 136 | }, 137 | ) 138 | .context("Unable to monitor active window")?; 139 | let reply = xutils::get_property(conn, window, *window_name, x::ATOM_ANY, 1024) 140 | .context("Getting window title")?; 141 | let buf: &[u8] = reply.value(); 142 | let title = String::from_utf8_lossy(buf).into_owned(); 143 | Ok(title) 144 | } 145 | 146 | fn main() -> anyhow::Result<()> { 147 | let (conn, screen_num) = 148 | xcb::Connection::connect_with_xlib_display_and_extensions(&[], &[]).unwrap(); 149 | 150 | let screen = { 151 | let setup = conn.get_setup(); 152 | setup.roots().nth(screen_num as usize).unwrap() 153 | } 154 | .to_owned(); 155 | 156 | xutils::send( 157 | &conn, 158 | &x::ChangeWindowAttributes { 159 | window: screen.root(), 160 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 161 | }, 162 | ) 163 | .context("Unable to monitor root window")?; 164 | 165 | let current_desktop = get_atom(&conn, "_NET_CURRENT_DESKTOP")?; 166 | let desktop_names = get_atom(&conn, "_NET_DESKTOP_NAMES")?; 167 | let active_window = get_atom(&conn, "_NET_ACTIVE_WINDOW")?; 168 | let window_name = get_atom(&conn, "_NET_WM_NAME")?; 169 | 170 | let args: Vec = std::env::args().collect(); 171 | if let Some(workspace) = args.get(1) { 172 | let workspace = workspace.parse()?; 173 | set_current_workspace(screen.root(), &conn, ¤t_desktop, workspace)?; 174 | return Ok(()); 175 | } 176 | 177 | println!("{}", serde_json::to_string(&i3bar::Header::default())?); 178 | println!("["); 179 | 180 | let mut workspaces = get_workspaces(screen.root(), &conn, ¤t_desktop, &desktop_names)?; 181 | let mut title = get_active_window_title(&conn, screen.root(), &active_window, &window_name)?; 182 | print_update(&workspaces, &title)?; 183 | 184 | loop { 185 | let event = match conn.wait_for_event() { 186 | Err(xcb::Error::Connection(xcb::ConnError::Connection)) => { 187 | debug!( 188 | "Exiting event thread gracefully: {}", 189 | std::thread::current().name().unwrap_or("") 190 | ); 191 | return Ok(()); 192 | } 193 | Err(err) => { 194 | return Err(anyhow::anyhow!( 195 | "unexpected error: {:#?}, {}", 196 | err, 197 | err.to_string() 198 | )); 199 | } 200 | Ok(event) => event, 201 | }; 202 | match event { 203 | xcb::Event::X(x::Event::PropertyNotify(ev)) => { 204 | if ev.atom() == current_desktop || ev.atom() == desktop_names { 205 | workspaces = 206 | get_workspaces(screen.root(), &conn, ¤t_desktop, &desktop_names)?; 207 | print_update(&workspaces, &title)?; 208 | } 209 | if ev.atom() == active_window || ev.atom() == window_name { 210 | title = get_active_window_title( 211 | &conn, 212 | screen.root(), 213 | &active_window, 214 | &window_name, 215 | )?; 216 | print_update(&workspaces, &title)?; 217 | } 218 | } 219 | _ => { 220 | debug!("Unhandled XCB event: {:?}", event); 221 | } 222 | } 223 | conn.flush()?; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/drawing.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | #[cfg(feature = "image")] 6 | use std::{path::PathBuf, str::FromStr}; 7 | 8 | #[cfg(feature = "raster")] 9 | use anyhow::Context as AnyhowContext; 10 | use pangocairo::pango; 11 | #[cfg(feature = "svg")] 12 | use resvg::{tiny_skia, usvg}; 13 | use xcb::x; 14 | 15 | pub struct FontCache { 16 | cache: HashMap, 17 | } 18 | 19 | impl FontCache { 20 | pub fn new() -> Self { 21 | Self { 22 | cache: HashMap::new(), 23 | } 24 | } 25 | 26 | pub fn get(&mut self, font_str: &str) -> &pango::FontDescription { 27 | self.cache 28 | .entry(font_str.into()) 29 | .or_insert_with(|| pango::FontDescription::from_string(font_str)) 30 | } 31 | } 32 | 33 | #[cfg(feature = "image")] 34 | pub type Image = cairo::ImageSurface; 35 | 36 | #[derive(Eq, Hash, Clone, PartialEq, Debug)] 37 | #[cfg(feature = "image")] 38 | pub struct ImageKey { 39 | file_name: String, 40 | fit_to_height: u32, 41 | } 42 | 43 | #[derive(Clone)] 44 | #[cfg(feature = "image")] 45 | pub struct ImageLoader { 46 | cache: HashMap, 47 | } 48 | 49 | #[cfg(feature = "image")] 50 | impl ImageLoader { 51 | #[cfg(any(feature = "raster", feature = "svg"))] 52 | fn image_from_rgba8( 53 | buf: &mut [u8], 54 | width: i32, 55 | height: i32, 56 | ) -> anyhow::Result { 57 | let format = cairo::Format::ARgb32; 58 | let mut image = cairo::ImageSurface::create(format, width, height)?; 59 | // rgba => bgra (reverse argb) 60 | for rgba in buf.chunks_mut(4) { 61 | rgba.swap(0, 2); 62 | // Pre-multiplying. 63 | for i in 0..3 { 64 | rgba[i] = (rgba[i] as u16 * rgba[3] as u16 / 256) as u8; 65 | } 66 | } 67 | image.data()?.copy_from_slice(buf); 68 | Ok(image) 69 | } 70 | 71 | #[cfg(feature = "raster")] 72 | fn load_raster(file_name: &str, fit_to_height: f64) -> anyhow::Result { 73 | let img_buf = image::io::Reader::open(file_name)? 74 | .decode() 75 | .context("Unable to decode image")? 76 | .into_rgba8(); 77 | let mut scale = fit_to_height as f32 / img_buf.height() as f32; 78 | if scale > 1.0 { 79 | // Do not scale up. 80 | scale = 1.0; 81 | } 82 | let img_buf = if (scale - 1.0).abs() < 0.01 { 83 | img_buf 84 | } else { 85 | image::imageops::resize( 86 | &img_buf, 87 | (img_buf.width() as f32 * scale) as u32, 88 | (img_buf.height() as f32 * scale) as u32, 89 | image::imageops::FilterType::Triangle, 90 | ) 91 | }; 92 | let (w, h) = (img_buf.width(), img_buf.height()); 93 | Self::image_from_rgba8(&mut img_buf.into_raw(), w.try_into()?, h.try_into()?) 94 | } 95 | 96 | #[cfg(feature = "svg")] 97 | fn load_svg(file_name: &str, fit_to_height: f64) -> anyhow::Result { 98 | let tree = { 99 | let mut opt = usvg::Options { 100 | resources_dir: std::fs::canonicalize(file_name) 101 | .ok() 102 | .and_then(|p| p.parent().map(|p| p.to_path_buf())), 103 | ..Default::default() 104 | }; 105 | opt.fontdb_mut().load_system_fonts(); 106 | let svg_data = std::fs::read(file_name).unwrap(); 107 | usvg::Tree::from_data(&svg_data, &opt).unwrap() 108 | }; 109 | let size = tree.size().to_int_size(); // cannot be zero. 110 | let mut scale = fit_to_height as f32 / size.height() as f32; 111 | if scale > 1.0 { 112 | // Do not scale up. 113 | scale = 1.0; 114 | } 115 | let (w, h) = (size.width() as f32 * scale, size.height() as f32 * scale); 116 | let mut pixmap = tiny_skia::Pixmap::new(w as u32, h as u32).unwrap(); 117 | resvg::render( 118 | &tree, 119 | tiny_skia::Transform::from_scale(scale, scale), 120 | &mut pixmap.as_mut(), 121 | ); 122 | Self::image_from_rgba8(pixmap.data_mut(), w as i32, h as i32) 123 | } 124 | 125 | fn do_load_image(&self, file_name: &str, fit_to_height: f64) -> anyhow::Result { 126 | match PathBuf::from_str(file_name)?.extension() { 127 | #[cfg(feature = "svg")] 128 | Some(s) if s == "svg" => Self::load_svg(file_name, fit_to_height), 129 | #[cfg(feature = "raster")] 130 | _ => Self::load_raster(file_name, fit_to_height), 131 | #[cfg(not(feature = "raster"))] 132 | ext => Err(anyhow::anyhow!( 133 | "Image format support for {} is not enabled", 134 | ext.map(|s| s.to_string_lossy()) 135 | .unwrap_or("".into()) 136 | )), 137 | } 138 | } 139 | 140 | pub fn load_image( 141 | &mut self, 142 | file_name: &str, 143 | fit_to_height: f64, 144 | cache_images: bool, 145 | ) -> anyhow::Result { 146 | let key = ImageKey { 147 | file_name: file_name.into(), 148 | fit_to_height: fit_to_height as u32, 149 | }; 150 | if cache_images { 151 | if let Some(image) = self.cache.get(&key) { 152 | tracing::debug!("Got {:?} from cache", key); 153 | return Ok(image.clone()); 154 | } 155 | tracing::debug!("{:?} not in cache, loading...", key); 156 | let image = self.do_load_image(file_name, fit_to_height)?; 157 | self.cache.insert(key, image.clone()); 158 | Ok(image) 159 | } else { 160 | tracing::debug!("Cache disabled, loading {:?}...", key); 161 | self.do_load_image(file_name, fit_to_height) 162 | } 163 | } 164 | 165 | pub fn new() -> Self { 166 | Self { 167 | cache: HashMap::new(), 168 | } 169 | } 170 | } 171 | 172 | #[derive(PartialEq, Eq, Clone)] 173 | pub enum Mode { 174 | Full, 175 | Shape, 176 | } 177 | 178 | #[derive(Clone)] 179 | pub struct Context { 180 | pub buffer: x::Pixmap, 181 | pub buffer_surface: cairo::XCBSurface, 182 | pub context: cairo::Context, 183 | pub pango_context: Option, 184 | pub mode: Mode, 185 | pub font_cache: Arc>, 186 | #[cfg(feature = "image")] 187 | pub image_loader: ImageLoader, 188 | pub pointer_position: Option<(i16, i16)>, 189 | pub hover: bool, 190 | } 191 | 192 | pub struct Color { 193 | pub r: f64, 194 | pub g: f64, 195 | pub b: f64, 196 | pub a: f64, 197 | } 198 | 199 | impl Color { 200 | pub fn parse(color: &str) -> anyhow::Result { 201 | let (pango_color, alpha) = pango::Color::parse_with_alpha(color)?; 202 | let scale = 65536.0; 203 | Ok(Self { 204 | r: pango_color.red() as f64 / scale, 205 | g: pango_color.green() as f64 / scale, 206 | b: pango_color.blue() as f64 / scale, 207 | a: alpha as f64 / scale, 208 | }) 209 | } 210 | } 211 | 212 | impl Context { 213 | pub fn new( 214 | font_cache: Arc>, 215 | #[cfg(feature = "image")] image_loader: ImageLoader, 216 | buffer: x::Pixmap, 217 | buffer_surface: cairo::XCBSurface, 218 | mode: Mode, 219 | ) -> anyhow::Result { 220 | let context = cairo::Context::new(buffer_surface.clone())?; 221 | context.set_antialias(cairo::Antialias::Fast); 222 | context.set_line_join(cairo::LineJoin::Round); 223 | context.set_line_cap(cairo::LineCap::Square); 224 | let pango_context = match mode { 225 | Mode::Full => Some(pangocairo::functions::create_context(&context)), 226 | Mode::Shape => None, 227 | }; 228 | Ok(Self { 229 | font_cache, 230 | #[cfg(feature = "image")] 231 | image_loader, 232 | buffer, 233 | buffer_surface, 234 | context, 235 | pango_context, 236 | mode, 237 | pointer_position: None, 238 | hover: false, 239 | }) 240 | } 241 | 242 | pub fn set_source_color(&self, color: Color) { 243 | self.context 244 | .set_source_rgba(color.r, color.g, color.b, color.a); 245 | } 246 | 247 | pub fn set_source_rgba(&self, color: &str) -> anyhow::Result<()> { 248 | if color.is_empty() { 249 | return Ok(()); 250 | } 251 | match Color::parse(color) { 252 | Ok(color) => { 253 | self.set_source_color(color); 254 | Ok(()) 255 | } 256 | Err(e) => Err(anyhow::anyhow!( 257 | "failed to parse color: {:?}, err={:?}", 258 | color, 259 | e 260 | )), 261 | } 262 | } 263 | 264 | pub fn set_source_rgba_background(&self, color: &str) -> anyhow::Result<()> { 265 | if color.is_empty() { 266 | return Ok(()); 267 | } 268 | match Color::parse(color) { 269 | Ok(color) if self.mode == Mode::Shape => { 270 | self.set_source_color(Color { 271 | r: 0.0, 272 | g: 0.0, 273 | b: 0.0, 274 | a: if color.a == 0.0 { 0.0 } else { 1.0 }, 275 | }); 276 | Ok(()) 277 | } 278 | Ok(color) => { 279 | self.set_source_color(color); 280 | Ok(()) 281 | } 282 | Err(e) => Err(anyhow::anyhow!( 283 | "failed to parse color: {:?}, err={:?}", 284 | color, 285 | e 286 | )), 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::Context; 16 | use std::collections::HashMap; 17 | use std::sync::{Arc, RwLock}; 18 | use xcb::{x, xinput}; 19 | 20 | use crate::{bar, config, parse, state, thread, window, wmready, xutils}; 21 | 22 | pub struct Engine { 23 | windows: HashMap, 24 | window_ids: Vec, 25 | state: Arc>, 26 | conn: Arc, 27 | screen: x::ScreenBuf, 28 | pub update_tx: crossbeam_channel::Sender, 29 | update_rx: Option>, 30 | } 31 | 32 | impl Engine { 33 | pub fn new( 34 | config: config::Config, 35 | initial_state: state::State, 36 | ) -> anyhow::Result { 37 | let state = Arc::new(RwLock::new(initial_state)); 38 | let (update_tx, update_rx) = crossbeam_channel::unbounded(); 39 | 40 | let (conn, _) = xcb::Connection::connect_with_xlib_display_and_extensions( 41 | &[ 42 | xcb::Extension::Input, 43 | xcb::Extension::Shape, 44 | xcb::Extension::RandR, 45 | ], 46 | &[], 47 | ) 48 | .unwrap(); 49 | let conn = Arc::new(conn); 50 | 51 | let wm_info = wmready::wait().context("Unable to connect to WM")?; 52 | 53 | let screen = { 54 | let setup = conn.get_setup(); 55 | setup.roots().next().unwrap() 56 | } 57 | .to_owned(); 58 | 59 | tracing::info!( 60 | "XInput init: {:?}", 61 | xutils::query( 62 | &conn, 63 | &xinput::XiQueryVersion { 64 | major_version: 2, 65 | minor_version: 0, 66 | }, 67 | ) 68 | .context("init xinput 2.0 extension")? 69 | ); 70 | 71 | let mut windows = HashMap::new(); 72 | 73 | for (index, bar) in config.bar.iter().enumerate() { 74 | let window = window::Window::create_and_show( 75 | format!("bar{}", index), 76 | // index, 77 | &config, 78 | bar.clone(), 79 | conn.clone(), 80 | state.clone(), 81 | update_tx.clone(), 82 | &wm_info, 83 | )?; 84 | windows.insert(window.id, window); 85 | } 86 | 87 | let window_ids = windows.keys().cloned().collect(); 88 | 89 | Ok(Self { 90 | windows, 91 | window_ids, 92 | state, 93 | conn, 94 | screen, 95 | update_tx, 96 | update_rx: Some(update_rx), 97 | }) 98 | } 99 | 100 | pub fn spawn_state_update_thread( 101 | &self, 102 | state_update_rx: crossbeam_channel::Receiver, 103 | ) -> anyhow::Result<()> { 104 | let window_ids = self.window_ids.clone(); 105 | let conn = self.conn.clone(); 106 | let state = self.state.clone(); 107 | 108 | thread::spawn("eng-state", move || loop { 109 | while let Ok(state_update) = state_update_rx.recv() { 110 | { 111 | let mut state = state.write().unwrap(); 112 | state.handle_state_update(state_update); 113 | } 114 | for window in window_ids.iter() { 115 | xutils::send( 116 | &conn, 117 | &x::SendEvent { 118 | destination: x::SendEventDest::Window(*window), 119 | event_mask: x::EventMask::EXPOSURE, 120 | propagate: false, 121 | event: &x::ExposeEvent::new(*window, 0, 0, 1, 1, 1), 122 | }, 123 | )?; 124 | } 125 | } 126 | }) 127 | } 128 | 129 | fn handle_event(&mut self, event: &xcb::Event) -> anyhow::Result<()> { 130 | match event { 131 | xcb::Event::X(x::Event::Expose(event)) => { 132 | if let Some(window) = self.windows.get_mut(&event.window()) { 133 | // Hack for now to distinguish on-demand expose. 134 | if let Err(e) = window.render(event.width() != 1) { 135 | tracing::error!("Failed to render bar {:?}", e); 136 | } 137 | } 138 | } 139 | xcb::Event::Input(xinput::Event::RawMotion(_event)) => { 140 | let pointer = xutils::query( 141 | &self.conn, 142 | &x::QueryPointer { 143 | window: self.screen.root(), 144 | }, 145 | )?; 146 | for window in self.windows.values() { 147 | window.handle_raw_motion(pointer.root_x(), pointer.root_y())?; 148 | } 149 | } 150 | xcb::Event::X(x::Event::MotionNotify(event)) => { 151 | if let Some(window) = self.windows.get(&event.event()) { 152 | window.handle_motion(event.event_x(), event.event_y())?; 153 | } 154 | } 155 | xcb::Event::X(x::Event::LeaveNotify(event)) => { 156 | if let Some(window) = self.windows.get(&event.event()) { 157 | window.handle_motion_leave()?; 158 | } 159 | } 160 | xcb::Event::X(x::Event::ButtonPress(event)) => { 161 | for window in self.windows.values_mut() { 162 | if window.id == event.event() { 163 | tracing::trace!( 164 | "Button press: X={}, Y={}, button={}", 165 | event.event_x(), 166 | event.event_y(), 167 | event.detail() 168 | ); 169 | let button = match event.detail() { 170 | 1 => Some(bar::Button::Left), 171 | 2 => Some(bar::Button::Middle), 172 | 3 => Some(bar::Button::Right), 173 | 4 => Some(bar::Button::ScrollUp), 174 | 5 => Some(bar::Button::ScrollDown), 175 | _ => None, 176 | }; 177 | if let Some(button) = button { 178 | window.handle_button_press(event.event_x(), event.event_y(), button)?; 179 | } 180 | } 181 | } 182 | } 183 | _ => { 184 | tracing::debug!("Unhandled XCB event: {:?}", event); 185 | } 186 | } 187 | Ok(()) 188 | } 189 | 190 | pub fn run(&mut self) -> anyhow::Result<()> { 191 | match self.update_rx.take() { 192 | Some(update_rx) => { 193 | self.spawn_state_update_thread(update_rx) 194 | .context("engine state update")?; 195 | } 196 | None => { 197 | return Err(anyhow::anyhow!("run() can be run only once")); 198 | } 199 | } 200 | loop { 201 | let event = xutils::get_event(&self.conn).context("failed getting an X event")?; 202 | match event { 203 | Some(event) => { 204 | if let Err(e) = self.handle_event(&event) { 205 | tracing::error!("Failed handling event {:?}, error: {:?}", event, e); 206 | } 207 | } 208 | None => { 209 | return Ok(()); 210 | } 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/ipc.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::BTreeMap; 3 | use std::io::prelude::*; 4 | use std::os::unix::net::UnixStream; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 8 | #[serde(rename_all = "snake_case")] 9 | #[serde(tag = "type")] 10 | pub enum Command { 11 | Poke, 12 | SetVar { name: String, value: String }, 13 | GetVar { name: String }, 14 | ListVars {}, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 18 | #[serde(rename_all = "snake_case")] 19 | pub enum ResponseData { 20 | Value(String), 21 | Vars(BTreeMap), 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 25 | #[serde(rename_all = "snake_case")] 26 | pub struct Request { 27 | pub command: Command, 28 | } 29 | 30 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] 31 | #[serde(rename_all = "snake_case")] 32 | pub struct Response { 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub data: Option, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub error: Option, 37 | } 38 | 39 | pub fn socket_path(instance_name: &str) -> anyhow::Result { 40 | let mut path = dirs::runtime_dir() 41 | .or_else(dirs::state_dir) 42 | .unwrap_or_else(std::env::temp_dir); 43 | path.push(format!("oatbar/{}.sock", instance_name)); 44 | Ok(path) 45 | } 46 | 47 | pub struct Client { 48 | socket_path: PathBuf, 49 | } 50 | 51 | impl Client { 52 | pub fn new(instance_name: &str) -> anyhow::Result { 53 | Ok(Self { 54 | socket_path: socket_path(instance_name)?, 55 | }) 56 | } 57 | 58 | pub fn send_command(&self, command: Command) -> anyhow::Result { 59 | let mut stream = UnixStream::connect(&self.socket_path)?; 60 | let request = Request { command }; 61 | serde_json::to_writer(&mut stream, &request)?; 62 | stream.shutdown(std::net::Shutdown::Write); 63 | let mut vec = Vec::with_capacity(10 * 1024); 64 | stream.read_to_end(&mut vec)?; 65 | let response = serde_json::from_slice(&vec)?; 66 | Ok(response) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ipcserver.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::io::prelude::*; 3 | use std::os::unix::net::{UnixListener, UnixStream}; 4 | use std::sync::{Arc, RwLock}; 5 | 6 | use anyhow::Context; 7 | 8 | use crate::{ipc, source, state, thread}; 9 | 10 | #[derive(Clone)] 11 | pub struct Server { 12 | poker: source::Poker, 13 | state_update_tx: crossbeam_channel::Sender, 14 | vars: Arc>>, 15 | } 16 | 17 | impl Server { 18 | fn handle_poke(&self) -> anyhow::Result { 19 | self.poker.poke(); 20 | Ok(Default::default()) 21 | } 22 | 23 | fn sent_set_var(&self, name: String, value: String) -> anyhow::Result<()> { 24 | let (command_name, name): (Option, String) = match name.split_once(':') { 25 | Some((command_name, name)) => (Some(command_name.into()), name.into()), 26 | None => (None, name), 27 | }; 28 | self.state_update_tx 29 | .send(state::Update::VarUpdate(state::VarUpdate { 30 | command_name, 31 | entries: vec![state::UpdateEntry { 32 | var: name, 33 | value, 34 | ..Default::default() 35 | }], 36 | ..Default::default() 37 | }))?; 38 | Ok(()) 39 | } 40 | 41 | fn handle_set_var(&self, name: String, value: String) -> anyhow::Result { 42 | self.sent_set_var(name, value)?; 43 | Ok(Default::default()) 44 | } 45 | 46 | fn handle_get_var(&self, name: &str) -> anyhow::Result { 47 | let vars = self.vars.read().unwrap(); 48 | Ok(ipc::Response { 49 | data: Some(ipc::ResponseData::Value( 50 | vars.get(name).cloned().unwrap_or_default(), 51 | )), 52 | ..Default::default() 53 | }) 54 | } 55 | 56 | fn handle_list_vars(&self) -> anyhow::Result { 57 | let vars = self.vars.read().unwrap(); 58 | Ok(ipc::Response { 59 | data: Some(ipc::ResponseData::Vars(vars.clone())), 60 | ..Default::default() 61 | }) 62 | } 63 | 64 | fn handle_client(&self, mut stream: UnixStream) -> anyhow::Result<()> { 65 | let mut vec = Vec::with_capacity(10 * 1024); 66 | if stream.read_to_end(&mut vec).is_ok() { 67 | if vec.is_empty() { 68 | return Ok(()); 69 | } 70 | let request: ipc::Request = serde_json::from_slice(&vec)?; 71 | tracing::info!("IPC request {:?}", request); 72 | let response = match request.command { 73 | ipc::Command::Poke => self.handle_poke(), 74 | ipc::Command::SetVar { name, value } => self.handle_set_var(name, value), 75 | ipc::Command::GetVar { name } => self.handle_get_var(&name), 76 | ipc::Command::ListVars {} => self.handle_list_vars(), 77 | }?; 78 | serde_json::to_writer(stream, &response)?; 79 | } 80 | Ok(()) 81 | } 82 | 83 | pub fn spawn( 84 | instance_name: &str, 85 | poker: source::Poker, 86 | state_update_tx: crossbeam_channel::Sender, 87 | var_snapshot_updates_rx: crossbeam_channel::Receiver, 88 | ) -> anyhow::Result<()> { 89 | let path = ipc::socket_path(instance_name).context("Unable to get socket path")?; 90 | tracing::info!("IPC socket path: {:?}", path); 91 | if UnixStream::connect(path.clone()).is_ok() { 92 | return Err(anyhow::anyhow!( 93 | "Unable to start oatbar, IPC socket {:?} is in use, probably another oatbar is running.", 94 | path)); 95 | } 96 | 97 | if let Some(parent) = path.parent() { 98 | let _ = std::fs::create_dir_all(parent); 99 | } 100 | let _ = std::fs::remove_file(&path); 101 | let socket = UnixListener::bind(&path).context("Unable to bind")?; 102 | let server = Server { 103 | poker, 104 | state_update_tx, 105 | vars: Default::default(), 106 | }; 107 | let vars = server.vars.clone(); 108 | thread::spawn("ipc", move || { 109 | for stream in socket.incoming() { 110 | let server = server.clone(); 111 | thread::spawn("ipc-client", move || server.handle_client(stream?))?; 112 | } 113 | Ok(()) 114 | })?; 115 | thread::spawn("ipc-vars", move || { 116 | while let Ok(var_snapshot_update) = var_snapshot_updates_rx.recv() { 117 | let mut vars = vars.write().unwrap(); 118 | for (name, new_value) in var_snapshot_update.vars { 119 | vars.insert(name, new_value); 120 | } 121 | } 122 | Ok(()) 123 | })?; 124 | 125 | Ok(()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/keyboard.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod protocol; 16 | #[allow(unused)] 17 | mod xutils; 18 | 19 | use anyhow::anyhow; 20 | use clap::{Parser, Subcommand}; 21 | use protocol::i3bar; 22 | use std::collections::BTreeMap; 23 | use tracing::*; 24 | use xcb::{ 25 | x, 26 | xkb::{self, StatePart}, 27 | }; 28 | 29 | #[derive(Debug)] 30 | struct KeyboardState { 31 | current: usize, 32 | variants: Vec, 33 | indicators: BTreeMap, 34 | } 35 | 36 | fn to_indicator_name(s: &str) -> String { 37 | let mut result = String::with_capacity(s.len() * 2); 38 | for (i, ch) in s.char_indices() { 39 | if ch.is_whitespace() { 40 | continue; 41 | } 42 | if ch.is_uppercase() { 43 | if i > 0 { 44 | result.push('_'); 45 | } 46 | result.push(ch.to_ascii_lowercase()); 47 | } else { 48 | result.push(ch); 49 | } 50 | } 51 | result 52 | } 53 | 54 | fn get_current_state(conn: &xcb::Connection, group: xkb::Group) -> anyhow::Result { 55 | let reply = xutils::query( 56 | conn, 57 | &xkb::GetNames { 58 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 59 | which: xkb::NameDetail::SYMBOLS, 60 | }, 61 | )?; 62 | let one_value = reply 63 | .value_list() 64 | .first() 65 | .cloned() 66 | .ok_or_else(|| anyhow::anyhow!("More than one value"))?; 67 | let atom_name = if let xkb::GetNamesReplyValueList::Symbols(atom) = one_value { 68 | let reply = xutils::query(conn, &x::GetAtomName { atom })?; 69 | Ok(reply.name().to_utf8().to_string()) 70 | } else { 71 | Err(anyhow::anyhow!("Unexpected reply type")) 72 | }?; 73 | 74 | let variants: Vec = atom_name 75 | .split('+') 76 | .filter(|s| !s.contains('(')) 77 | .map(|s| s.split(':').next().unwrap()) 78 | .filter(|s| s != &"pc") 79 | .map(String::from) 80 | .collect(); 81 | 82 | let layout_index = match group { 83 | xkb::Group::N1 => 0, 84 | xkb::Group::N2 => 1, 85 | xkb::Group::N3 => 2, 86 | xkb::Group::N4 => 3, 87 | }; 88 | 89 | let indicator_atoms = get_indicator_atoms(conn)?; 90 | let mut indicators = BTreeMap::new(); 91 | for atom in indicator_atoms { 92 | let reply = xutils::query(conn, &x::GetAtomName { atom })?; 93 | let name = to_indicator_name(&reply.name().to_utf8()); 94 | let reply = xutils::query( 95 | conn, 96 | &xkb::GetNamedIndicator { 97 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 98 | indicator: atom, 99 | led_class: xkb::LedClass::KbdFeedbackClass, 100 | led_id: 0, 101 | }, 102 | )?; 103 | indicators.insert(name, reply.on()); 104 | } 105 | 106 | debug!( 107 | "atom={},layout_index={},indicators={:?}", 108 | atom_name, layout_index, indicators 109 | ); 110 | 111 | Ok(KeyboardState { 112 | current: layout_index, 113 | variants, 114 | indicators, 115 | }) 116 | } 117 | 118 | fn get_indicator_atoms(conn: &xcb::Connection) -> anyhow::Result> { 119 | let reply = xutils::query( 120 | conn, 121 | &xkb::GetNames { 122 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 123 | which: xkb::NameDetail::INDICATOR_NAMES, 124 | }, 125 | )?; 126 | let one_value = reply 127 | .value_list() 128 | .first() 129 | .cloned() 130 | .ok_or_else(|| anyhow::anyhow!("More than one value"))?; 131 | if let xkb::GetNamesReplyValueList::IndicatorNames(atoms) = one_value { 132 | Ok(atoms) 133 | } else { 134 | Err(anyhow::anyhow!("Unexpected reply type")) 135 | } 136 | } 137 | 138 | fn state_to_blocks(state: KeyboardState) -> Vec { 139 | let mut result = Vec::with_capacity(state.indicators.len() + 1); 140 | 141 | let value = state 142 | .variants 143 | .get(state.current) 144 | .unwrap_or(&"?".to_string()) 145 | .to_string(); 146 | let mut other = BTreeMap::new(); 147 | other.insert("active".into(), state.current.into()); 148 | other.insert("variants".into(), state.variants.join(",").into()); 149 | other.insert("value".into(), value.clone().into()); 150 | result.push(i3bar::Block { 151 | name: Some("layout".into()), 152 | full_text: format!("layout: {}", value), 153 | instance: None, 154 | other, 155 | }); 156 | 157 | let indicator_blocks: Vec<_> = state 158 | .indicators 159 | .into_iter() 160 | .map(|(k, v)| { 161 | let value = if v { "on" } else { "off" }; 162 | let mut other = BTreeMap::new(); 163 | other.insert("value".into(), value.into()); 164 | i3bar::Block { 165 | name: Some("indicator".into()), 166 | full_text: format!("{}:{:>3}", k, value), 167 | instance: Some(k), 168 | other, 169 | } 170 | }) 171 | .collect(); 172 | result.extend_from_slice(&indicator_blocks); 173 | 174 | result 175 | } 176 | 177 | #[derive(Parser)] 178 | #[command( 179 | author, version, 180 | about = "Keyboard util for oatbar", 181 | long_about = None)] 182 | #[command(propagate_version = true)] 183 | struct Cli { 184 | #[command(subcommand)] 185 | command: Option, 186 | } 187 | 188 | #[derive(Subcommand)] 189 | enum LayoutSubcommand { 190 | /// Set a keyboard layout. 191 | Set { 192 | /// Layout index as returned by oatbar-keyboard stream. 193 | layout: usize, 194 | }, 195 | } 196 | 197 | #[derive(Subcommand)] 198 | enum Commands { 199 | /// Work with keyboard layouts. 200 | Layout { 201 | #[clap(subcommand)] 202 | layout_cmd: LayoutSubcommand, 203 | }, 204 | } 205 | 206 | fn handle_set_layout(conn: &xcb::Connection, layout: usize) -> anyhow::Result<()> { 207 | let group = match layout { 208 | 0 => xkb::Group::N1, 209 | 1 => xkb::Group::N2, 210 | 2 => xkb::Group::N3, 211 | _ => xkb::Group::N4, 212 | }; 213 | xutils::send( 214 | conn, 215 | &xkb::LatchLockState { 216 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 217 | group_lock: group, 218 | lock_group: true, 219 | latch_group: false, 220 | group_latch: 0, 221 | affect_mod_locks: x::ModMask::empty(), 222 | affect_mod_latches: x::ModMask::empty(), 223 | mod_locks: x::ModMask::empty(), 224 | }, 225 | )?; 226 | Ok(()) 227 | } 228 | 229 | fn main() -> anyhow::Result<()> { 230 | let cli = Cli::parse(); 231 | let (conn, _) = 232 | xcb::Connection::connect_with_xlib_display_and_extensions(&[xcb::Extension::Xkb], &[])?; 233 | 234 | let xkb_ver = xutils::query( 235 | &conn, 236 | &xkb::UseExtension { 237 | wanted_major: 1, 238 | wanted_minor: 0, 239 | }, 240 | )?; 241 | 242 | if !xkb_ver.supported() { 243 | return Err(anyhow!("xkb-1.0 is not supported")); 244 | } 245 | 246 | if let Some(command) = cli.command { 247 | match command { 248 | Commands::Layout { layout_cmd } => match layout_cmd { 249 | LayoutSubcommand::Set { layout } => handle_set_layout(&conn, layout)?, 250 | }, 251 | } 252 | return Ok(()); 253 | } 254 | 255 | let events = xkb::EventType::NEW_KEYBOARD_NOTIFY | xkb::EventType::STATE_NOTIFY; 256 | xutils::send( 257 | &conn, 258 | &xkb::SelectEvents { 259 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 260 | affect_which: events, 261 | clear: xkb::EventType::empty(), 262 | select_all: events, 263 | affect_map: xkb::MapPart::empty(), 264 | map: xkb::MapPart::empty(), 265 | details: &[], 266 | }, 267 | )?; 268 | 269 | let reply = xutils::query( 270 | &conn, 271 | &xkb::GetState { 272 | device_spec: xkb::Id::UseCoreKbd as xkb::DeviceSpec, 273 | }, 274 | )?; 275 | 276 | println!("{}", serde_json::to_string(&i3bar::Header::default())?); 277 | println!("["); 278 | 279 | let state = get_current_state(&conn, reply.group())?; 280 | debug!("Initial: {:?}", state); 281 | println!("{},", serde_json::to_string(&state_to_blocks(state))?); 282 | 283 | loop { 284 | let event = xutils::get_event(&conn)?; 285 | match event { 286 | Some(xcb::Event::Xkb(xkb::Event::StateNotify(n))) => { 287 | if n.changed().contains(StatePart::GROUP_STATE) 288 | || n.changed().contains(StatePart::MODIFIER_LOCK) 289 | { 290 | let state = get_current_state(&conn, n.group())?; 291 | debug!("State updated: {:?}", state); 292 | println!("{},", serde_json::to_string(&state_to_blocks(state))?); 293 | } 294 | } 295 | None => return Ok(()), 296 | _ => { 297 | debug!("Unhandled XCB event: {:?}", event); 298 | } 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[macro_use] 16 | extern crate macro_rules_attribute; 17 | 18 | mod bar; 19 | // #[allow(unused)] 20 | mod config; 21 | mod drawing; 22 | mod engine; 23 | #[allow(unused)] 24 | mod ipc; 25 | mod ipcserver; 26 | #[allow(unused_macros)] 27 | mod parse; 28 | mod process; 29 | mod protocol; 30 | mod source; 31 | mod state; 32 | mod thread; 33 | mod timer; 34 | mod window; 35 | mod wmready; 36 | mod xrandr; 37 | mod xutils; 38 | 39 | use clap::Parser; 40 | 41 | #[derive(Parser)] 42 | #[command( 43 | author, version, 44 | about = "Oatbar window manager bar", 45 | long_about = None)] 46 | #[command(propagate_version = true)] 47 | struct Cli { 48 | /// Unique name of the oatbar server instance. 49 | #[arg(long, default_value = "oatbar")] 50 | instance_name: String, 51 | } 52 | 53 | fn main() -> anyhow::Result<()> { 54 | let cli = Cli::parse(); 55 | 56 | #[cfg(feature = "profile")] 57 | let guard = pprof::ProfilerGuardBuilder::default() 58 | .frequency(100) 59 | .blocklist(&["libc", "libgcc", "pthread", "vdso"]) 60 | .build() 61 | .unwrap(); 62 | 63 | let sub = tracing_subscriber::fmt().compact().with_thread_names(true); 64 | 65 | #[cfg(debug_assertions)] 66 | let sub = sub.with_max_level(tracing::Level::TRACE); 67 | 68 | sub.init(); 69 | 70 | let config = config::load()?; 71 | let commands = config.commands.clone(); 72 | 73 | let (ipc_server_tx, ipc_server_rx) = crossbeam_channel::unbounded(); 74 | 75 | let state: state::State = state::State::new(config.clone(), vec![ipc_server_tx]); 76 | 77 | let mut engine = engine::Engine::new(config, state)?; 78 | 79 | let mut poker = source::Poker::new(); 80 | for (index, config) in commands.into_iter().enumerate() { 81 | let command = source::Command { index, config }; 82 | command.spawn(engine.update_tx.clone(), poker.add())?; 83 | } 84 | 85 | ipcserver::Server::spawn( 86 | &cli.instance_name, 87 | poker, 88 | engine.update_tx.clone(), 89 | ipc_server_rx, 90 | )?; 91 | 92 | #[cfg(feature = "profile")] 93 | std::thread::spawn(move || loop { 94 | if let Ok(report) = guard.report().build() { 95 | let file = std::fs::File::create("flamegraph.svg").unwrap(); 96 | report.flamegraph(file).unwrap(); 97 | }; 98 | std::thread::sleep(std::time::Duration::from_secs(5)); 99 | }); 100 | 101 | engine.run()?; 102 | Ok(()) 103 | } 104 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryFrom; 3 | use std::fmt::Debug; 4 | use std::ops::Deref; 5 | use std::sync::Arc; 6 | 7 | use anyhow::Context; 8 | use serde::Deserialize; 9 | 10 | pub trait PlaceholderContext { 11 | fn get(&self, key: &str) -> Option<&String>; 12 | } 13 | 14 | impl PlaceholderContext for HashMap { 15 | fn get(&self, key: &str) -> Option<&String> { 16 | self.get(key) 17 | } 18 | } 19 | 20 | #[derive(Debug, Default, Clone, PartialEq, Deserialize)] 21 | #[serde(try_from = "String")] 22 | pub struct Placeholder { 23 | tokens: Arc>, 24 | pub value: String, 25 | } 26 | 27 | impl Placeholder { 28 | pub fn new(expr: &str) -> anyhow::Result { 29 | let tokens = 30 | parse_expr(expr).with_context(|| format!("Failed to parse expression: {:?}", expr))?; 31 | Ok(Self { 32 | tokens: Arc::new(tokens), 33 | value: "".into(), 34 | }) 35 | } 36 | 37 | pub fn infallable(value: &str) -> Self { 38 | Self::new(value).unwrap() 39 | } 40 | 41 | pub fn update(&mut self, vars: &dyn PlaceholderContext) -> anyhow::Result { 42 | let new_value = self.resolve(vars)?; 43 | let updated = new_value != self.value; 44 | if updated { 45 | self.value = new_value; 46 | } 47 | Ok(updated) 48 | } 49 | 50 | pub fn is_empty(&self) -> bool { 51 | self.tokens.is_empty() 52 | } 53 | } 54 | 55 | impl Deref for Placeholder { 56 | type Target = str; 57 | 58 | fn deref(&self) -> &Self::Target { 59 | &self.value 60 | } 61 | } 62 | 63 | impl TryFrom for Placeholder { 64 | type Error = anyhow::Error; 65 | fn try_from(value: String) -> Result { 66 | Self::new(&value) 67 | } 68 | } 69 | 70 | pub trait PlaceholderExt { 71 | type R; 72 | 73 | fn resolve(&self, vars: &dyn PlaceholderContext) -> anyhow::Result; 74 | } 75 | 76 | impl PlaceholderExt for Placeholder { 77 | type R = String; 78 | fn resolve(&self, vars: &dyn PlaceholderContext) -> anyhow::Result { 79 | Ok(self 80 | .tokens 81 | .iter() 82 | .map(|token| match token { 83 | Token::String(s) => Ok(s.clone()), 84 | Token::Var(v) => v 85 | .resolve(vars) 86 | .with_context(|| format!("Cannot resolve variable: {:?}", v.name)), 87 | }) 88 | .collect::>>()? 89 | .join("")) 90 | } 91 | } 92 | 93 | #[derive(Debug, Clone, PartialEq)] 94 | pub enum AlignDirection { 95 | Left, 96 | Center, 97 | Right, 98 | } 99 | 100 | #[derive(Debug, Clone, PartialEq)] 101 | pub struct Align { 102 | direction: AlignDirection, 103 | space: Option, 104 | width: usize, 105 | } 106 | 107 | impl Align { 108 | fn parse(expression: &str) -> anyhow::Result { 109 | if let Some((space, width)) = expression.rsplit_once('<') { 110 | Ok(Self { 111 | direction: AlignDirection::Left, 112 | space: space.chars().next(), 113 | width: width.parse()?, 114 | }) 115 | } else if let Some((space, width)) = expression.rsplit_once('>') { 116 | Ok(Self { 117 | direction: AlignDirection::Right, 118 | space: space.chars().next(), 119 | width: width.parse()?, 120 | }) 121 | } else if let Some((space, width)) = expression.rsplit_once('^') { 122 | Ok(Self { 123 | direction: AlignDirection::Center, 124 | space: space.chars().next(), 125 | width: width.parse()?, 126 | }) 127 | } else { 128 | Err(anyhow::anyhow!( 129 | "Incorrect format of format expression: {:?}", 130 | expression 131 | )) 132 | } 133 | } 134 | 135 | fn apply(&self, input: &str) -> anyhow::Result { 136 | let len = input.chars().count(); 137 | let pad_left = match self.direction { 138 | AlignDirection::Right => self.width.checked_sub(len).unwrap_or_default(), 139 | AlignDirection::Left => 0, 140 | AlignDirection::Center => self.width.checked_sub(len).unwrap() / 2, 141 | }; 142 | let pad_right = match self.direction { 143 | AlignDirection::Right => 0, 144 | AlignDirection::Left => self.width.checked_sub(len).unwrap_or_default(), 145 | AlignDirection::Center => self.width.checked_sub(len).unwrap() / 2, 146 | }; 147 | let pad_right_extra = match self.direction { 148 | AlignDirection::Center => self.width.checked_sub(len).unwrap() % 2, 149 | _ => 0, 150 | }; 151 | let space = self.space.unwrap_or(' '); 152 | let mut result = String::with_capacity(self.width * 2); 153 | for _ in 0..pad_left { 154 | result.push(space); 155 | } 156 | result.push_str(input); 157 | for _ in 0..(pad_right + pad_right_extra) { 158 | result.push(space); 159 | } 160 | Ok(result) 161 | } 162 | } 163 | 164 | #[derive(Debug, Clone, PartialEq)] 165 | enum Filter { 166 | DefaultValue(String), 167 | Max(usize), 168 | Align(Align), 169 | } 170 | 171 | impl Filter { 172 | fn parse(expression: &str) -> anyhow::Result { 173 | match expression.trim_start().split_once(':') { 174 | Some(("def", v)) => Ok(Filter::DefaultValue(v.to_string())), 175 | Some(("align", v)) => Ok(Filter::Align(Align::parse(v)?)), 176 | Some(("max", v)) => Ok(Filter::Max(v.parse()?)), 177 | Some((name, _)) => Err(anyhow::anyhow!("Unknown filter: {:?}", name)), 178 | None => Err(anyhow::anyhow!( 179 | "Filter format must be filter:args..., found: {:?}", 180 | expression 181 | )), 182 | } 183 | } 184 | 185 | fn apply(&self, input: &str) -> anyhow::Result { 186 | Ok(match self { 187 | Self::DefaultValue(v) => { 188 | if input.trim().is_empty() { 189 | v.clone() 190 | } else { 191 | input.to_string() 192 | } 193 | } 194 | Self::Max(max_length) => { 195 | if input.chars().count() > *max_length { 196 | let mut result = String::with_capacity(max_length * 2); 197 | let ellipsis = "..."; 198 | let truncate_len = std::cmp::max(max_length - ellipsis.len(), 0); 199 | for ch in input.chars().take(truncate_len) { 200 | result.push(ch); 201 | } 202 | result.push_str(ellipsis); 203 | result 204 | } else { 205 | input.to_string() 206 | } 207 | } 208 | Self::Align(align) => align.apply(input)?, 209 | }) 210 | } 211 | } 212 | 213 | #[derive(Debug, Default, Clone, PartialEq)] 214 | pub struct VarToken { 215 | pub name: String, 216 | filters: Vec, 217 | } 218 | 219 | impl VarToken { 220 | fn parse(expression: &str) -> anyhow::Result { 221 | let mut split = expression.split('|'); 222 | let var = split.next().unwrap().trim(); 223 | let filters = split 224 | .map(Filter::parse) 225 | .collect::>>()?; 226 | Ok(VarToken { 227 | name: var.to_string(), 228 | filters, 229 | }) 230 | } 231 | 232 | pub fn resolve(&self, vars: &dyn PlaceholderContext) -> anyhow::Result { 233 | let mut value = vars.get(&self.name).cloned().unwrap_or_default(); 234 | for filter in self.filters.iter() { 235 | value = filter.apply(&value)?; 236 | } 237 | Ok(value) 238 | } 239 | } 240 | 241 | #[derive(Debug, Clone, PartialEq)] 242 | pub enum Token { 243 | String(String), 244 | Var(VarToken), 245 | } 246 | 247 | pub fn parse_expr(expression: &str) -> anyhow::Result> { 248 | let mut result = Vec::::with_capacity(5); 249 | let mut char_iter = expression.chars(); 250 | let mut string_buf = String::with_capacity(255); 251 | while let Some(char) = char_iter.next() { 252 | match char { 253 | '$' => match char_iter.next() { 254 | Some('{') => { 255 | let mut var = Vec::::with_capacity(255); 256 | loop { 257 | match char_iter.next() { 258 | Some('}') => { 259 | if !string_buf.is_empty() { 260 | result.push(Token::String(string_buf.clone())); 261 | string_buf.clear(); 262 | } 263 | 264 | let var: String = var.into_iter().collect(); 265 | let var_token = VarToken::parse(&var)?; 266 | result.push(Token::Var(var_token)); 267 | break; 268 | } 269 | Some(other) => { 270 | var.push(other); 271 | } 272 | None => return Err(anyhow::anyhow!("Non-closed placeholder")), 273 | } 274 | } 275 | } 276 | Some(other) => { 277 | string_buf.push('$'); 278 | string_buf.push(other); 279 | } 280 | None => { 281 | return Err(anyhow::anyhow!("Unescaped $ at the end of the string")); 282 | } 283 | }, 284 | char => string_buf.push(char), 285 | } 286 | } 287 | if !string_buf.is_empty() { 288 | result.push(Token::String(string_buf.clone())); 289 | string_buf.clear(); 290 | } 291 | Ok(result.into_iter().collect()) 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | use super::*; 297 | use std::collections::HashMap; 298 | 299 | #[test] 300 | fn test_max() { 301 | let mut map = HashMap::new(); 302 | map.insert("a".into(), "hello world".into()); 303 | assert_eq!( 304 | "( hello world )", 305 | Placeholder::new("( ${a|max:20} )") 306 | .unwrap() 307 | .resolve(&map) 308 | .unwrap(), 309 | ); 310 | assert_eq!( 311 | "( hello w... )", 312 | Placeholder::new("( ${a|max:10} )") 313 | .unwrap() 314 | .resolve(&map) 315 | .unwrap(), 316 | ); 317 | } 318 | 319 | #[test] 320 | fn test_align() { 321 | let mut map = HashMap::new(); 322 | map.insert("a".into(), "hello".into()); 323 | assert_eq!( 324 | "( hello )", 325 | Placeholder::new("( ${a|align:-<5} )") 326 | .unwrap() 327 | .resolve(&map) 328 | .unwrap(), 329 | ); 330 | assert_eq!( 331 | "( -----hello )", 332 | Placeholder::new("( ${a|align:->10} )") 333 | .unwrap() 334 | .resolve(&map) 335 | .unwrap(), 336 | ); 337 | assert_eq!( 338 | "( hello----- )", 339 | Placeholder::new("( ${a|align:-<10} )") 340 | .unwrap() 341 | .resolve(&map) 342 | .unwrap(), 343 | ); 344 | assert_eq!( 345 | "( --hello-- )", 346 | Placeholder::new("( ${a|align:-^9} )") 347 | .unwrap() 348 | .resolve(&map) 349 | .unwrap(), 350 | ); 351 | assert_eq!( 352 | "( --hello--- )", 353 | Placeholder::new("( ${a|align:-^10} )") 354 | .unwrap() 355 | .resolve(&map) 356 | .unwrap(), 357 | ); 358 | } 359 | 360 | #[test] 361 | fn test_value() { 362 | let mut map = HashMap::new(); 363 | map.insert("foo".into(), "hello".into()); 364 | map.insert("bar".into(), "world".into()); 365 | map.insert("baz".into(), "unuzed".into()); 366 | let value = " ${foo} $$ ${bar}, (${not_found}) ${not_found|def:default} "; 367 | let result = Placeholder::new(&value).unwrap().resolve(&map); 368 | assert!(result.is_ok()); 369 | assert_eq!(result.unwrap(), " hello $$ world, () default "); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | pub fn run_detached(command: &str, envs: Vec<(String, String)>) -> anyhow::Result<()> { 2 | match fork::fork() { 3 | Err(e) => { 4 | tracing::error!("Failed to spawn {:?}: {:?}", command, e); 5 | } 6 | Ok(fork::Fork::Parent(mut pid)) => unsafe { 7 | libc::wait(&mut pid); 8 | }, 9 | Ok(fork::Fork::Child) => { 10 | if let Err(e) = fork::setsid() { 11 | tracing::error!("Failed to setsid() {:?}: {:?}", command, e); 12 | std::process::exit(1); 13 | } 14 | match fork::fork() { 15 | Err(e) => { 16 | tracing::error!("Failed to spawn {:?}: {:?}", command, e); 17 | std::process::exit(1); 18 | } 19 | Ok(fork::Fork::Parent(_)) => { 20 | std::process::exit(0); 21 | } 22 | Ok(fork::Fork::Child) => { 23 | use std::os::unix::process::CommandExt; 24 | let _ = std::process::Command::new("sh") 25 | .arg("-c") 26 | .arg(command) 27 | .envs(envs) 28 | .exec(); 29 | } 30 | } 31 | } 32 | } 33 | tracing::info!("{:?} spawned", command); 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | pub mod i3bar { 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::BTreeMap; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct Header { 7 | pub version: i32, 8 | } 9 | 10 | impl Default for Header { 11 | fn default() -> Self { 12 | Self { version: 1 } 13 | } 14 | } 15 | 16 | #[derive(Clone, Default, Debug, Serialize, Deserialize)] 17 | pub struct Block { 18 | pub full_text: String, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub name: Option, 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub instance: Option, 23 | #[serde(flatten)] 24 | pub other: BTreeMap, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/source.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::protocol::i3bar; 16 | use anyhow::Context; 17 | use crossbeam_channel::select; 18 | use serde::de::*; 19 | use serde::Deserialize; 20 | use std::fmt; 21 | use std::io::{BufRead, BufReader}; 22 | use std::time::Duration; 23 | 24 | use crate::{state, thread}; 25 | 26 | #[derive(Clone)] 27 | pub struct Poker { 28 | tx: Vec>, 29 | } 30 | 31 | impl Poker { 32 | pub fn new() -> Self { 33 | Self { tx: vec![] } 34 | } 35 | 36 | pub fn add(&mut self) -> crossbeam_channel::Receiver<()> { 37 | let (tx, rx) = crossbeam_channel::unbounded(); 38 | self.tx.push(tx); 39 | rx 40 | } 41 | 42 | pub fn poke(&self) { 43 | for tx in self.tx.iter() { 44 | let _ = tx.send(()); 45 | } 46 | } 47 | } 48 | 49 | struct RowVisitor { 50 | tx: crossbeam_channel::Sender, 51 | command_name: String, 52 | } 53 | 54 | pub fn block_to_su_entry(idx: usize, block: i3bar::Block) -> Vec { 55 | let name = block.name.unwrap_or_else(|| format!("{}", idx)); 56 | let full_text = vec![state::UpdateEntry { 57 | name: Some(name.clone()), 58 | instance: block.instance.clone(), 59 | var: "full_text".into(), 60 | value: block.full_text, 61 | }]; 62 | block 63 | .other 64 | .into_iter() 65 | .map(|(var, value)| { 66 | let value = match value { 67 | serde_json::Value::String(s) => s, 68 | serde_json::Value::Bool(b) => b.to_string(), 69 | other => other.to_string(), 70 | }; 71 | state::UpdateEntry { 72 | name: Some(name.clone()), 73 | instance: block.instance.clone(), 74 | var, 75 | value, 76 | } 77 | }) 78 | .chain(full_text) 79 | .collect() 80 | } 81 | 82 | impl<'de> Visitor<'de> for RowVisitor { 83 | type Value = (); 84 | 85 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 86 | formatter.write_str("infinite array of i3bar protocol rows") 87 | } 88 | 89 | fn visit_seq(self, mut seq: A) -> Result 90 | where 91 | A: SeqAccess<'de>, 92 | { 93 | while let Some(row) = seq.next_element::>()? { 94 | let entries = row 95 | .into_iter() 96 | .enumerate() 97 | .flat_map(|(idx, block)| block_to_su_entry(idx, block)) 98 | .collect(); 99 | self.tx 100 | .send(state::Update::VarUpdate(state::VarUpdate { 101 | command_name: Some(self.command_name.clone()), 102 | entries, 103 | ..Default::default() 104 | })) 105 | .unwrap(); 106 | } 107 | Ok(()) 108 | } 109 | } 110 | 111 | struct PlainSender { 112 | command_name: String, 113 | tx: crossbeam_channel::Sender, 114 | line_names: Vec, // has at least 1 element. 115 | entries: Vec, 116 | } 117 | 118 | impl PlainSender { 119 | fn new( 120 | command_name: &str, 121 | tx: crossbeam_channel::Sender, 122 | line_names: Vec, 123 | ) -> Self { 124 | let line_names = if line_names.is_empty() { 125 | vec!["value".to_string()] 126 | } else { 127 | line_names 128 | }; 129 | let entries = Vec::with_capacity(line_names.len()); 130 | Self { 131 | command_name: command_name.into(), 132 | tx, 133 | line_names, 134 | entries, 135 | } 136 | } 137 | 138 | fn send(&mut self, line: String) -> anyhow::Result<()> { 139 | self.entries.push(state::UpdateEntry { 140 | var: self.line_names.get(self.entries.len()).unwrap().clone(), 141 | value: line, 142 | ..Default::default() 143 | }); 144 | if self.entries.len() == self.line_names.len() { 145 | let entries = 146 | std::mem::replace(&mut self.entries, Vec::with_capacity(self.line_names.len())); 147 | self.tx.send(state::Update::VarUpdate(state::VarUpdate { 148 | command_name: Some(self.command_name.clone()), 149 | entries, 150 | ..Default::default() 151 | }))?; 152 | } 153 | Ok(()) 154 | } 155 | } 156 | 157 | #[derive(Debug, Deserialize, Clone, Eq, PartialEq)] 158 | #[serde(rename_all = "snake_case")] 159 | pub enum Format { 160 | Auto, 161 | Plain, 162 | I3bar, 163 | } 164 | 165 | #[derive(Debug, Deserialize, Clone)] 166 | pub struct CommandConfig { 167 | name: Option, 168 | command: String, 169 | interval: Option, 170 | #[serde(default = "default_format")] 171 | format: Format, 172 | #[serde(default)] 173 | line_names: Vec, 174 | #[serde(default)] 175 | once: bool, 176 | } 177 | 178 | fn default_format() -> Format { 179 | Format::Auto 180 | } 181 | 182 | pub struct Command { 183 | pub index: usize, 184 | pub config: CommandConfig, 185 | } 186 | 187 | impl Command { 188 | fn run_command( 189 | &self, 190 | command_name: &str, 191 | tx: &crossbeam_channel::Sender, 192 | ) -> anyhow::Result<()> { 193 | let mut child = std::process::Command::new("sh") 194 | .arg("-c") 195 | .arg(&self.config.command) 196 | .stdout(std::process::Stdio::piped()) 197 | .spawn() 198 | .context("Failed spawning")?; 199 | if let Err(e) = self.process_child_output(command_name, &mut child, tx.clone()) { 200 | return Err(anyhow::anyhow!("Error running command: {:?}", e)); 201 | } 202 | let result = child.wait()?; 203 | if !result.success() { 204 | if let Some(code) = result.code() { 205 | return Err(anyhow::anyhow!("command exit code {:?}", code)); 206 | } else { 207 | return Err(anyhow::anyhow!( 208 | "command exit code unknown, result: {:?}", 209 | result 210 | )); 211 | } 212 | } 213 | Ok(()) 214 | } 215 | 216 | fn process_child_output( 217 | &self, 218 | command_name: &str, 219 | child: &mut std::process::Child, 220 | tx: crossbeam_channel::Sender, 221 | ) -> anyhow::Result<()> { 222 | let stdout = child.stdout.take().unwrap(); 223 | let mut reader = BufReader::new(stdout); 224 | 225 | let mut format = self.config.format.clone(); 226 | 227 | let mut plain_sender = 228 | PlainSender::new(command_name, tx.clone(), self.config.line_names.clone()); 229 | 230 | if format == Format::Auto || format == Format::I3bar { 231 | let mut first_line = String::new(); 232 | reader.read_line(&mut first_line)?; 233 | match serde_json::from_str::(&first_line) { 234 | Ok(header) => { 235 | if header.version != 1 { 236 | return Err(anyhow::anyhow!( 237 | "Unexpected i3bar protocol version: {}", 238 | header.version 239 | )); 240 | } 241 | format = Format::I3bar; 242 | } 243 | Err(e) => { 244 | if format == Format::I3bar { 245 | return Err(anyhow::anyhow!("Cannot parse i3bar header: {:?}", e)); 246 | } 247 | // It was Auto, falling back to Plain. 248 | format = Format::Plain; 249 | if first_line.ends_with('\n') { 250 | first_line.pop(); 251 | if first_line.ends_with('\r') { 252 | first_line.pop(); 253 | } 254 | } 255 | plain_sender.send(first_line)?; 256 | } 257 | } 258 | } 259 | 260 | if format == Format::I3bar { 261 | let mut stream = serde_json::Deserializer::from_reader(reader); 262 | stream.deserialize_seq(RowVisitor { 263 | tx, 264 | command_name: command_name.into(), 265 | })?; 266 | return Ok(()); 267 | } 268 | 269 | // Process plain format. 270 | for line in reader.lines() { 271 | if let Err(e) = &line { 272 | tracing::warn!("Error from command {:?}: {:?}", command_name, e); 273 | break; 274 | } 275 | let line = line.ok().unwrap_or_default(); 276 | plain_sender.send(line)?; 277 | } 278 | Ok(()) 279 | } 280 | 281 | pub fn spawn( 282 | self, 283 | tx: crossbeam_channel::Sender, 284 | poke_rx: crossbeam_channel::Receiver<()>, 285 | ) -> anyhow::Result<()> { 286 | let command_name = self 287 | .config 288 | .name 289 | .clone() 290 | .unwrap_or_else(|| format!("cm{}", self.index)); 291 | 292 | let result = { 293 | let tx = tx.clone(); 294 | let command_name = command_name.clone(); 295 | thread::spawn(command_name.clone(), move || loop { 296 | let result = self.run_command(&command_name, &tx); 297 | if let Err(e) = result { 298 | tx.send(state::Update::VarUpdate(state::VarUpdate { 299 | command_name: Some(command_name.clone()), 300 | error: Some(format!("Command failed: {:?}", e)), 301 | ..Default::default() 302 | }))?; 303 | } 304 | if self.config.once { 305 | return Ok(()); 306 | } 307 | select! { 308 | recv(poke_rx) -> _ => tracing::info!("Skipping interval for {} command", command_name), 309 | default(Duration::from_secs(self.config.interval.unwrap_or(10))) => (), 310 | } 311 | }) 312 | }; 313 | if let Err(e) = result { 314 | tx.send(state::Update::VarUpdate(state::VarUpdate { 315 | command_name: Some(command_name.clone()), 316 | error: Some(format!("Spawning thread failed: {:?}", e)), 317 | ..Default::default() 318 | }))?; 319 | } 320 | Ok(()) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::config; 16 | use crate::parse; 17 | // use crate::parse::AlignDirection; 18 | 19 | use anyhow::Context; 20 | 21 | use std::collections::{BTreeMap, HashMap}; 22 | 23 | #[derive(Clone, Debug, Default)] 24 | pub struct State { 25 | pub vars: HashMap, 26 | pub error: Option, 27 | pub command_errors: BTreeMap, 28 | pub var_snapshot_updates_tx: Vec>, 29 | pub pointer_position: HashMap, 30 | config: config::Config, 31 | } 32 | 33 | fn format_error_str(error_str: &str) -> String { 34 | use itertools::Itertools; 35 | error_str 36 | .split('\n') 37 | .filter(|s| !s.trim().is_empty()) 38 | .join(" ") 39 | } 40 | 41 | impl State { 42 | pub fn new( 43 | config: config::Config, 44 | var_snapshot_updates_tx: Vec>, 45 | ) -> Self { 46 | Self { 47 | config, 48 | var_snapshot_updates_tx, 49 | ..Default::default() 50 | } 51 | } 52 | 53 | pub fn build_error_msg(&self) -> Option { 54 | if let Some(error) = &self.error { 55 | Some(error.clone()) 56 | } else if let Some((cmd, error)) = self.command_errors.first_key_value() { 57 | Some(format!("{}: {}", cmd, error)) 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | pub fn handle_state_update(&mut self, state_update: Update) { 64 | match state_update { 65 | Update::VarUpdate(u) => self.handle_var_update(u), 66 | Update::MotionUpdate(u) => self.handle_motion_update(u), 67 | } 68 | } 69 | 70 | pub fn handle_motion_update(&mut self, motion_update: MotionUpdate) { 71 | if let Some(position) = motion_update.position { 72 | self.pointer_position 73 | .insert(motion_update.window_name, position); 74 | } else { 75 | self.pointer_position.remove(&motion_update.window_name); 76 | } 77 | } 78 | 79 | pub fn handle_var_update(&mut self, var_update: VarUpdate) { 80 | let mut var_snapshot_update = VarSnapshotUpdate { 81 | vars: Default::default(), 82 | }; 83 | 84 | for update in var_update.entries.into_iter() { 85 | let mut var = Vec::with_capacity(3); 86 | if let Some(name) = update.name { 87 | var.push(name); 88 | } 89 | if let Some(instance) = update.instance { 90 | var.push(instance); 91 | } 92 | var.push(update.var); 93 | let name = match var_update.command_name { 94 | Some(ref command_name) => format!("{}:{}", command_name, var.join(".")), 95 | None => var.join("."), 96 | }; 97 | 98 | let old_value = self 99 | .vars 100 | .insert(name.clone(), update.value.clone()) 101 | .unwrap_or_default(); 102 | if old_value != update.value { 103 | var_snapshot_update.vars.insert(name, update.value); 104 | } 105 | } 106 | 107 | self.error = None; 108 | for var_name in self.config.var_order.iter() { 109 | let var = self 110 | .config 111 | .vars 112 | .get_mut(var_name) 113 | .expect("var from var_order should be present in the map"); 114 | match var 115 | .input 116 | .update(&self.vars) 117 | .with_context(|| format!("var: '{}'", var.name)) 118 | { 119 | Ok(updated) if updated => { 120 | let processed: &str = &var.input.value; 121 | self.vars.insert(var.name.clone(), processed.to_string()); 122 | var_snapshot_update 123 | .vars 124 | .insert(var.name.clone(), processed.to_string()); 125 | } 126 | Err(e) => { 127 | self.error = Some(format_error_str(&format!("{:?}", e))); 128 | } 129 | _ => {} 130 | } 131 | } 132 | 133 | if let Some(command_name) = var_update.command_name { 134 | if let Some(error) = var_update.error { 135 | self.command_errors.insert( 136 | command_name, 137 | format_error_str(&format!("State error: {}", error)), 138 | ); 139 | } else { 140 | self.command_errors.remove(&command_name); 141 | } 142 | } 143 | 144 | if !var_snapshot_update.vars.is_empty() { 145 | for rx in self.var_snapshot_updates_tx.iter() { 146 | if let Err(e) = rx.send(var_snapshot_update.clone()) { 147 | tracing::error!( 148 | "Failed to send var update: {:?}: {:?}", 149 | var_snapshot_update, 150 | e 151 | ); 152 | } 153 | } 154 | } 155 | } 156 | } 157 | 158 | #[derive(Debug, Default)] 159 | pub struct MotionUpdate { 160 | pub window_name: String, 161 | pub position: Option<(i16, i16)>, 162 | } 163 | 164 | #[derive(Debug, Default)] 165 | pub struct UpdateEntry { 166 | pub name: Option, 167 | pub instance: Option, 168 | pub var: String, 169 | pub value: String, 170 | } 171 | 172 | #[derive(Debug, Default)] 173 | pub struct VarUpdate { 174 | pub command_name: Option, 175 | pub entries: Vec, 176 | pub error: Option, 177 | } 178 | 179 | #[derive(Debug)] 180 | pub enum Update { 181 | VarUpdate(VarUpdate), 182 | MotionUpdate(MotionUpdate), 183 | } 184 | 185 | #[derive(Debug, Default, Clone)] 186 | pub struct VarSnapshotUpdate { 187 | pub vars: HashMap, 188 | } 189 | -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | mod protocol; 2 | use anyhow::Context; 3 | use protocol::i3bar; 4 | use std::collections::{BTreeMap, HashMap}; 5 | use systemstat::Platform; 6 | 7 | fn try_extend(blocks: &mut Vec, extra_blocks: anyhow::Result>) { 8 | match extra_blocks { 9 | Ok(extra_blocks) => { 10 | blocks.extend(extra_blocks); 11 | } 12 | Err(e) => { 13 | eprintln!("Failed to get stats: {:?}", e); 14 | } 15 | } 16 | } 17 | 18 | fn cpu( 19 | system: &P, 20 | cpu_load: std::io::Result>, 21 | ) -> anyhow::Result> { 22 | let cpu_load = cpu_load?.done()?; 23 | let percent = ((1.0 - cpu_load.idle) * 100.0) as u16; 24 | let mut other = BTreeMap::new(); 25 | other.insert("percent".into(), percent.into()); 26 | if let Ok(temp) = system.cpu_temp() { 27 | other.insert("temp_c".into(), (temp as u16).into()); 28 | } 29 | Ok(vec![i3bar::Block { 30 | name: Some("cpu".into()), 31 | instance: None, 32 | full_text: format!("cpu:{: >3}%", percent), 33 | other, 34 | }]) 35 | } 36 | 37 | fn memory(system: &P) -> anyhow::Result> { 38 | let mem = system.memory()?; 39 | let used = mem.total.as_u64() - mem.free.as_u64(); 40 | let percent = used * 100 / mem.total.as_u64(); 41 | let mut other = BTreeMap::new(); 42 | other.insert("percent".into(), percent.into()); 43 | other.insert("used".into(), used.into()); 44 | other.insert("free".into(), mem.free.as_u64().into()); 45 | other.insert("total".into(), mem.total.as_u64().into()); 46 | Ok(vec![i3bar::Block { 47 | name: Some("memory".into()), 48 | instance: None, 49 | full_text: format!("mem:{: >3}% {}", percent, mem.total), 50 | other, 51 | }]) 52 | } 53 | 54 | #[derive(Debug)] 55 | struct Address { 56 | up: bool, 57 | running: bool, 58 | address: Option, 59 | netmask: Option, 60 | broadcast: Option, 61 | destination: Option, 62 | } 63 | 64 | #[derive(Debug, Default)] 65 | struct Interface { 66 | mac: Vec
, 67 | ipv4: Vec
, 68 | ipv6: Vec
, 69 | } 70 | 71 | fn sockaddr_to_str(addr: nix::sys::socket::SockaddrStorage) -> String { 72 | let s: String = addr.to_string(); 73 | match s.strip_suffix(":0") { 74 | Some(s) => s.to_owned(), 75 | None => s, 76 | } 77 | } 78 | 79 | fn get_interfaces() -> anyhow::Result> { 80 | let addrs = nix::ifaddrs::getifaddrs().context("getifaddrs")?; 81 | let mut map: BTreeMap = BTreeMap::new(); 82 | for ifaddr in addrs { 83 | use nix::sys::socket::SockaddrLike; 84 | let running = ifaddr 85 | .flags 86 | .contains(nix::net::if_::InterfaceFlags::IFF_RUNNING); 87 | let up = ifaddr.flags.contains(nix::net::if_::InterfaceFlags::IFF_UP); 88 | let address = Address { 89 | running, 90 | up, 91 | address: ifaddr.address.map(sockaddr_to_str), 92 | netmask: ifaddr.netmask.map(sockaddr_to_str), 93 | broadcast: ifaddr.broadcast.map(sockaddr_to_str), 94 | destination: ifaddr.destination.map(sockaddr_to_str), 95 | }; 96 | if let Some(addr) = ifaddr.address { 97 | use nix::sys::socket::AddressFamily; 98 | let interface = map.entry(ifaddr.interface_name).or_default(); 99 | match addr.family() { 100 | #[cfg(any( 101 | target_os = "dragonfly", 102 | target_os = "freebsd", 103 | target_os = "ios", 104 | target_os = "macos", 105 | target_os = "netbsd", 106 | target_os = "openbsd" 107 | ))] 108 | Some(AddressFamily::Link) => { 109 | interface.mac.push(address); 110 | } 111 | #[cfg(any( 112 | target_os = "android", 113 | target_os = "linux", 114 | target_os = "fuchsia", 115 | target_os = "solaris" 116 | ))] 117 | Some(AddressFamily::Packet) => { 118 | interface.mac.push(address); 119 | } 120 | Some(AddressFamily::Inet) => { 121 | interface.ipv4.push(address); 122 | } 123 | Some(AddressFamily::Inet6) => { 124 | interface.ipv6.push(address); 125 | } 126 | _ => {} 127 | } 128 | } 129 | } 130 | Ok(map) 131 | } 132 | 133 | fn insert_address( 134 | other: &mut BTreeMap, 135 | prefix: &str, 136 | address: &Address, 137 | ) { 138 | other.insert(format!("{}_run", prefix), address.running.into()); 139 | other.insert(format!("{}_up", prefix), address.up.into()); 140 | if let Some(v) = &address.address { 141 | other.insert(format!("{}_addr", prefix), v.clone().into()); 142 | } 143 | if let Some(v) = &address.netmask { 144 | other.insert(format!("{}_mask", prefix), v.clone().into()); 145 | } 146 | if let Some(v) = &address.broadcast { 147 | other.insert(format!("{}_broadcast", prefix), v.clone().into()); 148 | } 149 | if let Some(v) = &address.destination { 150 | other.insert(format!("{}_dest", prefix), v.clone().into()); 151 | } 152 | } 153 | 154 | fn network( 155 | system: &P, 156 | name: &str, 157 | interface: &Interface, 158 | network_stats: &mut HashMap, 159 | ) -> anyhow::Result> { 160 | let mut other = BTreeMap::new(); 161 | for (idx, addr) in interface.ipv4.iter().enumerate() { 162 | insert_address(&mut other, &format!("ipv4_{}", idx), addr); 163 | } 164 | for (idx, addr) in interface.ipv6.iter().enumerate() { 165 | insert_address(&mut other, &format!("ipv6_{}", idx), addr); 166 | } 167 | for (idx, addr) in interface.mac.iter().enumerate() { 168 | insert_address(&mut other, &format!("mac_{}", idx), addr); 169 | } 170 | if let Ok(stats) = system.network_stats(name) { 171 | if let Some(old_stats) = network_stats.get(name).cloned() { 172 | other.insert( 173 | "rx_per_sec".into(), 174 | (stats.rx_bytes.as_u64() - old_stats.rx_bytes.as_u64()).into(), 175 | ); 176 | other.insert( 177 | "tx_per_sec".into(), 178 | (stats.tx_bytes.as_u64() - old_stats.tx_bytes.as_u64()).into(), 179 | ); 180 | } 181 | network_stats.insert(name.into(), stats); 182 | } 183 | let first_up = interface 184 | .ipv4 185 | .iter() 186 | .find(|a| a.running && a.address.is_some()) 187 | .or_else(|| { 188 | interface 189 | .ipv6 190 | .iter() 191 | .find(|a| a.running && a.address.is_some()) 192 | }) 193 | .map(|up| up.address.clone().unwrap()); 194 | let full_text = match first_up { 195 | Some(addr) => format!("{}: {}", name, addr), 196 | None => format!("{}: down", name), 197 | }; 198 | Ok(vec![i3bar::Block { 199 | name: Some("net".into()), 200 | instance: Some(name.into()), 201 | full_text, 202 | other, 203 | }]) 204 | } 205 | 206 | fn main() -> anyhow::Result<()> { 207 | println!("{}", serde_json::to_string(&i3bar::Header::default())?); 208 | println!("["); 209 | let system = systemstat::System::new(); 210 | let mut network_stats = HashMap::new(); 211 | loop { 212 | let cpu_load = system.cpu_load_aggregate(); 213 | std::thread::sleep(std::time::Duration::from_secs(1)); 214 | let mut blocks = vec![]; 215 | try_extend(&mut blocks, memory(&system).context("memory")); 216 | try_extend(&mut blocks, cpu(&system, cpu_load).context("cpu")); 217 | let interfaces = get_interfaces()?; 218 | for (name, interface) in interfaces { 219 | try_extend( 220 | &mut blocks, 221 | network(&system, &name, &interface, &mut network_stats).context("network"), 222 | ); 223 | } 224 | println!("{},", serde_json::to_string(&blocks)?); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/thread.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::Context; 16 | use tracing::*; 17 | 18 | pub fn spawn(name: S, f: F) -> anyhow::Result<()> 19 | where 20 | S: Into, 21 | F: FnOnce() -> anyhow::Result<()> + Send + 'static, 22 | { 23 | let name: String = name.into(); 24 | let context_name = name.clone(); 25 | std::thread::Builder::new() 26 | .name(name) 27 | .spawn(move || { 28 | trace!("Thread started."); 29 | let result = f(); 30 | match &result { 31 | Ok(_) => { 32 | trace!("Thread finished: {:?}.", result); 33 | } 34 | Err(_) => { 35 | error!("Thread finished: {:?}", result); 36 | } 37 | } 38 | }) 39 | .with_context(move || format!("failed to spawn thread: {}", context_name))?; 40 | 41 | Ok(()) 42 | } 43 | 44 | pub fn spawn_loop(name: S, mut f: F) -> anyhow::Result<()> 45 | where 46 | S: Into, 47 | F: FnMut() -> anyhow::Result + Send + 'static, 48 | { 49 | spawn(name, move || loop { 50 | let keep_running = f()?; 51 | if !keep_running { 52 | return Ok(()); 53 | } 54 | })?; 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /src/timer.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | sync::{Arc, Mutex}, 5 | thread::sleep, 6 | time::SystemTime, 7 | }; 8 | 9 | use crate::thread; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct Timer { 13 | at: Arc>, 14 | } 15 | 16 | impl Timer { 17 | pub fn new(name: &str, at: SystemTime, f: F) -> anyhow::Result 18 | where 19 | F: Fn() + Send + 'static, 20 | { 21 | let timer = Timer { 22 | at: Arc::new(Mutex::new(at)), 23 | }; 24 | { 25 | let timer = timer.clone(); 26 | thread::spawn_loop(name, move || { 27 | let at = timer.elapses_at(); 28 | match at.duration_since(SystemTime::now()) { 29 | Ok(duration) => { 30 | sleep(duration); 31 | Ok(true) 32 | } 33 | Err(_) => { 34 | f(); 35 | Ok(false) 36 | } 37 | } 38 | })?; 39 | } 40 | Ok(timer) 41 | } 42 | 43 | fn elapses_at(&self) -> SystemTime { 44 | *self.at.lock().unwrap() 45 | } 46 | 47 | pub fn set_at(&self, time: SystemTime) { 48 | let mut at = self.at.lock().unwrap(); 49 | *at = time; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/wait.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub fn wait_ready() -> anyhow::Result<()> { 16 | let conn = xcb::Connection::connect(None)?; 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::Context; 16 | use std::{ 17 | collections::{HashMap, HashSet}, 18 | sync::{Arc, Mutex, RwLock}, 19 | time::{Duration, SystemTime}, 20 | }; 21 | use xcb::{x, xinput, Xid}; 22 | 23 | use crate::{ 24 | bar::{self, BarUpdates, BlockUpdates}, 25 | config, drawing, parse, state, timer, wmready, xutils, 26 | }; 27 | use tracing::*; 28 | 29 | pub struct VisibilityControl { 30 | name: String, 31 | conn: Arc, 32 | window_id: x::Window, 33 | timer: Option, 34 | show_only: Option>>, 35 | visible: bool, 36 | default_visibility: bool, 37 | popped_up: bool, 38 | } 39 | 40 | impl VisibilityControl { 41 | fn is_poping_up(&self) -> bool { 42 | self.timer.is_some() 43 | } 44 | 45 | fn set_visible(&mut self, visible: bool) -> anyhow::Result<()> { 46 | if self.visible == visible { 47 | return Ok(()); 48 | } 49 | if visible { 50 | xutils::send( 51 | &self.conn, 52 | &x::MapWindow { 53 | window: self.window_id, 54 | }, 55 | )?; 56 | } else { 57 | xutils::send( 58 | &self.conn, 59 | &x::UnmapWindow { 60 | window: self.window_id, 61 | }, 62 | )?; 63 | } 64 | self.conn.flush()?; 65 | self.visible = visible; 66 | Ok(()) 67 | } 68 | 69 | fn extend_show_only(&mut self, extra_show_only: HashMap>) { 70 | if extra_show_only.is_empty() { 71 | return; 72 | } 73 | 74 | let show_only = self.show_only.get_or_insert_with(Default::default); 75 | 76 | for (k, v) in extra_show_only.into_iter() { 77 | show_only.entry(k).or_default().extend(v.into_iter()); 78 | } 79 | } 80 | 81 | fn set_default_visibility(&mut self, value: bool) -> anyhow::Result<()> { 82 | if value == self.default_visibility { 83 | return Ok(()); 84 | } 85 | self.default_visibility = value; 86 | if !self.popped_up { 87 | self.set_visible(self.default_visibility)?; 88 | } 89 | Ok(()) 90 | } 91 | 92 | fn reset_show_only(&mut self) { 93 | self.show_only = None 94 | } 95 | 96 | fn show_or_prolong_popup( 97 | visibility_control_lock: &Arc>, 98 | ) -> anyhow::Result<()> { 99 | let reset_timer_at = SystemTime::now() 100 | .checked_add(Duration::from_secs(1)) 101 | .unwrap(); 102 | let mut visibility_control = visibility_control_lock.write().unwrap(); 103 | match &visibility_control.timer { 104 | Some(timer) => { 105 | timer.set_at(reset_timer_at); 106 | } 107 | None => { 108 | let timer = { 109 | visibility_control.set_visible(true)?; 110 | visibility_control.popped_up = true; 111 | let visibility_control_lock = visibility_control_lock.clone(); 112 | timer::Timer::new( 113 | &format!("pptimer-{}", visibility_control.name), 114 | reset_timer_at, 115 | move || { 116 | let mut visibility_control = visibility_control_lock.write().unwrap(); 117 | visibility_control.timer = None; 118 | visibility_control.reset_show_only(); 119 | let default_visibility = visibility_control.default_visibility; 120 | visibility_control.popped_up = false; 121 | if let Err(e) = visibility_control.set_visible(default_visibility) { 122 | tracing::error!("Failed to show window: {:?}", e); 123 | } 124 | }, 125 | )? 126 | }; 127 | visibility_control.timer = Some(timer); 128 | } 129 | } 130 | Ok(()) 131 | } 132 | } 133 | 134 | pub struct Window { 135 | pub conn: Arc, 136 | pub id: x::Window, 137 | pub name: String, 138 | pub width: u16, 139 | pub height: u16, 140 | back_buffer_context: drawing::Context, 141 | shape_buffer_context: drawing::Context, 142 | swap_gc: x::Gcontext, 143 | bar: bar::Bar, 144 | // bar_index: usize, 145 | bar_config: config::Bar, 146 | state: Arc>, 147 | screen: x::ScreenBuf, 148 | state_update_tx: crossbeam_channel::Sender, 149 | visibility_control: Arc>, 150 | } 151 | 152 | impl Window { 153 | pub fn create_and_show( 154 | name: String, 155 | // bar_index: usize, 156 | config: &config::Config, 157 | bar_config: config::Bar, 158 | conn: Arc, 159 | state: Arc>, 160 | state_update_tx: crossbeam_channel::Sender, 161 | wm_info: &wmready::WMInfo, 162 | ) -> anyhow::Result { 163 | info!("Loading bar {:?}", name); 164 | let screen = { 165 | let setup = conn.get_setup(); 166 | setup.roots().next().unwrap() 167 | } 168 | .to_owned(); 169 | 170 | let mut vis32 = match_visual(&screen, 32).unwrap(); 171 | 172 | let margin = &bar_config.margin; 173 | 174 | let height = bar_config.height; 175 | 176 | let monitor = crate::xrandr::get_monitor(&conn, screen.root(), &bar_config.monitor)? 177 | .unwrap_or_else(|| crate::xrandr::Monitor { 178 | name: "default".into(), 179 | primary: true, 180 | x: 0, 181 | y: 0, 182 | width: screen.width_in_pixels(), 183 | height: screen.height_in_pixels(), 184 | }); 185 | 186 | let window_width = monitor.width; 187 | let window_height = height + margin.top + margin.bottom; 188 | 189 | let cid = conn.generate_id(); 190 | xutils::send( 191 | &conn, 192 | &x::CreateColormap { 193 | mid: cid, 194 | window: screen.root(), 195 | visual: vis32.visual_id(), 196 | alloc: x::ColormapAlloc::None, 197 | }, 198 | )?; 199 | 200 | let id: x::Window = conn.generate_id(); 201 | let y = match bar_config.position { 202 | config::BarPosition::Top => 0, 203 | config::BarPosition::Center => (monitor.height as i16 - window_height as i16) / 2, 204 | config::BarPosition::Bottom => monitor.height as i16 - window_height as i16, 205 | }; 206 | let x = monitor.x as i16; 207 | 208 | info!( 209 | "Placing the bar at x: {}, y: {}, width: {}, height: {}", 210 | x, y, window_width, window_height 211 | ); 212 | conn.send_request(&x::CreateWindow { 213 | depth: 32, 214 | wid: id, 215 | parent: screen.root(), 216 | x, 217 | y, 218 | width: window_width, 219 | height: window_height, 220 | border_width: 0, 221 | class: x::WindowClass::InputOutput, 222 | visual: vis32.visual_id(), 223 | value_list: &[ 224 | x::Cw::BorderPixel(screen.white_pixel()), 225 | x::Cw::OverrideRedirect(bar_config.popup), 226 | //x::Cw::OverrideRedirect(true), 227 | x::Cw::EventMask( 228 | x::EventMask::EXPOSURE 229 | | x::EventMask::KEY_PRESS 230 | | x::EventMask::BUTTON_PRESS 231 | | x::EventMask::LEAVE_WINDOW 232 | | x::EventMask::POINTER_MOTION, 233 | ), 234 | x::Cw::Colormap(cid), 235 | ], 236 | }); 237 | 238 | let raw_motion_mask_buf = 239 | xinput::EventMaskBuf::new(xinput::Device::All, &[xinput::XiEventMask::RAW_MOTION]); 240 | 241 | xutils::send( 242 | &conn, 243 | &xinput::XiSelectEvents { 244 | window: screen.root(), 245 | masks: &[raw_motion_mask_buf], 246 | }, 247 | )?; 248 | 249 | let app_name = "oatbar".as_bytes(); 250 | xutils::replace_property_atom(&conn, id, x::ATOM_WM_NAME, x::ATOM_STRING, app_name)?; 251 | xutils::replace_property_atom(&conn, id, x::ATOM_WM_CLASS, x::ATOM_STRING, app_name)?; 252 | if let Err(e) = xutils::replace_atom_property( 253 | &conn, 254 | id, 255 | "_NET_WM_WINDOW_TYPE", 256 | &["_NET_WM_WINDOW_TYPE_DOCK"], 257 | ) { 258 | warn!("Unable to set window property: {:?}", e); 259 | } 260 | xutils::replace_atom_property( 261 | &conn, 262 | id, 263 | "_NET_WM_STATE", 264 | &["_NET_WM_STATE_STICKY", "_NET_WM_STATE_ABOVE"], 265 | )?; 266 | 267 | if !bar_config.popup { 268 | let top = bar_config.position == config::BarPosition::Top; 269 | let sp_result = xutils::replace_property( 270 | &conn, 271 | id, 272 | "_NET_WM_STRUT_PARTIAL", 273 | x::ATOM_CARDINAL, 274 | &[ 275 | 0_u32, 276 | 0, 277 | if top { window_height.into() } else { 0 }, 278 | if top { 0 } else { window_height.into() }, 279 | 0, 280 | 0, 281 | 0, 282 | 0, 283 | 0, 284 | if top { window_width.into() } else { 0 }, 285 | 0, 286 | if top { 0 } else { window_width.into() }, 287 | ], 288 | ) 289 | .context("_NET_WM_STRUT_PARTIAL"); 290 | if let Err(e) = sp_result { 291 | debug!("Unable to set _NET_WM_STRUT_PARTIAL: {:?}", e); 292 | } 293 | let s_result = xutils::replace_property( 294 | &conn, 295 | id, 296 | "_NET_WM_STRUT", 297 | x::ATOM_CARDINAL, 298 | &[ 299 | 0_u32, 300 | 0, 301 | if top { window_height.into() } else { 0 }, 302 | if top { 0 } else { window_height.into() }, 303 | ], 304 | ) 305 | .context("_NET_WM_STRUT"); 306 | if let Err(e) = s_result { 307 | debug!("Unable to set _NET_WM_STRUT: {:?}", e); 308 | } 309 | } 310 | let back_buffer: x::Pixmap = conn.generate_id(); 311 | xutils::send( 312 | &conn, 313 | &x::CreatePixmap { 314 | depth: 32, 315 | pid: back_buffer, 316 | drawable: xcb::x::Drawable::Window(id), 317 | width: window_width, 318 | height: window_height, 319 | }, 320 | )?; 321 | 322 | let font_cache = Arc::new(Mutex::new(drawing::FontCache::new())); 323 | #[cfg(feature = "image")] 324 | let image_loader = drawing::ImageLoader::new(); 325 | 326 | let back_buffer_surface = 327 | make_pixmap_surface(&conn, &back_buffer, &mut vis32, window_width, window_height)?; 328 | let back_buffer_context = drawing::Context::new( 329 | font_cache.clone(), 330 | #[cfg(feature = "image")] 331 | image_loader.clone(), 332 | back_buffer, 333 | back_buffer_surface, 334 | drawing::Mode::Full, 335 | )?; 336 | 337 | let shape_buffer: x::Pixmap = conn.generate_id(); 338 | xutils::send( 339 | &conn, 340 | &x::CreatePixmap { 341 | depth: 1, 342 | pid: shape_buffer, 343 | drawable: xcb::x::Drawable::Window(id), 344 | width: window_width, 345 | height: window_height, 346 | }, 347 | )?; 348 | let shape_buffer_surface = make_pixmap_surface_for_bitmap( 349 | &conn, 350 | &shape_buffer, 351 | &screen, 352 | window_width, 353 | window_height, 354 | )?; 355 | let shape_buffer_context = drawing::Context::new( 356 | font_cache, 357 | #[cfg(feature = "image")] 358 | image_loader, 359 | shape_buffer, 360 | shape_buffer_surface, 361 | drawing::Mode::Shape, 362 | )?; 363 | 364 | let swap_gc: x::Gcontext = conn.generate_id(); 365 | xutils::send( 366 | &conn, 367 | &x::CreateGc { 368 | cid: swap_gc, 369 | drawable: x::Drawable::Window(id), 370 | value_list: &[x::Gc::GraphicsExposures(false)], 371 | }, 372 | )?; 373 | conn.flush()?; 374 | 375 | let mut config_value_list = 376 | vec![x::ConfigWindow::X(x.into()), x::ConfigWindow::Y(y.into())]; 377 | xutils::send( 378 | &conn, 379 | &x::ConfigureWindow { 380 | window: id, 381 | value_list: &config_value_list, 382 | }, 383 | )?; 384 | conn.flush()?; 385 | 386 | if !bar_config.popup { 387 | xutils::send(&conn, &x::MapWindow { window: id })?; 388 | config_value_list.extend_from_slice(&[ 389 | x::ConfigWindow::Sibling(wm_info.support), 390 | x::ConfigWindow::StackMode(x::StackMode::Below), 391 | ]); 392 | } 393 | 394 | if let Err(e) = xutils::send( 395 | &conn, 396 | &x::ConfigureWindow { 397 | window: id, 398 | value_list: &config_value_list, 399 | }, 400 | ) { 401 | tracing::error!("Failed to restack: {:?}", e); 402 | } 403 | conn.flush()?; 404 | 405 | let visible = !bar_config.popup; 406 | 407 | let bar = bar::Bar::new(config, bar_config.clone())?; 408 | 409 | Ok(Self { 410 | conn: conn.clone(), 411 | id, 412 | name: name.clone(), 413 | width: window_width, 414 | height: window_height, 415 | back_buffer_context, 416 | shape_buffer_context, 417 | swap_gc, 418 | // bar_index, 419 | bar, 420 | state, 421 | state_update_tx, 422 | screen, 423 | bar_config, 424 | visibility_control: Arc::new(RwLock::new(VisibilityControl { 425 | name, 426 | window_id: id, 427 | timer: None, 428 | conn, 429 | show_only: None, 430 | visible, 431 | default_visibility: visible, 432 | popped_up: false, 433 | })), 434 | }) 435 | } 436 | 437 | fn render_bar(&mut self, redraw: &bar::RedrawScope) -> anyhow::Result<()> { 438 | self.bar.render(&self.back_buffer_context, redraw)?; 439 | self.bar.render(&self.shape_buffer_context, redraw)?; 440 | 441 | self.swap_buffers()?; 442 | self.apply_shape()?; 443 | self.conn.flush()?; 444 | Ok(()) 445 | } 446 | 447 | pub fn render(&mut self, from_os: bool) -> anyhow::Result<()> { 448 | let state = self.state.clone(); 449 | let state = state.read().unwrap(); 450 | let pointer_position = state.pointer_position.get(&self.name).copied(); 451 | let mut error = state.build_error_msg(); 452 | 453 | let mut updates = 454 | match self 455 | .bar 456 | .update(&mut self.back_buffer_context, &state.vars, pointer_position) 457 | { 458 | Ok(updates) => updates, 459 | Err(e) => { 460 | error = Some(format!("Error: {:?}", e)); 461 | BarUpdates { 462 | block_updates: BlockUpdates { 463 | redraw: bar::RedrawScope::All, 464 | popup: Default::default(), 465 | }, 466 | visible_from_vars: None, 467 | } 468 | } 469 | }; 470 | 471 | self.bar.set_error(&mut self.back_buffer_context, error); 472 | 473 | if from_os { 474 | updates.block_updates.redraw = bar::RedrawScope::All; 475 | } 476 | 477 | if self.bar_config.popup && !updates.block_updates.popup.is_empty() { 478 | if let Err(e) = VisibilityControl::show_or_prolong_popup(&self.visibility_control) { 479 | tracing::error!("Showing popup failed: {:?}", e); 480 | } 481 | } 482 | let (visible, show_only, mut redraw) = { 483 | let mut visibility_control = self.visibility_control.write().unwrap(); 484 | if let Some(visible_from_vars) = updates.visible_from_vars { 485 | visibility_control.set_default_visibility(visible_from_vars)?; 486 | } 487 | if self.bar_config.popup { 488 | visibility_control.extend_show_only(updates.block_updates.popup); 489 | let redraw_mode = if visibility_control.show_only.is_some() { 490 | bar::RedrawScope::All 491 | } else { 492 | updates.block_updates.redraw 493 | }; 494 | // Maybe there is a race condition between visibility and rendering. 495 | ( 496 | visibility_control.visible, 497 | visibility_control.show_only.clone(), 498 | redraw_mode, 499 | ) 500 | } else { 501 | ( 502 | visibility_control.visible, 503 | None, 504 | updates.block_updates.redraw, 505 | ) 506 | } 507 | }; 508 | 509 | if visible && redraw != bar::RedrawScope::None { 510 | let layout_changed = self.bar.layout_groups(self.width as f64, &show_only); 511 | if layout_changed { 512 | redraw = bar::RedrawScope::All; 513 | } 514 | 515 | self.render_bar(&redraw)?; 516 | } 517 | Ok(()) 518 | } 519 | 520 | pub fn handle_button_press( 521 | &mut self, 522 | x: i16, 523 | y: i16, 524 | button: bar::Button, 525 | ) -> anyhow::Result<()> { 526 | self.bar.handle_button_press(x, y, button) 527 | } 528 | 529 | pub fn handle_raw_motion(&self, x: i16, y: i16) -> anyhow::Result<()> { 530 | self.handle_motion_popup(x, y)?; 531 | Ok(()) 532 | } 533 | 534 | pub fn handle_motion(&self, x: i16, y: i16) -> anyhow::Result<()> { 535 | self.state_update_tx 536 | .send(state::Update::MotionUpdate(state::MotionUpdate { 537 | window_name: self.name.clone(), 538 | position: Some((x, y)), 539 | }))?; 540 | Ok(()) 541 | } 542 | 543 | pub fn handle_motion_leave(&self) -> anyhow::Result<()> { 544 | self.state_update_tx 545 | .send(state::Update::MotionUpdate(state::MotionUpdate { 546 | window_name: self.name.clone(), 547 | position: None, 548 | }))?; 549 | Ok(()) 550 | } 551 | 552 | pub fn handle_motion_popup(&self, _x: i16, y: i16) -> anyhow::Result<()> { 553 | if !self.bar_config.popup || !self.bar_config.popup_at_edge { 554 | return Ok(()); 555 | } 556 | let edge_size: i16 = 3; 557 | let screen_height: i16 = self.screen.height_in_pixels() as i16; 558 | let over_window = match self.bar_config.position { 559 | config::BarPosition::Top => y < self.height as i16, 560 | config::BarPosition::Bottom => y > screen_height - self.height as i16, 561 | config::BarPosition::Center => false, 562 | }; 563 | let over_edge = match self.bar_config.position { 564 | config::BarPosition::Top => y < edge_size, 565 | config::BarPosition::Bottom => y > screen_height - edge_size, 566 | config::BarPosition::Center => false, 567 | }; 568 | 569 | let mut visibility_control = self.visibility_control.write().unwrap(); 570 | if !visibility_control.is_poping_up() { 571 | if !visibility_control.visible && over_edge { 572 | visibility_control.set_visible(true)?; 573 | } else if visibility_control.visible && !over_window { 574 | visibility_control.set_visible(false)?; 575 | visibility_control.reset_show_only(); 576 | } 577 | } 578 | Ok(()) 579 | } 580 | 581 | fn apply_shape(&self) -> anyhow::Result<()> { 582 | self.shape_buffer_context.buffer_surface.flush(); 583 | xutils::send( 584 | &self.conn, 585 | &xcb::shape::Mask { 586 | operation: xcb::shape::So::Set, 587 | destination_kind: xcb::shape::Sk::Bounding, 588 | destination_window: self.id, 589 | x_offset: 0, 590 | y_offset: 0, 591 | source_bitmap: self.shape_buffer_context.buffer, 592 | }, 593 | )?; 594 | Ok(()) 595 | } 596 | 597 | fn swap_buffers(&self) -> anyhow::Result<()> { 598 | self.back_buffer_context.buffer_surface.flush(); 599 | xutils::send( 600 | &self.conn, 601 | &xcb::x::ClearArea { 602 | window: self.id, 603 | x: 0, 604 | y: 0, 605 | height: self.height, 606 | width: self.width, 607 | exposures: false, 608 | }, 609 | )?; 610 | self.conn.flush()?; 611 | xutils::send( 612 | &self.conn, 613 | &xcb::x::CopyArea { 614 | src_drawable: xcb::x::Drawable::Pixmap(self.back_buffer_context.buffer), 615 | dst_drawable: xcb::x::Drawable::Window(self.id), 616 | src_x: 0, 617 | src_y: 0, 618 | dst_x: 0, 619 | dst_y: 0, 620 | width: self.width, 621 | height: self.height, 622 | gc: self.swap_gc, 623 | }, 624 | )?; 625 | Ok(()) 626 | } 627 | } 628 | 629 | fn match_visual(screen: &xcb::x::Screen, depth: u8) -> Option { 630 | let d_iter: xcb::x::DepthIterator = screen.allowed_depths(); 631 | for allowed_depth in d_iter { 632 | if allowed_depth.depth() != depth { 633 | continue; 634 | } 635 | for vis in allowed_depth.visuals() { 636 | if vis.class() == xcb::x::VisualClass::TrueColor { 637 | return Some(*vis); 638 | } 639 | } 640 | } 641 | None 642 | } 643 | 644 | fn make_pixmap_surface( 645 | conn: &xcb::Connection, 646 | pixmap: &x::Pixmap, 647 | visual: &mut x::Visualtype, 648 | width: u16, 649 | height: u16, 650 | ) -> anyhow::Result { 651 | let cairo_xcb_connection = unsafe { 652 | cairo::XCBConnection::from_raw_none(std::mem::transmute::< 653 | *mut xcb::ffi::xcb_connection_t, 654 | *mut cairo::ffi::xcb_connection_t, 655 | >(conn.get_raw_conn())) 656 | }; 657 | let cairo_xcb_visual = unsafe { 658 | cairo::XCBVisualType::from_raw_none(std::mem::transmute::< 659 | *mut xcb::x::Visualtype, 660 | *mut cairo::ffi::xcb_visualtype_t, 661 | >(visual as *mut _)) 662 | }; 663 | 664 | let pixmap_surface = cairo::XCBSurface::create( 665 | &cairo_xcb_connection, 666 | &cairo::XCBDrawable(pixmap.resource_id()), 667 | &cairo_xcb_visual, 668 | width.into(), 669 | height.into(), 670 | )?; 671 | 672 | conn.flush()?; 673 | 674 | Ok(pixmap_surface) 675 | } 676 | 677 | fn make_pixmap_surface_for_bitmap( 678 | conn: &xcb::Connection, 679 | pixmap: &x::Pixmap, 680 | screen: &x::Screen, 681 | width: u16, 682 | height: u16, 683 | ) -> anyhow::Result { 684 | let cairo_xcb_connection = unsafe { 685 | cairo::XCBConnection::from_raw_none(std::mem::transmute::< 686 | *mut xcb::ffi::xcb_connection_t, 687 | *mut cairo::ffi::xcb_connection_t, 688 | >(conn.get_raw_conn())) 689 | }; 690 | let cairo_xcb_screen = unsafe { 691 | cairo::XCBScreen::from_raw_none( 692 | screen as *const _ as *mut x::Screen as *mut cairo::ffi::xcb_screen_t, 693 | ) 694 | }; 695 | let cairo_xcb_pixmap = cairo::XCBPixmap(pixmap.resource_id()); 696 | 697 | let pixmap_surface = cairo::XCBSurface::create_for_bitmap( 698 | &cairo_xcb_connection, 699 | &cairo_xcb_screen, 700 | &cairo_xcb_pixmap, 701 | width.into(), 702 | height.into(), 703 | )?; 704 | 705 | conn.flush()?; 706 | 707 | Ok(pixmap_surface) 708 | } 709 | -------------------------------------------------------------------------------- /src/wmready.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::anyhow; 16 | use xcb::x; 17 | 18 | use crate::xutils; 19 | use tracing::*; 20 | 21 | pub struct WMInfo { 22 | pub name: String, 23 | pub support: x::Window, 24 | } 25 | 26 | fn validate_wm( 27 | conn: &xcb::Connection, 28 | screen: &x::Screen, 29 | wm_support_atom: x::Atom, 30 | wm_name: x::Atom, 31 | wm_supported: x::Atom, 32 | ) -> anyhow::Result { 33 | let reply = xutils::get_property(conn, screen.root(), wm_support_atom, x::ATOM_WINDOW, 2)?; 34 | 35 | let support = *reply 36 | .value::() 37 | .first() 38 | .ok_or_else(|| anyhow!("Failed to find wm window"))?; 39 | 40 | let reply = xutils::get_property(conn, support, wm_name, x::ATOM_ANY, 256)?; 41 | let name = String::from_utf8_lossy(reply.value::()).to_string(); 42 | 43 | let reply = xutils::get_property(conn, screen.root(), wm_supported, x::ATOM_ATOM, 4096)?; 44 | 45 | info!("Supported EWMH: {:?}", reply); 46 | 47 | Ok(WMInfo { name, support }) 48 | } 49 | 50 | fn refetch_atoms(conn: &xcb::Connection) -> anyhow::Result<(x::Atom, x::Atom, x::Atom)> { 51 | let wm_support_atom = xutils::get_atom(conn, "_NET_SUPPORTING_WM_CHECK")?; 52 | let wm_name = xutils::get_atom(conn, "_NET_WM_NAME")?; 53 | let wm_supported = xutils::get_atom(conn, "_NET_SUPPORTED")?; 54 | info!( 55 | "Debug: wm_support={:?}, wm_name={:?}, wm_net_supported={:?}", 56 | wm_support_atom, wm_name, wm_supported 57 | ); 58 | Ok((wm_support_atom, wm_name, wm_supported)) 59 | } 60 | 61 | pub fn wait() -> anyhow::Result { 62 | let (conn, screen_num) = xcb::Connection::connect(None)?; 63 | let screen = { 64 | let setup = conn.get_setup(); 65 | setup.roots().nth(screen_num as usize).unwrap() 66 | }; 67 | xutils::send( 68 | &conn, 69 | &x::ChangeWindowAttributes { 70 | window: screen.root(), 71 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 72 | }, 73 | )?; 74 | conn.flush()?; 75 | 76 | let (wm_support_atom, wm_name, wm_supported) = refetch_atoms(&conn)?; 77 | if let Ok(wm_info) = validate_wm(&conn, screen, wm_support_atom, wm_name, wm_supported) { 78 | info!("Detected WM: {:?}", wm_info.name); 79 | return Ok(wm_info); 80 | } 81 | 82 | info!("WM not detected on startup, waiting for it to initialize..."); 83 | 84 | // TODO: fix infinite waiting here. 85 | 86 | while let Ok(event) = xutils::get_event(&conn) { 87 | let (wm_support_atom, wm_name, wm_supported) = refetch_atoms(&conn)?; 88 | match event { 89 | Some(xcb::Event::X(x::Event::PropertyNotify(pn))) if pn.atom() == wm_support_atom => { 90 | if let Ok(wm_info) = 91 | validate_wm(&conn, screen, wm_support_atom, wm_name, wm_supported) 92 | { 93 | info!("Eventually detected WM: {:?}", wm_info.name); 94 | 95 | return Ok(wm_info); 96 | } 97 | } 98 | other => { 99 | debug!("Unhandled event: {:?}", other); 100 | } 101 | } 102 | } 103 | 104 | Err(anyhow!( 105 | "Unable to detect WM, maybe your WM does not support EWMH" 106 | )) 107 | } 108 | -------------------------------------------------------------------------------- /src/xrandr.rs: -------------------------------------------------------------------------------- 1 | use tracing::*; 2 | use xcb::{randr, x}; 3 | 4 | use crate::xutils; 5 | 6 | #[allow(dead_code)] 7 | #[derive(Debug, Clone)] 8 | pub struct Monitor { 9 | pub name: String, 10 | pub primary: bool, 11 | pub x: u16, 12 | pub y: u16, 13 | pub width: u16, 14 | pub height: u16, 15 | } 16 | 17 | pub fn get_monitor( 18 | conn: &xcb::Connection, 19 | root: x::Window, 20 | name: &Option, 21 | ) -> anyhow::Result> { 22 | if let Some(name) = &name { 23 | tracing::info!("Trying to find monitor {:?}", name); 24 | } else { 25 | tracing::info!("Trying to find the primary monitor."); 26 | } 27 | 28 | let monitors_reply = xutils::query( 29 | conn, 30 | &randr::GetMonitors { 31 | window: root, 32 | get_active: true, 33 | }, 34 | )?; 35 | 36 | let mut monitors = Vec::::with_capacity(monitors_reply.monitors().count()); 37 | 38 | for info in monitors_reply.monitors() { 39 | let name_reply = xutils::query(conn, &x::GetAtomName { atom: info.name() })?; 40 | 41 | let monitor = Monitor { 42 | name: name_reply.name().to_utf8().into(), 43 | primary: info.primary(), 44 | x: info.x() as u16, 45 | y: info.y() as u16, 46 | width: info.width(), 47 | height: info.height(), 48 | }; 49 | 50 | info!("Detected {:?}", monitor); 51 | monitors.push(monitor); 52 | } 53 | 54 | if monitors.is_empty() { 55 | warn!("No monitors returned by XRandr"); 56 | return Ok(None); 57 | } 58 | 59 | let monitor_found = if let Some(name) = &name { 60 | monitors.iter().find(|m| m.name == *name) 61 | } else { 62 | monitors.iter().find(|m| m.primary) 63 | }; 64 | 65 | let monitor = if let Some(monitor) = monitor_found { 66 | info!("Monitor found: {}", monitor.name); 67 | monitor_found 68 | } else if let Some(name) = name { 69 | return Err(anyhow::anyhow!( 70 | "Monitor {:?} not found, but specified for the bar", 71 | name 72 | )); 73 | } else { 74 | info!("Primary monitor not found, picking the first one"); 75 | monitors.first() 76 | }; 77 | Ok(monitor.cloned()) 78 | } 79 | -------------------------------------------------------------------------------- /src/xutils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oatbar Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Debug; 16 | 17 | use anyhow::Context; 18 | use tracing::*; 19 | use xcb::x; 20 | 21 | pub fn get_atom(conn: &xcb::Connection, name: &str) -> anyhow::Result { 22 | let reply = query( 23 | conn, 24 | &x::InternAtom { 25 | only_if_exists: true, 26 | name: name.as_bytes(), 27 | }, 28 | ) 29 | .context(format!("get_atom: {}", name))?; 30 | 31 | Ok(reply.atom()) 32 | } 33 | 34 | pub fn get_property( 35 | conn: &xcb::Connection, 36 | window: x::Window, 37 | atom: x::Atom, 38 | atom_type: x::Atom, 39 | long_length: u32, 40 | ) -> anyhow::Result { 41 | let reply = query( 42 | conn, 43 | &x::GetProperty { 44 | property: atom, 45 | window, 46 | r#type: atom_type, 47 | long_offset: 0, 48 | long_length, 49 | delete: false, 50 | }, 51 | )?; 52 | Ok(reply) 53 | } 54 | 55 | pub fn replace_property_atom( 56 | conn: &xcb::Connection, 57 | window: x::Window, 58 | atom: x::Atom, 59 | atom_type: x::Atom, 60 | value: &[P], 61 | ) -> anyhow::Result<()> { 62 | send( 63 | conn, 64 | &x::ChangeProperty { 65 | mode: x::PropMode::Replace, 66 | window, 67 | property: atom, 68 | r#type: atom_type, 69 | data: value, 70 | }, 71 | ) 72 | .context(format!("replace_property_atom: {:?}", atom))?; 73 | Ok(()) 74 | } 75 | 76 | pub fn replace_property( 77 | conn: &xcb::Connection, 78 | window: x::Window, 79 | atom_name: &str, 80 | atom_type: x::Atom, 81 | value: &[P], 82 | ) -> anyhow::Result { 83 | let atom = get_atom(conn, atom_name)?; 84 | send( 85 | conn, 86 | &x::ChangeProperty { 87 | mode: x::PropMode::Replace, 88 | window, 89 | property: atom, 90 | r#type: atom_type, 91 | data: value, 92 | }, 93 | ) 94 | .context(format!("replace_property: {}", atom_name))?; 95 | Ok(atom) 96 | } 97 | 98 | pub fn replace_atom_property( 99 | conn: &xcb::Connection, 100 | window: x::Window, 101 | atom_name: &str, 102 | value_atom_name: &[&str], 103 | ) -> anyhow::Result<(x::Atom, Vec)> { 104 | let mut value_atoms = Vec::with_capacity(value_atom_name.len()); 105 | for atom_name in value_atom_name { 106 | let value_atom = get_atom(conn, atom_name)?; 107 | value_atoms.push(value_atom); 108 | } 109 | let atom = replace_property( 110 | conn, 111 | window, 112 | atom_name, 113 | x::ATOM_ATOM, 114 | value_atoms.as_slice(), 115 | )?; 116 | Ok((atom, value_atoms)) 117 | } 118 | 119 | pub fn send( 120 | conn: &xcb::Connection, 121 | req: &X, 122 | ) -> anyhow::Result<()> { 123 | let cookie = conn.send_request_checked(req); 124 | conn.check_request(cookie) 125 | .with_context(|| format!("xcb request failed: req={:?}", req))?; 126 | Ok(()) 127 | } 128 | 129 | pub fn query( 130 | conn: &xcb::Connection, 131 | req: &X, 132 | ) -> anyhow::Result<<::Cookie as xcb::CookieWithReplyChecked>::Reply> 133 | where 134 | ::Cookie: xcb::CookieWithReplyChecked, 135 | { 136 | let cookie = conn.send_request(req); 137 | conn.wait_for_reply(cookie) 138 | .with_context(|| format!("xcb request failed: req={:?}", req)) 139 | } 140 | 141 | #[inline] 142 | pub fn handler_event_errors( 143 | event: Result, 144 | ) -> anyhow::Result> { 145 | let event = match event { 146 | Err(xcb::Error::Connection(xcb::ConnError::Connection)) => { 147 | debug!( 148 | "XCB connection terminated for thread {}", 149 | std::thread::current().name().unwrap_or("") 150 | ); 151 | return Ok(None); 152 | } 153 | Err(err) => { 154 | return Err(anyhow::anyhow!( 155 | "unexpected error: {:#?}, {}", 156 | err, 157 | err.to_string() 158 | )); 159 | } 160 | Ok(event) => event, 161 | }; 162 | Ok(Some(event)) 163 | } 164 | 165 | #[inline] 166 | pub fn get_event(conn: &xcb::Connection) -> anyhow::Result> { 167 | handler_event_errors(conn.wait_for_event()) 168 | } 169 | --------------------------------------------------------------------------------