├── .cargo └── config.toml ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other-issues.md ├── dependabot.yml └── workflows │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .zellij.kdl ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── demo.gif ├── demo.png └── socialcard.png ├── benches ├── benches.rs └── widgets.rs ├── cliff.toml ├── examples ├── compact.kdl ├── compact.png ├── conky.conf ├── conky.kdl ├── conky.png ├── simple.kdl ├── simple.png ├── slanted.kdl ├── slanted.png ├── swap-layouts.kdl ├── swap-layouts.png ├── tmux.kdl └── tmux.png ├── flake.lock ├── flake.nix ├── justfile ├── rust-toolchain.toml ├── shell.nix ├── src ├── bin │ ├── zjframes.rs │ └── zjstatus.rs ├── border.rs ├── config.rs ├── frames.rs ├── lib.rs ├── pipe.rs ├── render.rs └── widgets │ ├── command.rs │ ├── datetime.rs │ ├── mod.rs │ ├── mode.rs │ ├── notification.rs │ ├── pipe.rs │ ├── session.rs │ ├── swap_layout.rs │ ├── tabs.rs │ └── widget.rs └── tests ├── zjframes ├── config.kdl └── layout.kdl └── zjstatus ├── config.kdl └── layout.kdl /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip1" 3 | 4 | [target.wasm32-wasip1] 5 | runner = ["wasmtime", "--dir", "./target::target"] 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Zellij version [e.g. v0.39.0] 29 | - Version [e.g. v0.9.0] 30 | 31 | **Layout** 32 | 33 | How does the layout look like? Please copy it into a code block. 34 | 35 | ```kdl 36 | // layout goes here <-- 37 | ``` 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issues 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description** 11 | 12 | Please describe as clear as possible how your issue looks like. Give examples and try to give as many valuable information as possible, such that another person without context is able to understand it. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: push 3 | name: Test & Lint 4 | 5 | # Make sure CI fails on all warnings, including Clippy lints 6 | env: 7 | RUSTFLAGS: "-Dwarnings" 8 | 9 | jobs: 10 | clippy_check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | 15 | - name: Install Rust 16 | uses: dtolnay/rust-toolchain@4f647fc679bcd3b11499ccb42104547c83dabe96 # stable 17 | with: 18 | toolchain: '1.86.0' 19 | target: wasm32-wasip1 20 | components: clippy 21 | 22 | - name: Run Clippy 23 | uses: clechasseur/rs-clippy-check@23f6dcf86d7e4e0d98b000bba0bb81ac587c44aa # v3 24 | with: 25 | args: --all-features --lib 26 | toolchain: '1.86.0' 27 | 28 | tests: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 32 | - uses: jcbhmr/setup-wasmtime@960c367a99921eb0b02f5778fce9ae8f110bf0f0 # v2 33 | 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@4f647fc679bcd3b11499ccb42104547c83dabe96 # stable 36 | with: 37 | toolchain: '1.86.0' 38 | target: wasm32-wasip1 39 | 40 | - name: Install cargo wasi 41 | run: cargo install cargo-nextest 42 | 43 | - name: Run tests 44 | run: cargo nextest run --lib 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-release: 11 | name: build-release 12 | runs-on: ubuntu-latest 13 | env: 14 | RUST_BACKTRACE: 1 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 18 | with: 19 | fetch-tags: false # https://github.com/actions/checkout/issues/1467 20 | 21 | - run: git fetch --tags --all --force && git pull origin ${{ github.ref }} --unshallow --force 22 | name: Fetch tags manually as a workaround. See https://github.com/actions/checkout/issues/1467 23 | 24 | - name: Install Rust 25 | uses: dtolnay/rust-toolchain@4f647fc679bcd3b11499ccb42104547c83dabe96 # stable 26 | with: 27 | profile: minimal 28 | override: true 29 | toolchain: '1.86.0' 30 | target: wasm32-wasip1 31 | 32 | - name: Build release binary 33 | run: cargo build --release 34 | 35 | - name: Generate a changelog 36 | uses: orhun/git-cliff-action@4a4a951bc43fafe41cd2348d181853f52356bee7 # v3 37 | id: git-cliff 38 | with: 39 | config: cliff.toml 40 | args: -vv --latest --strip header 41 | env: 42 | OUTPUT: CHANGES.md 43 | 44 | - name: Create release 45 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | draft: true 50 | body: ${{ steps.git-cliff.outputs.content }} 51 | prerelease: false 52 | files: | 53 | ./target/wasm32-wasip1/release/zjstatus.wasm 54 | ./target/wasm32-wasip1/release/zjframes.wasm 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .direnv/ 3 | .zjstatus.log 4 | .zjframes.log 5 | -------------------------------------------------------------------------------- /.zellij.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | default_tab_template { 3 | children 4 | pane size=1 borderless=true { 5 | plugin location="zjstatus" 6 | } 7 | } 8 | 9 | tab name="edit" focus=true { 10 | pane { 11 | command "direnv" 12 | args "exec" "." "hx" 13 | } 14 | } 15 | 16 | tab name="run" { 17 | pane split_direction="horizontal" { 18 | pane 19 | pane { 20 | cwd "/var/folders/tt/kygkt3t13gqd68q5rz6ly7pw0000gn/T/zellij-501" 21 | } 22 | } 23 | } 24 | 25 | tab name="git" { 26 | pane 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | daniel.jankowski (at) rub (dot) de. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zjstatus" 3 | version = "0.20.2" 4 | authors = ["Daniel Jankowski"] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "zjstatus" 9 | bench = false 10 | 11 | [[bin]] 12 | name = "zjframes" 13 | bench = false 14 | 15 | [lib] 16 | bench = false 17 | 18 | [features] 19 | bench = [] 20 | tracing = [] 21 | 22 | [dependencies] 23 | zellij-tile = "0.42.2" 24 | chrono = { version = "0.4.40", default-features = false } 25 | regex = "1.11.1" 26 | chrono-tz = "0.10.3" 27 | anyhow = "1.0.98" 28 | anstyle = "1.0.10" 29 | uuid = { version = "1.16.0", features = ["v4"] } 30 | lazy_static = "1.5.0" 31 | cached = { version = "0.55.1", features = ["wasm"] } 32 | console = "0.15.11" 33 | tracing-subscriber = "0.3.19" 34 | tracing = "0.1.41" 35 | kdl = { version = "6.3.4", features = ["v1", "v1-fallback"] } 36 | rstest = "0.25.0" 37 | itertools = "0.14.0" 38 | 39 | [dev-dependencies] 40 | criterion = { version = "0.5.1", default-features = false, features = [ 41 | "html_reports", 42 | ] } 43 | 44 | [[bench]] 45 | name = "benches" 46 | harness = false 47 | 48 | [[bench]] 49 | name = "widgets" 50 | harness = false 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Jankowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

zjstatus & zjframes

2 | 3 |

4 | A configurable and themable statusbar for zellij. 5 |

6 | 7 | clippy check 8 | 9 | 10 | latest version 11 | 12 | 13 | GitHub Wiki 14 | 15 | 16 |

17 | The goal of this statusbar is to provide a highly customizable and extensible statusbar for zellij. Single 18 | modules can be formatted separately. Due to the widget structure new modules can be created with ease. 19 | 20 | As an addition, this repsitory contains *zjframes* which can be used to toggle pane frames based on different 21 | conditions even without loading *zjstatus*, e.g. when using the default status bars. 22 |

23 | 24 | ![Screenshot of the statusbar](./assets/demo.png) 25 | 26 | ### [👉 Check out and share your awesome configs in the community showcase!](https://github.com/dj95/zjstatus/discussions/44) 27 | 28 |
29 |

Examples

30 | tmux style 31 | tmux style bar 32 |
33 | simple style 34 | simple style bar 35 |
36 | slanted style 37 | slanted style bar 38 |
39 | example for swapping layouts with zjstatus 40 | example for swapping layouts with zjstatus 41 |
42 | compact style (thanks to @segaja) 43 | compact style bar 44 |
45 | conky status (thanks to @benzwt) 46 | conky.conf 47 | conky status 48 |
49 | Demo GIF 50 | Demo GIF of zellij with zjstatus 51 |
52 | 53 | ## 🚀 Installation 54 | 55 | > [!TIP] 56 | > For more detailed instructions, check out the [wiki](https://github.com/dj95/zjstatus/wiki/1-%E2%80%90-Installation)! 57 | 58 | Download the latest binary in the github releases. Place it somewhere, zellij is able to access it. Then the 59 | plugin can be included by referencing it in a layout file, e.g. the default layout one, or the config file. 60 | 61 | In contrast to *zjstatus*, *zjframes* should only be used in the `load_plugins` option of the *config.kdl* 62 | from zellij, as it should only be loaded in the background. For more details, please follow the [documentation](https://github.com/dj95/zjstatus/wiki/6---zjframes) 63 | 64 | You could also refer to the plugin guide from zellij, after downloading the binary: [https://zellij.dev/documentation/plugin-loading](https://zellij.dev/documentation/plugin-loading) 65 | 66 | Please ensure, that the configuration is correct. 67 | 68 | > [!IMPORTANT] 69 | > In case you experience any crashes or issues, please in the first step try to clear the cache! (`$HOME/.cache/zellij/` for Linux, `$HOME/Library/Caches/org.Zellij-Contributors.Zellij/` on macOS) 70 | 71 | Sometimes, especially when updating plugins, it might come to caching issues, which can be resolved by clearing it. Please keep in 72 | mind, that it will also clear the cache for running sessions and revokes granted permissions for plugins. 73 | 74 | ## ❄️ Installation with nix flake 75 | 76 | Add this repository to your inputs and then with the following overlay to your packages. 77 | Then you are able to install and refer to it with `pkgs.zjstatus`. When templating the 78 | config file, you can use `${pkgs.zjstatus}/bin/zjstatus.wasm` as the path. `${pkgs.zjstatus}/bin/zjframes.wasm` 79 | is also available in case you only want to use *zjframes*. 80 | 81 | ```nix 82 | inputs = { 83 | # ... 84 | 85 | zjstatus = { 86 | url = "github:dj95/zjstatus"; 87 | }; 88 | }; 89 | 90 | 91 | # define the outputs of this flake - especially the home configurations 92 | outputs = { self, nixpkgs, zjstatus, ... }@inputs: 93 | let 94 | inherit (inputs.nixpkgs.lib) attrValues; 95 | 96 | overlays = with inputs; [ 97 | # ... 98 | (final: prev: { 99 | zjstatus = zjstatus.packages.${prev.system}.default; 100 | }) 101 | ]; 102 | ``` 103 | 104 | ## ⚙️ Configuration 105 | 106 | For configuring, please follow the [documentation](https://github.com/dj95/zjstatus/wiki/3-%E2%80%90-Configuration). 107 | 108 | ## 🏎️ Quick Start for zjstatus 109 | 110 | Place the following configuration in your default layout file, e.g. `~/.config/zellij/layouts/default.kdl`. Right after starting zellij, it will prompt for permissions, that needs to be granted in order for zjstatus to work. Simply navigate to the pane or click on it and press `y`. This must be repeated on updates. For more details on permissions, please visit the [wiki](https://github.com/dj95/zjstatus/wiki/2-%E2%80%90-Permissions). 111 | 112 | > [!IMPORTANT] 113 | > Downloading zjstatus as file and using `file:~/path/to/zjstatus.wasm` is recommend, even if the quickstart includes the https location. 114 | 115 | > [!IMPORTANT] 116 | > Using zjstatus involves creating new layouts and overriding the default one. This will lead to swap layouts not working, when they are not configured correctly. Please follow [this documentation](https://github.com/dj95/zjstatus/wiki/3-%E2%80%90-Configuration#swap-layouts) for getting swap layouts back to work, if you need them. 117 | 118 | > [!IMPORTANT] 119 | > If you want to hide borders, please remove the `hide_frame_for_single_pane` option or set it to `false`. Otherwise zjstatus will toggle frame borders even if the are hidden in zellijs config! 120 | 121 | ```javascript 122 | layout { 123 | default_tab_template { 124 | children 125 | pane size=1 borderless=true { 126 | plugin location="https://github.com/dj95/zjstatus/releases/latest/download/zjstatus.wasm" { 127 | format_left "{mode} #[fg=#89B4FA,bold]{session}" 128 | format_center "{tabs}" 129 | format_right "{command_git_branch} {datetime}" 130 | format_space "" 131 | 132 | border_enabled "false" 133 | border_char "─" 134 | border_format "#[fg=#6C7086]{char}" 135 | border_position "top" 136 | 137 | hide_frame_for_single_pane "true" 138 | 139 | mode_normal "#[bg=blue] " 140 | mode_tmux "#[bg=#ffc387] " 141 | 142 | tab_normal "#[fg=#6C7086] {name} " 143 | tab_active "#[fg=#9399B2,bold,italic] {name} " 144 | 145 | command_git_branch_command "git rev-parse --abbrev-ref HEAD" 146 | command_git_branch_format "#[fg=blue] {stdout} " 147 | command_git_branch_interval "10" 148 | command_git_branch_rendermode "static" 149 | 150 | datetime "#[fg=#6C7086,bold] {format} " 151 | datetime_format "%A, %d %b %Y %H:%M" 152 | datetime_timezone "Europe/Berlin" 153 | } 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | ## 🏎️ Quickstart for zjframes 160 | 161 | Add the following to the *config.kdl* or add the plugin to `load_plugins`, if you already load other plugins in the background. 162 | Double check if the configuration matches your expectations. Then restart zellij. 163 | 164 | > [!IMPORTANT] 165 | > Downloading zjframes as file and using `file:~/path/to/zjframes.wasm` is recommend, even if the quickstart includes the https location. 166 | 167 | ```javascript 168 | // Plugins to load in the background when a new session starts 169 | load_plugins { 170 | "https://github.com/dj95/zjstatus/releases/latest/download/zjframes.wasm" { 171 | hide_frame_for_single_pane "true" 172 | hide_frame_except_for_search "true" 173 | hide_frame_except_for_scroll "true" 174 | hide_frame_except_for_fullscreen "true" 175 | } 176 | } 177 | ``` 178 | 179 | ## 🧱 Widgets 180 | 181 | The documentation for the widgets can be found in the [wiki](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets). 182 | 183 | The following widgets are available: 184 | 185 | - [command](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#command) 186 | - [datetime](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#datetime) 187 | - [mode](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#mode) 188 | - [notifications](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#notifications) 189 | - [pipe](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#pipe) 190 | - [session](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#session) 191 | - [swap layout](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#swap-layout) 192 | - [tabs](https://github.com/dj95/zjstatus/wiki/4-%E2%80%90-Widgets#tabs) 193 | 194 | ## 🚧 Development 195 | 196 | Make sure you have rust and the `wasm32-wasi` target installed. If using nix, you could utilize the nix-shell 197 | in this repo for obtaining `cargo` and `rustup`. Then you'll only need to add the target with 198 | `rustup target add wasm32-wasi`. 199 | 200 | With the toolchain, simply build `zjstatus` with `cargo build`. Then you are able to run the example configuration 201 | with `zellij -l plugin-dev-workspace.kdl` from the root of the repository. 202 | 203 | ## 🤝 Contributing 204 | 205 | If you are missing features or find some annoying bugs please feel free to submit an issue or a bugfix within a pull request :) 206 | 207 | ## 📝 License 208 | 209 | © 2024 Daniel Jankowski 210 | 211 | This project is licensed under the MIT license. 212 | 213 | Permission is hereby granted, free of charge, to any person obtaining a copy 214 | of this software and associated documentation files (the "Software"), to deal 215 | in the Software without restriction, including without limitation the rights 216 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 217 | copies of the Software, and to permit persons to whom the Software is 218 | furnished to do so, subject to the following conditions: 219 | 220 | The above copyright notice and this permission notice shall be included in all 221 | copies or substantial portions of the Software. 222 | 223 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 224 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 225 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 226 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 227 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 228 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 229 | SOFTWARE. 230 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/assets/demo.gif -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/assets/demo.png -------------------------------------------------------------------------------- /assets/socialcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/assets/socialcard.png -------------------------------------------------------------------------------- /benches/benches.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use std::{collections::BTreeMap, sync::Arc}; 3 | use zellij_tile::prelude::{ModeInfo, TabInfo}; 4 | 5 | use zjstatus::{ 6 | config::{ModuleConfig, ZellijState}, 7 | render::{ 8 | formatted_part_from_string_cached, formatted_parts_from_string_cached, FormattedPart, 9 | }, 10 | widgets::{datetime::DateTimeWidget, mode::ModeWidget, session::SessionWidget, widget::Widget}, 11 | }; 12 | 13 | fn bench_moduleconfig_render_bar(c: &mut Criterion) { 14 | let config = BTreeMap::from([ 15 | ("format_left".to_owned(), "{mode} #[fg=#89B4FA,bg=#181825,bold]{session} {tabs} {command_1} {command_git_branch} {command_3}".to_owned()), 16 | ("format_right".to_owned(), "{datetime}".to_owned()), 17 | ("format_space".to_owned(), "#[bg=#181825]".to_owned()), 18 | ]); 19 | 20 | let mut module_config = ModuleConfig::new(&config).unwrap(); 21 | 22 | let mut widgets: BTreeMap> = BTreeMap::new(); 23 | 24 | widgets.insert( 25 | "mode".to_owned(), 26 | Arc::new(ModeWidget::new(&BTreeMap::from([( 27 | "mode_normal".to_owned(), 28 | "#[bg=blue] #[bg=yellow] ".to_owned(), 29 | )]))), 30 | ); 31 | 32 | widgets.insert( 33 | "datetime".to_owned(), 34 | Arc::new(DateTimeWidget::new(&BTreeMap::from([( 35 | "datetime".to_owned(), 36 | "#[fg=#6C7086,bg=#181825] {index} {name} ".to_owned(), 37 | )]))), 38 | ); 39 | 40 | widgets.insert( 41 | "session".to_owned(), 42 | Arc::new(SessionWidget::new(&BTreeMap::from([]))), 43 | ); 44 | 45 | let state = ZellijState { 46 | mode: ModeInfo::default(), 47 | tabs: vec![TabInfo { 48 | name: "test".to_owned(), 49 | active: true, 50 | ..Default::default() 51 | }], 52 | ..Default::default() 53 | }; 54 | 55 | c.bench_function("ModuleConfig::render_bar", |b| { 56 | b.iter(|| module_config.render_bar(state.clone(), widgets.clone())) 57 | }); 58 | } 59 | 60 | fn bench_formattedpart_format_string_with_widgets(c: &mut Criterion) { 61 | let mut format = FormattedPart::from_format_string( 62 | "#[fg=#9399B2,bg=#181825,bold,italic] {mode} {datetime} {session} [] ", 63 | &BTreeMap::new(), 64 | ); 65 | 66 | let mut widgets: BTreeMap> = BTreeMap::new(); 67 | 68 | widgets.insert( 69 | "mode".to_owned(), 70 | Arc::new(ModeWidget::new(&BTreeMap::from([( 71 | "mode_normal".to_owned(), 72 | "#[bg=blue] #[bg=yellow] ".to_owned(), 73 | )]))), 74 | ); 75 | 76 | widgets.insert( 77 | "datetime".to_owned(), 78 | Arc::new(DateTimeWidget::new(&BTreeMap::from([( 79 | "datetime".to_owned(), 80 | "#[fg=#6C7086,bg=#181825] {index} {name} ".to_owned(), 81 | )]))), 82 | ); 83 | 84 | widgets.insert( 85 | "session".to_owned(), 86 | Arc::new(SessionWidget::new(&BTreeMap::from([]))), 87 | ); 88 | 89 | let state = ZellijState { 90 | mode: ModeInfo::default(), 91 | tabs: vec![TabInfo { 92 | name: "test".to_owned(), 93 | active: true, 94 | ..Default::default() 95 | }], 96 | cache_mask: 0b00000011, 97 | ..Default::default() 98 | }; 99 | 100 | c.bench_function("FormattedPart::format_string_with_widgets", |b| { 101 | b.iter(|| format.format_string_with_widgets(&widgets, &state)) 102 | }); 103 | } 104 | 105 | fn bench_formattedpart_from_format_string(c: &mut Criterion) { 106 | c.bench_function("FormattedPart::from_format_string", |b| { 107 | b.iter(|| { 108 | FormattedPart::from_format_string( 109 | "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] ", 110 | &BTreeMap::new(), 111 | ) 112 | }) 113 | }); 114 | } 115 | 116 | fn bench_formattedpart_from_format_string_cached(c: &mut Criterion) { 117 | c.bench_function("formatted_part_from_string_cached", |b| { 118 | b.iter(|| { 119 | formatted_part_from_string_cached( 120 | "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] ", 121 | &BTreeMap::new(), 122 | ) 123 | }) 124 | }); 125 | } 126 | 127 | fn bench_formattedpart_multiple_from_format_string(c: &mut Criterion) { 128 | c.bench_function("FormattedPart::multiple_from_format_string", |b| { 129 | b.iter(|| { 130 | FormattedPart::multiple_from_format_string( 131 | "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] #[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] ", 132 | &BTreeMap::new(), 133 | ) 134 | }) 135 | }); 136 | } 137 | 138 | fn bench_formattedparts_from_format_string_cached(c: &mut Criterion) { 139 | c.bench_function("formatted_parts_from_string_cached", |b| { 140 | b.iter(|| { 141 | formatted_parts_from_string_cached( 142 | "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] #[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] ", 143 | &BTreeMap::new(), 144 | ) 145 | }) 146 | }); 147 | } 148 | 149 | fn bench_moduleconfig_new(c: &mut Criterion) { 150 | let mut config = BTreeMap::new(); 151 | 152 | config.insert("format_left".to_owned(), "{mode} #[fg=#89B4FA,bg=#181825,bold]{session} {tabs} {command_1} {command_git_branch} {command_3}".to_owned()); 153 | config.insert("format_right".to_owned(), "{datetime}".to_owned()); 154 | config.insert("format_space".to_owned(), "#[bg=#181825]".to_owned()); 155 | 156 | c.bench_function("ModuleConfig::new", |b| { 157 | b.iter(|| ModuleConfig::new(&config)) 158 | }); 159 | } 160 | 161 | fn criterion_benchmark(c: &mut Criterion) { 162 | bench_formattedpart_from_format_string(c); 163 | bench_formattedpart_from_format_string_cached(c); 164 | bench_formattedpart_multiple_from_format_string(c); 165 | bench_formattedparts_from_format_string_cached(c); 166 | bench_formattedpart_format_string_with_widgets(c); 167 | bench_moduleconfig_new(c); 168 | bench_moduleconfig_render_bar(c); 169 | } 170 | 171 | criterion_group!(benches, criterion_benchmark); 172 | criterion_main!(benches); 173 | -------------------------------------------------------------------------------- /benches/widgets.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, Local}; 2 | use criterion::{criterion_group, criterion_main, Criterion}; 3 | use std::{collections::BTreeMap, ops::Sub}; 4 | use zellij_tile::prelude::*; 5 | 6 | use zjstatus::{ 7 | config::ZellijState, 8 | widgets::{self, widget::Widget}, 9 | }; 10 | 11 | fn bench_widget_command(c: &mut Criterion) { 12 | let config = BTreeMap::from([ 13 | ( 14 | "command_test_format".to_owned(), 15 | "#[fg=#9399B2,bg=#181825,bold,italic] {exit_code} {stdout} ".to_owned(), 16 | ), 17 | ("command_test_interval".to_owned(), "100".to_owned()), 18 | ]); 19 | 20 | let wid = widgets::command::CommandWidget::new(&config); 21 | 22 | let ts = Local::now().sub(Duration::try_seconds(1).unwrap()); 23 | 24 | let state = ZellijState { 25 | command_results: BTreeMap::from([( 26 | "command_test".to_owned(), 27 | widgets::command::CommandResult { 28 | exit_code: Some(0), 29 | stdout: "test".to_owned(), 30 | stderr: "".to_owned(), 31 | context: BTreeMap::from([( 32 | "timestamp".to_owned(), 33 | ts.format(widgets::command::TIMESTAMP_FORMAT).to_string(), 34 | )]), 35 | }, 36 | )]), 37 | ..Default::default() 38 | }; 39 | 40 | c.bench_function("widgets::CommandWidget (static)", |b| { 41 | b.iter(|| { 42 | wid.process("command_test", &state); 43 | }) 44 | }); 45 | } 46 | 47 | fn bench_widget_command_dynamic(c: &mut Criterion) { 48 | let config = BTreeMap::from([ 49 | ( 50 | "command_test_format".to_owned(), 51 | "#[fg=#9399B2,bg=#181825,bold,italic] {exit_code} {stdout} ".to_owned(), 52 | ), 53 | ("command_test_interval".to_owned(), "100".to_owned()), 54 | ("command_test_rendermode".to_owned(), "dynamic".to_owned()), 55 | ]); 56 | 57 | let wid = widgets::command::CommandWidget::new(&config); 58 | 59 | let ts = Local::now().sub(Duration::try_seconds(1).unwrap()); 60 | 61 | let state = ZellijState { 62 | command_results: BTreeMap::from([( 63 | "command_test".to_owned(), 64 | widgets::command::CommandResult { 65 | exit_code: Some(0), 66 | stdout: "#[fg=#9399B2,bg=#181825,bold,italic] test #[fg=#9399B2,bg=#181825,bold,italic] test".to_owned(), 67 | stderr: "".to_owned(), 68 | context: BTreeMap::from([( 69 | "timestamp".to_owned(), 70 | ts.format(widgets::command::TIMESTAMP_FORMAT).to_string(), 71 | )]), 72 | }, 73 | )]), 74 | ..Default::default() 75 | }; 76 | 77 | c.bench_function("widgets::CommandWidget (dynamic)", |b| { 78 | b.iter(|| { 79 | wid.process("command_test", &state); 80 | }) 81 | }); 82 | } 83 | 84 | fn bench_widget_tabs(c: &mut Criterion) { 85 | let config = BTreeMap::from([ 86 | ( 87 | "tab_normal".to_owned(), 88 | "#[fg=#6C7086,bg=#181825] {index} {name} ".to_owned(), 89 | ), 90 | ( 91 | "tab_normal_fullscreen".to_owned(), 92 | "#[fg=#6C7086,bg=#181825] {index} {name} ".to_owned(), 93 | ), 94 | ( 95 | "tab_active".to_owned(), 96 | "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} ".to_owned(), 97 | ), 98 | ]); 99 | 100 | let wid = widgets::tabs::TabsWidget::new(&config); 101 | 102 | let state = ZellijState { 103 | tabs: vec![TabInfo { 104 | name: "test".to_owned(), 105 | active: true, 106 | ..Default::default() 107 | }], 108 | ..Default::default() 109 | }; 110 | 111 | c.bench_function("widgets::TabsWidget", |b| { 112 | b.iter(|| { 113 | wid.process("tabs", &state); 114 | }) 115 | }); 116 | } 117 | 118 | fn bench_widget_mod(c: &mut Criterion) { 119 | let config = BTreeMap::from([( 120 | "mode_normal".to_owned(), 121 | "#[bg=blue] #[bg=yellow] ".to_owned(), 122 | )]); 123 | 124 | let wid = widgets::mode::ModeWidget::new(&config); 125 | 126 | let state = ZellijState { 127 | mode: ModeInfo::default(), 128 | ..Default::default() 129 | }; 130 | 131 | c.bench_function("widgets::ModeWidget", |b| { 132 | b.iter(|| { 133 | wid.process("mode", &state); 134 | }) 135 | }); 136 | } 137 | 138 | fn bench_widget_datetime(c: &mut Criterion) { 139 | let config = BTreeMap::new(); 140 | 141 | let wid = widgets::datetime::DateTimeWidget::new(&config); 142 | 143 | let state = ZellijState::default(); 144 | 145 | c.bench_function("widgets::DateTimeWidget", |b| { 146 | b.iter(|| { 147 | wid.process("datetime", &state); 148 | }) 149 | }); 150 | } 151 | 152 | fn criterion_benchmark(c: &mut Criterion) { 153 | bench_widget_datetime(c); 154 | bench_widget_mod(c); 155 | bench_widget_tabs(c); 156 | bench_widget_command(c); 157 | bench_widget_command_dynamic(c); 158 | } 159 | 160 | criterion_group!(benches, criterion_benchmark); 161 | criterion_main!(benches); 162 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | { message = "^chore\\(deps.*\\)", skip = true }, 68 | { message = "^chore\\(pr\\)", skip = true }, 69 | { message = "^chore\\(pull\\)", skip = true }, 70 | { message = "^chore|ci", group = "⚙️ Miscellaneous Tasks" }, 71 | { body = ".*security", group = "🛡️ Security" }, 72 | { message = "^revert", group = "◀️ Revert" }, 73 | ] 74 | # protect breaking changes from being skipped due to matching a skipping commit_parser 75 | protect_breaking_commits = false 76 | # filter out the commits that are not matched by commit parsers 77 | filter_commits = false 78 | # regex for matching git tags 79 | # tag_pattern = "v[0-9].*" 80 | # regex for skipping tags 81 | # skip_tags = "" 82 | # regex for ignoring tags 83 | # ignore_tags = "" 84 | # sort the tags topologically 85 | topo_order = false 86 | # sort the commits inside sections by oldest/newest order 87 | sort_commits = "oldest" 88 | # limit the number of commits included in the changelog. 89 | # limit_commits = 42 90 | -------------------------------------------------------------------------------- /examples/compact.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=1 borderless=true { 7 | plugin location="file:target/wasm32-wasi/debug/zjstatus.wasm" { 8 | format_left "#[fg=#FFFFFF,bold] {session} {mode} {tabs}" 9 | format_right "#[bg=#8A8A8A,fg=#000000] #[bg=#8A8A8A,fg=#000000,bold]{swap_layout} #[bg=#000000,fg=#8A8A8A]" 10 | 11 | mode_locked "#[fg=#FF00D9,bold] {name} " 12 | mode_normal "#[fg=#AFFF00,bold] {name} " 13 | mode_resize "#[fg=#D75F00,bold] {name} " 14 | mode_default_to_mode "resize" 15 | 16 | tab_normal "#[bg=#8A8A8A,fg=#000000] #[bg=#8A8A8A,fg=#000000,bold]{name} {sync_indicator}{fullscreen_indicator}{floating_indicator} #[bg=#000000,fg=#8A8A8A]" 17 | tab_active "#[bg=#AFFF00,fg=#000000] #[bg=#AFFF00,fg=#000000,bold]{name} {sync_indicator}{fullscreen_indicator}{floating_indicator} #[bg=#000000,fg=#AFFF00]" 18 | 19 | tab_sync_indicator " " 20 | tab_fullscreen_indicator "□ " 21 | tab_floating_indicator "󰉈 " 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/compact.png -------------------------------------------------------------------------------- /examples/conky.conf: -------------------------------------------------------------------------------- 1 | conky.config = { 2 | units_spacer = '', 3 | short_units = true, 4 | out_to_console = true, 5 | total_run_times = 1, 6 | }; 7 | conky.text = [[${uptime_short}${fs_used_perc}% ${fs_free}${loadavg 1}${memperc}% ${memmax} 󰁹${battery_percent}%]] 8 | -------------------------------------------------------------------------------- /examples/conky.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=1 borderless=true { 7 | plugin location="file:target/wasm32-wasi/debug/zjstatus.wasm" { 8 | format_left "{mode}#[fg=black,bg=blue,bold]{session} #[fg=blue,bg=#181825]{tabs}" 9 | format_right "{command_conky}#[fg=#9c86bf,bg=#DCD7BA]{datetime}" 10 | format_space "#[bg=#181825]" 11 | 12 | hide_frame_for_single_pane "true" 13 | 14 | mode_normal "#[bg=blue]" 15 | mode_locked "#[bg=red]" 16 | 17 | tab_normal "#[fg=#181825,bg=#4C4C59] #[fg=#000000,bg=#4C4C59]{index} {name} #[fg=#4C4C59,bg=#181825]" 18 | tab_normal_fullscreen "#[fg=#6C7086,bg=#181825] {index} {name} [] " 19 | tab_normal_sync "#[fg=#6C7086,bg=#181825] {index} {name} <> " 20 | tab_active "#[fg=#181825,bg=#ffffff,bold,italic]{index} {name} #[fg=#ffffff,bg=#181825]" 21 | tab_active_fullscreen "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] " 22 | tab_active_sync "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} <> " 23 | 24 | 25 | // TODO: set this path correctly to the absolute path to the conky.conf 26 | command_conky_command "conky -i1 -c path/to/conky.conf" 27 | command_conky_format "{stdout}" 28 | command_conky_interval "1" 29 | 30 | datetime "#[fg=#000000,bg=#DCD7BA,nobold]{format}" 31 | datetime_format "%d/%m %H:%M" 32 | datetime_timezone "Asia/Taipei" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/conky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/conky.png -------------------------------------------------------------------------------- /examples/simple.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=1 borderless=true { 7 | plugin location="file:target/wasm32-wasi/debug/zjstatus.wasm" { 8 | hide_frame_for_single_pane "true" 9 | 10 | format_left "{mode}#[fg=#89B4FA,bg=#181825,bold] {session}#[bg=#181825] {tabs}" 11 | format_right "{command_kubectx}#[fg=#424554,bg=#181825]::{command_kubens}{datetime}" 12 | format_space "#[bg=#181825]" 13 | 14 | mode_normal "#[bg=#89B4FA] " 15 | mode_tmux "#[bg=#ffc387] " 16 | mode_default_to_mode "tmux" 17 | 18 | tab_normal "#[fg=#6C7086,bg=#181825] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}" 19 | tab_active "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}" 20 | tab_fullscreen_indicator "□ " 21 | tab_sync_indicator " " 22 | tab_floating_indicator "󰉈 " 23 | 24 | command_kubectx_command "kubectx -c" 25 | command_kubectx_format "#[fg=#6C7086,bg=#181825,italic] {stdout}" 26 | command_kubectx_interval "2" 27 | 28 | command_kubens_command "kubens -c" 29 | command_kubens_format "#[fg=#6C7086,bg=#181825]{stdout} " 30 | command_kubens_interval "2" 31 | 32 | datetime "#[fg=#9399B2,bg=#181825] {format} " 33 | datetime_format "%A, %d %b %Y %H:%M" 34 | datetime_timezone "Europe/Berlin" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/simple.png -------------------------------------------------------------------------------- /examples/slanted.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=1 borderless=true { 7 | plugin location="file:target/wasm32-wasi/debug/zjstatus.wasm" { 8 | format_left "{mode}#[fg=black,bg=blue,bold]{session} #[fg=blue,bg=#181825]{tabs}" 9 | format_right "#[fg=#181825,bg=#b1bbfa]{datetime}" 10 | format_space "#[bg=#181825]" 11 | 12 | hide_frame_for_single_pane "true" 13 | 14 | mode_normal "#[bg=blue] " 15 | 16 | tab_normal "#[fg=#181825,bg=#4C4C59] #[fg=#000000,bg=#4C4C59]{index}  {name} #[fg=#4C4C59,bg=#181825]" 17 | tab_normal_fullscreen "#[fg=#6C7086,bg=#181825] {index} {name} [] " 18 | tab_normal_sync "#[fg=#6C7086,bg=#181825] {index} {name} <> " 19 | tab_active "#[fg=#181825,bg=#ffffff,bold,italic] {index}  {name} #[fg=#ffffff,bg=#181825]" 20 | tab_active_fullscreen "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} [] " 21 | tab_active_sync "#[fg=#9399B2,bg=#181825,bold,italic] {index} {name} <> " 22 | 23 | 24 | datetime "#[fg=#6C7086,bg=#b1bbfa,bold] {format} " 25 | datetime_format "%A, %d %b %Y %H:%M" 26 | datetime_timezone "Europe/Berlin" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/slanted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/slanted.png -------------------------------------------------------------------------------- /examples/swap-layouts.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | tab { 3 | pane 4 | } 5 | 6 | swap_tiled_layout name="vertical" { 7 | tab max_panes=5 { 8 | pane split_direction="vertical" { 9 | pane 10 | pane { children; } 11 | } 12 | } 13 | tab max_panes=8 { 14 | pane split_direction="vertical" { 15 | pane { children; } 16 | pane { pane; pane; pane; pane; } 17 | } 18 | } 19 | tab max_panes=12 { 20 | pane split_direction="vertical" { 21 | pane { children; } 22 | pane { pane; pane; pane; pane; } 23 | pane { pane; pane; pane; pane; } 24 | } 25 | } 26 | } 27 | 28 | swap_tiled_layout name="horizontal" { 29 | tab max_panes=5 { 30 | pane 31 | pane 32 | } 33 | tab max_panes=8 { 34 | pane { 35 | pane split_direction="vertical" { children; } 36 | pane split_direction="vertical" { pane; pane; pane; pane; } 37 | } 38 | } 39 | tab max_panes=12 { 40 | pane { 41 | pane split_direction="vertical" { children; } 42 | pane split_direction="vertical" { pane; pane; pane; pane; } 43 | pane split_direction="vertical" { pane; pane; pane; pane; } 44 | } 45 | } 46 | } 47 | 48 | default_tab_template { 49 | children 50 | pane size=1 borderless=true { 51 | plugin location="file:../target/wasm32-wasi/debug/zjstatus.wasm" { 52 | format_left "{mode}{tabs}" 53 | format_right "#[fg=#83a598,bold]{session} #[fg=#b8bb26,bold]{swap_layout}" 54 | 55 | mode_normal "#[fg=#b8bb26,bold]{name}" 56 | mode_locked "#[fg=#fb4934,bold]{name}" 57 | mode_resize "#[fg=#fabd2f,bold]{name}" 58 | mode_pane "#[fg=#d3869b,bold]{name}" 59 | mode_tab "#[fg=#83a598,bold]{name}" 60 | mode_scroll "#[fg=#8ec07c,bold]{name}" 61 | mode_session "#[fg=#fe8019,bold]{name}" 62 | mode_move "#[fg=#a89984,bold]{name}" 63 | 64 | tab_normal "#[fg=#a89984,bold] {name}" 65 | tab_active "#[fg=#83a598,bold] {name}" 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/swap-layouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/swap-layouts.png -------------------------------------------------------------------------------- /examples/tmux.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=1 borderless=true { 7 | plugin location="file:target/wasm32-wasi/debug/zjstatus.wasm" { 8 | format_left "#[fg=0,bg=10][{session}] {tabs}" 9 | format_right "#[fg=0,bg=10]{datetime}" 10 | format_space "#[bg=10]" 11 | 12 | hide_frame_for_single_pane "true" 13 | 14 | tab_normal "{index}:{name} " 15 | tab_active "{index}:{name}* " 16 | 17 | datetime " {format} " 18 | datetime_format "%H:%M %d-%b-%y" 19 | datetime_timezone "Europe/Berlin" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/tmux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dj95/zjstatus/a819e3bfe6bfef0438d811cdbb1bcfdc29912c62/examples/tmux.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "locked": { 5 | "lastModified": 1743700120, 6 | "narHash": "sha256-8BjG/P0xnuCyVOXlYRwdI1B8nVtyYLf3oDwPSimqREY=", 7 | "owner": "ipetkov", 8 | "repo": "crane", 9 | "rev": "e316f19ee058e6db50075115783be57ac549c389", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "ipetkov", 14 | "repo": "crane", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils": { 19 | "inputs": { 20 | "systems": "systems" 21 | }, 22 | "locked": { 23 | "lastModified": 1731533236, 24 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 25 | "owner": "numtide", 26 | "repo": "flake-utils", 27 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs": { 37 | "locked": { 38 | "lastModified": 1743689281, 39 | "narHash": "sha256-y7Hg5lwWhEOgflEHRfzSH96BOt26LaYfrYWzZ+VoVdg=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "2bfc080955153be0be56724be6fa5477b4eefabb", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "NixOS", 47 | "ref": "nixpkgs-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "crane": "crane", 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": [ 63 | "nixpkgs" 64 | ] 65 | }, 66 | "locked": { 67 | "lastModified": 1743682350, 68 | "narHash": "sha256-S/MyKOFajCiBm5H5laoE59wB6w0NJ4wJG53iAPfYW3k=", 69 | "owner": "oxalica", 70 | "repo": "rust-overlay", 71 | "rev": "c4a8327b0f25d1d81edecbb6105f74d7cf9d7382", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "owner": "oxalica", 76 | "repo": "rust-overlay", 77 | "type": "github" 78 | } 79 | }, 80 | "systems": { 81 | "locked": { 82 | "lastModified": 1681028828, 83 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 84 | "owner": "nix-systems", 85 | "repo": "default", 86 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 87 | "type": "github" 88 | }, 89 | "original": { 90 | "owner": "nix-systems", 91 | "repo": "default", 92 | "type": "github" 93 | } 94 | } 95 | }, 96 | "root": "root", 97 | "version": 7 98 | } 99 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A configurable and themable statusbar for zellij."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | 7 | crane = { 8 | url = "github:ipetkov/crane"; 9 | }; 10 | 11 | flake-utils.url = "github:numtide/flake-utils"; 12 | 13 | rust-overlay = { 14 | url = "github:oxalica/rust-overlay"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | }; 18 | 19 | outputs = { self, nixpkgs, crane, flake-utils, rust-overlay, ... }: 20 | flake-utils.lib.eachDefaultSystem (system: 21 | let 22 | pkgs = import nixpkgs { 23 | inherit system; 24 | overlays = [ (import rust-overlay) ]; 25 | }; 26 | 27 | rustWithWasiTarget = pkgs.rust-bin.stable.latest.default.override { 28 | extensions = [ "rust-src" "rust-std" "rust-analyzer" ]; 29 | targets = [ "wasm32-wasip1" ]; 30 | }; 31 | 32 | # NB: we don't need to overlay our custom toolchain for the *entire* 33 | # pkgs (which would require rebuidling anything else which uses rust). 34 | # Instead, we just want to update the scope that crane will use by appending 35 | # our specific toolchain there. 36 | craneLib = (crane.mkLib pkgs).overrideToolchain rustWithWasiTarget; 37 | 38 | zjstatus = craneLib.buildPackage { 39 | src = craneLib.cleanCargoSource (craneLib.path ./.); 40 | 41 | cargoExtraArgs = "--target wasm32-wasip1"; 42 | 43 | # Tests currently need to be run via `cargo wasi` which 44 | # isn't packaged in nixpkgs yet... 45 | doCheck = false; 46 | doNotSign = true; 47 | 48 | buildInputs = [ 49 | # Add additional build inputs here 50 | pkgs.libiconv 51 | ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ 52 | # Additional darwin specific inputs can be set here 53 | ]; 54 | }; 55 | in 56 | { 57 | checks = { 58 | inherit zjstatus; 59 | }; 60 | 61 | packages.default = zjstatus; 62 | 63 | devShells.default = craneLib.devShell { 64 | # Inherit inputs from checks. 65 | checks = self.checks.${system}; 66 | 67 | # Extra inputs can be added here; cargo and rustc are provided by default 68 | # from the toolchain that was specified earlier. 69 | packages = with pkgs; [ 70 | just 71 | rustWithWasiTarget 72 | cargo-audit 73 | cargo-component 74 | cargo-edit 75 | cargo-nextest 76 | cargo-watch 77 | clippy 78 | libiconv 79 | wasmtime 80 | ]; 81 | }; 82 | } 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # Default recipe, when nothing's selected 2 | [private] 3 | default: 4 | just --choose 5 | 6 | # Run benchmarks and compare to previous runs. 7 | bench: 8 | cargo bench --features=bench 9 | 10 | # Build zjstatus with the tracing feature enabled. 11 | build: 12 | cargo build --features tracing 13 | 14 | # Build zjstatus with tracing and start a zellij session with the dev layout. 15 | run target="zjstatus": build 16 | #!/usr/bin/env bash 17 | case "{{target}}" in 18 | "zjframes") 19 | zellij -s zjframes-dev --config ./tests/zjframes/config.kdl -n ./tests/zjframes/layout.kdl 20 | ;; 21 | *) 22 | zellij \ 23 | -s zjstatus-dev \ 24 | --config ./tests/zjstatus/config.kdl \ 25 | -n ./tests/zjstatus/layout.kdl 26 | ;; 27 | esac 28 | 29 | # Watch and run tests with nextest. 30 | test: 31 | cargo watch -x "nextest run --lib" 32 | 33 | # Lint with clippy and cargo audit. 34 | lint: 35 | cargo clippy --all-features --lib 36 | cargo audit 37 | 38 | # Create and push a new release version. 39 | release: 40 | #!/usr/bin/env bash 41 | export VERSION="$( git cliff --bumped-version )" 42 | cargo set-version "${VERSION:1}" 43 | direnv exec . cargo build --release 44 | git commit -am "chore: bump version to $VERSION" 45 | git tag -m "$VERSION" "$VERSION" 46 | git push origin main 47 | git push origin "$VERSION" 48 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86.0" 3 | components = ["clippy", "rust-src", "rust-std", "rust-analyzer"] 4 | targets = ["wasm32-wasip1"] 5 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | let 4 | pinnedPkgs = fetchFromGitHub { 5 | owner = "NixOS"; 6 | repo = "nixpkgs"; 7 | rev = "a63a64b593dcf2fe05f7c5d666eb395950f36bc9"; 8 | sha256 = "sha256-+ZoAny3ZxLcfMaUoLVgL9Ywb/57wP+EtsdNGuXUJrwg="; 9 | }; 10 | 11 | pkgs = import pinnedPkgs {}; 12 | 13 | inherit (lib) optional optionals; 14 | inherit (darwin.apple_sdk.frameworks) Cocoa CoreGraphics Foundation IOKit Kernel OpenGL Security; 15 | in 16 | pkgs.mkShell rec { 17 | buildInputs = with pkgs; [ 18 | clang 19 | # Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16) 20 | llvmPackages.bintools 21 | rustup 22 | libiconv 23 | watchexec 24 | ]; 25 | RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain; 26 | # https://github.com/rust-lang/rust-bindgen#environment-variables 27 | LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; 28 | shellHook = '' 29 | export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin 30 | export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ 31 | ''; 32 | # Add precompiled library to rustc search path 33 | RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [ 34 | # add libraries here (e.g. pkgs.libvmi) 35 | ]); 36 | # Add glibc, clang, glib and other headers to bindgen search path 37 | BINDGEN_EXTRA_CLANG_ARGS = 38 | # Includes with normal include path 39 | (builtins.map (a: ''-I"${a}/include"'') [ 40 | # add dev libraries here (e.g. pkgs.libvmi.dev) 41 | pkgs.libiconv 42 | ]) 43 | # Includes with special directory paths 44 | ++ [ 45 | ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' 46 | ''-I"${pkgs.libiconv.out}/lib/"'' 47 | ]; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/bin/zjframes.rs: -------------------------------------------------------------------------------- 1 | use zellij_tile::prelude::*; 2 | 3 | use std::{collections::BTreeMap, sync::Arc}; 4 | 5 | use zjstatus::frames; 6 | 7 | #[derive(Default, Debug, Clone)] 8 | pub struct ZellijState { 9 | pub mode: ModeInfo, 10 | pub panes: PaneManifest, 11 | pub tabs: Vec, 12 | } 13 | 14 | #[derive(Default)] 15 | struct State { 16 | pending_events: Vec, 17 | got_permissions: bool, 18 | state: ZellijState, 19 | 20 | hide_frame_for_single_pane: bool, 21 | hide_frame_except_for_search: bool, 22 | hide_frame_except_for_fullscreen: bool, 23 | hide_frame_except_for_scroll: bool, 24 | 25 | err: Option, 26 | } 27 | 28 | #[cfg(not(test))] 29 | register_plugin!(State); 30 | 31 | #[cfg(feature = "tracing")] 32 | fn init_tracing() { 33 | use std::fs::File; 34 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 35 | 36 | let file = File::create("/host/.zjframes.log"); 37 | let file = match file { 38 | Ok(file) => file, 39 | Err(error) => panic!("Error: {:?}", error), 40 | }; 41 | let debug_log = tracing_subscriber::fmt::layer().with_writer(Arc::new(file)); 42 | 43 | tracing_subscriber::registry().with(debug_log).init(); 44 | 45 | tracing::info!("tracing initialized"); 46 | } 47 | 48 | impl ZellijPlugin for State { 49 | fn load(&mut self, configuration: BTreeMap) { 50 | #[cfg(feature = "tracing")] 51 | init_tracing(); 52 | 53 | // we need the ReadApplicationState permission to receive the ModeUpdate and TabUpdate 54 | // events 55 | // we need the RunCommands permission to run "cargo test" in a floating window 56 | request_permission(&[ 57 | PermissionType::ReadApplicationState, 58 | PermissionType::ChangeApplicationState, 59 | ]); 60 | 61 | subscribe(&[ 62 | EventType::ModeUpdate, 63 | EventType::PaneUpdate, 64 | EventType::PermissionRequestResult, 65 | EventType::TabUpdate, 66 | EventType::SessionUpdate, 67 | ]); 68 | 69 | self.hide_frame_for_single_pane = match configuration.get("hide_frame_for_single_pane") { 70 | Some(toggle) => toggle == "true", 71 | None => false, 72 | }; 73 | self.hide_frame_except_for_search = match configuration.get("hide_frame_except_for_search") 74 | { 75 | Some(toggle) => toggle == "true", 76 | None => false, 77 | }; 78 | self.hide_frame_except_for_fullscreen = 79 | match configuration.get("hide_frame_except_for_fullscreen") { 80 | Some(toggle) => toggle == "true", 81 | None => false, 82 | }; 83 | self.hide_frame_except_for_scroll= 84 | match configuration.get("hide_frame_except_for_scroll") { 85 | Some(toggle) => toggle == "true", 86 | None => false, 87 | }; 88 | 89 | self.pending_events = Vec::new(); 90 | self.got_permissions = false; 91 | self.state = ZellijState::default(); 92 | } 93 | 94 | #[tracing::instrument(skip_all, fields(event_type))] 95 | fn update(&mut self, event: Event) -> bool { 96 | if let Event::PermissionRequestResult(PermissionStatus::Granted) = event { 97 | self.got_permissions = true; 98 | 99 | while !self.pending_events.is_empty() { 100 | tracing::debug!("processing cached event"); 101 | let ev = self.pending_events.pop(); 102 | 103 | self.handle_event(ev.unwrap()); 104 | } 105 | } 106 | 107 | if !self.got_permissions { 108 | tracing::debug!("caching event"); 109 | self.pending_events.push(event); 110 | 111 | return false; 112 | } 113 | 114 | self.handle_event(event) 115 | } 116 | 117 | #[tracing::instrument(skip_all)] 118 | fn render(&mut self, _rows: usize, _cols: usize) { 119 | if !self.got_permissions { 120 | return; 121 | } 122 | 123 | if let Some(err) = &self.err { 124 | println!("Error: {:?}", err); 125 | 126 | return; 127 | } 128 | 129 | print!("Please load this plugin in the background"); 130 | } 131 | } 132 | 133 | impl State { 134 | fn handle_event(&mut self, event: Event) -> bool { 135 | let mut should_render = false; 136 | match event { 137 | Event::ModeUpdate(mode_info) => { 138 | tracing::Span::current().record("event_type", "Event::ModeUpdate"); 139 | tracing::debug!(mode = ?mode_info.mode); 140 | tracing::debug!(mode = ?mode_info.session_name); 141 | 142 | self.state.mode = mode_info; 143 | 144 | frames::hide_frames_conditionally( 145 | &frames::FrameConfig::new( 146 | self.hide_frame_for_single_pane, 147 | self.hide_frame_except_for_search, 148 | self.hide_frame_except_for_fullscreen, 149 | self.hide_frame_except_for_scroll, 150 | ), 151 | &self.state.tabs, 152 | &self.state.panes, 153 | &self.state.mode, 154 | get_plugin_ids(), 155 | true, 156 | ); 157 | } 158 | Event::PaneUpdate(pane_info) => { 159 | tracing::Span::current().record("event_type", "Event::PaneUpdate"); 160 | tracing::debug!(pane_count = ?pane_info.panes.len()); 161 | 162 | self.state.panes = pane_info; 163 | 164 | frames::hide_frames_conditionally( 165 | &frames::FrameConfig::new( 166 | self.hide_frame_for_single_pane, 167 | self.hide_frame_except_for_search, 168 | self.hide_frame_except_for_fullscreen, 169 | self.hide_frame_except_for_scroll, 170 | ), 171 | &self.state.tabs, 172 | &self.state.panes, 173 | &self.state.mode, 174 | get_plugin_ids(), 175 | true, 176 | ); 177 | } 178 | Event::PermissionRequestResult(result) => { 179 | tracing::Span::current().record("event_type", "Event::PermissionRequestResult"); 180 | tracing::debug!(result = ?result); 181 | set_selectable(false); 182 | should_render = true; 183 | } 184 | Event::SessionUpdate(session_info, _) => { 185 | tracing::Span::current().record("event_type", "Event::SessionUpdate"); 186 | 187 | let current_session = session_info.iter().find(|s| s.is_current_session); 188 | 189 | if let Some(current_session) = current_session { 190 | frames::hide_frames_conditionally( 191 | &frames::FrameConfig::new( 192 | self.hide_frame_for_single_pane, 193 | self.hide_frame_except_for_search, 194 | self.hide_frame_except_for_fullscreen, 195 | self.hide_frame_except_for_scroll, 196 | ), 197 | ¤t_session.tabs, 198 | ¤t_session.panes, 199 | &self.state.mode, 200 | get_plugin_ids(), 201 | true, 202 | ); 203 | } 204 | } 205 | Event::TabUpdate(tab_info) => { 206 | tracing::Span::current().record("event_type", "Event::TabUpdate"); 207 | tracing::debug!(tab_count = ?tab_info.len()); 208 | 209 | self.state.tabs = tab_info; 210 | } 211 | _ => (), 212 | }; 213 | should_render 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/bin/zjstatus.rs: -------------------------------------------------------------------------------- 1 | use zellij_tile::prelude::*; 2 | 3 | use chrono::Local; 4 | use std::{collections::BTreeMap, sync::Arc}; 5 | use uuid::Uuid; 6 | 7 | use zjstatus::{ 8 | config::{self, ModuleConfig, UpdateEventMask, ZellijState}, 9 | frames, pipe, 10 | widgets::{ 11 | command::{CommandResult, CommandWidget}, 12 | datetime::DateTimeWidget, 13 | mode::ModeWidget, 14 | notification::NotificationWidget, 15 | pipe::PipeWidget, 16 | session::SessionWidget, 17 | swap_layout::SwapLayoutWidget, 18 | tabs::TabsWidget, 19 | widget::Widget, 20 | }, 21 | }; 22 | 23 | #[derive(Default)] 24 | struct State { 25 | pending_events: Vec, 26 | got_permissions: bool, 27 | state: ZellijState, 28 | userspace_configuration: BTreeMap, 29 | module_config: config::ModuleConfig, 30 | widget_map: BTreeMap>, 31 | err: Option, 32 | } 33 | 34 | #[cfg(not(test))] 35 | register_plugin!(State); 36 | 37 | #[cfg(feature = "tracing")] 38 | fn init_tracing() { 39 | use std::fs::File; 40 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 41 | 42 | let file = File::create("/host/.zjstatus.log"); 43 | let file = match file { 44 | Ok(file) => file, 45 | Err(error) => panic!("Error: {:?}", error), 46 | }; 47 | let debug_log = tracing_subscriber::fmt::layer().with_writer(Arc::new(file)); 48 | 49 | tracing_subscriber::registry().with(debug_log).init(); 50 | 51 | tracing::info!("tracing initialized"); 52 | } 53 | 54 | impl ZellijPlugin for State { 55 | fn load(&mut self, configuration: BTreeMap) { 56 | #[cfg(feature = "tracing")] 57 | init_tracing(); 58 | 59 | // we need the ReadApplicationState permission to receive the ModeUpdate and TabUpdate 60 | // events 61 | // we need the RunCommands permission to run "cargo test" in a floating window 62 | request_permission(&[ 63 | PermissionType::ReadApplicationState, 64 | PermissionType::ChangeApplicationState, 65 | PermissionType::RunCommands, 66 | ]); 67 | 68 | subscribe(&[ 69 | EventType::Mouse, 70 | EventType::ModeUpdate, 71 | EventType::PaneUpdate, 72 | EventType::PermissionRequestResult, 73 | EventType::TabUpdate, 74 | EventType::SessionUpdate, 75 | EventType::RunCommandResult, 76 | ]); 77 | 78 | self.module_config = match ModuleConfig::new(&configuration) { 79 | Ok(mc) => mc, 80 | Err(e) => { 81 | self.err = Some(e); 82 | return; 83 | } 84 | }; 85 | self.widget_map = register_widgets(&configuration); 86 | self.userspace_configuration = configuration; 87 | self.pending_events = Vec::new(); 88 | self.got_permissions = false; 89 | let uid = Uuid::new_v4(); 90 | 91 | self.state = ZellijState { 92 | cols: 0, 93 | command_results: BTreeMap::new(), 94 | pipe_results: BTreeMap::new(), 95 | mode: ModeInfo::default(), 96 | panes: PaneManifest::default(), 97 | plugin_uuid: uid.to_string(), 98 | tabs: Vec::new(), 99 | sessions: Vec::new(), 100 | start_time: Local::now(), 101 | cache_mask: 0, 102 | incoming_notification: None, 103 | }; 104 | } 105 | 106 | fn pipe(&mut self, pipe_message: PipeMessage) -> bool { 107 | let mut should_render = false; 108 | 109 | match pipe_message.source { 110 | PipeSource::Cli(_) => { 111 | if let Some(input) = pipe_message.payload { 112 | should_render = pipe::parse_protocol(&mut self.state, &input); 113 | } 114 | } 115 | PipeSource::Plugin(_) => { 116 | if let Some(input) = pipe_message.payload { 117 | should_render = pipe::parse_protocol(&mut self.state, &input); 118 | } 119 | } 120 | PipeSource::Keybind => { 121 | if let Some(input) = pipe_message.payload { 122 | should_render = pipe::parse_protocol(&mut self.state, &input); 123 | } 124 | } 125 | } 126 | 127 | should_render 128 | } 129 | 130 | #[tracing::instrument(skip_all, fields(event_type))] 131 | fn update(&mut self, event: Event) -> bool { 132 | if let Event::PermissionRequestResult(PermissionStatus::Granted) = event { 133 | self.got_permissions = true; 134 | 135 | while !self.pending_events.is_empty() { 136 | tracing::debug!("processing cached event"); 137 | let ev = self.pending_events.pop(); 138 | 139 | self.handle_event(ev.unwrap()); 140 | } 141 | } 142 | 143 | if !self.got_permissions { 144 | tracing::debug!("caching event"); 145 | self.pending_events.push(event); 146 | 147 | return false; 148 | } 149 | 150 | self.handle_event(event) 151 | } 152 | 153 | #[tracing::instrument(skip_all)] 154 | fn render(&mut self, _rows: usize, cols: usize) { 155 | if !self.got_permissions { 156 | return; 157 | } 158 | 159 | if let Some(err) = &self.err { 160 | println!("Error: {:?}", err); 161 | 162 | return; 163 | } 164 | 165 | self.state.cols = cols; 166 | 167 | tracing::debug!("{:?}", self.state.mode.session_name); 168 | 169 | let output = self 170 | .module_config 171 | .render_bar(self.state.clone(), self.widget_map.clone()); 172 | 173 | print!("{}", output); 174 | } 175 | } 176 | 177 | impl State { 178 | fn handle_event(&mut self, event: Event) -> bool { 179 | let mut should_render = false; 180 | match event { 181 | Event::Mouse(mouse_info) => { 182 | tracing::Span::current().record("event_type", "Event::Mouse"); 183 | tracing::debug!(mouse = ?mouse_info); 184 | 185 | self.module_config.handle_mouse_action( 186 | self.state.clone(), 187 | mouse_info, 188 | self.widget_map.clone(), 189 | ); 190 | } 191 | Event::ModeUpdate(mode_info) => { 192 | tracing::Span::current().record("event_type", "Event::ModeUpdate"); 193 | tracing::debug!(mode = ?mode_info.mode); 194 | tracing::debug!(mode = ?mode_info.session_name); 195 | 196 | self.state.mode = mode_info; 197 | self.state.cache_mask = UpdateEventMask::Mode as u8; 198 | 199 | should_render = true; 200 | } 201 | Event::PaneUpdate(pane_info) => { 202 | tracing::Span::current().record("event_type", "Event::PaneUpdate"); 203 | tracing::debug!(pane_count = ?pane_info.panes.len()); 204 | 205 | frames::hide_frames_conditionally( 206 | &frames::FrameConfig::new( 207 | self.module_config.hide_frame_for_single_pane, 208 | self.module_config.hide_frame_except_for_search, 209 | self.module_config.hide_frame_except_for_fullscreen, 210 | self.module_config.hide_frame_except_for_scroll, 211 | ), 212 | &self.state.tabs, 213 | &pane_info, 214 | &self.state.mode, 215 | get_plugin_ids(), 216 | false, 217 | ); 218 | 219 | self.state.panes = pane_info; 220 | self.state.cache_mask = UpdateEventMask::Tab as u8; 221 | 222 | should_render = true; 223 | } 224 | Event::PermissionRequestResult(result) => { 225 | tracing::Span::current().record("event_type", "Event::PermissionRequestResult"); 226 | tracing::debug!(result = ?result); 227 | set_selectable(false); 228 | } 229 | Event::RunCommandResult(exit_code, stdout, stderr, context) => { 230 | tracing::Span::current().record("event_type", "Event::RunCommandResult"); 231 | tracing::debug!( 232 | exit_code = ?exit_code, 233 | stdout = ?String::from_utf8(stdout.clone()), 234 | stderr = ?String::from_utf8(stderr.clone()), 235 | context = ?context 236 | ); 237 | 238 | self.state.cache_mask = UpdateEventMask::Command as u8; 239 | 240 | if let Some(name) = context.get("name") { 241 | let stdout = match String::from_utf8(stdout) { 242 | Ok(s) => s, 243 | Err(_) => "".to_owned(), 244 | }; 245 | 246 | let stderr = match String::from_utf8(stderr) { 247 | Ok(s) => s, 248 | Err(_) => "".to_owned(), 249 | }; 250 | 251 | self.state.command_results.insert( 252 | name.to_owned(), 253 | CommandResult { 254 | exit_code, 255 | stdout, 256 | stderr, 257 | context, 258 | }, 259 | ); 260 | } 261 | } 262 | Event::SessionUpdate(session_info, _) => { 263 | tracing::Span::current().record("event_type", "Event::SessionUpdate"); 264 | 265 | let current_session = session_info.iter().find(|s| s.is_current_session); 266 | 267 | if let Some(current_session) = current_session { 268 | frames::hide_frames_conditionally( 269 | &frames::FrameConfig::new( 270 | self.module_config.hide_frame_for_single_pane, 271 | self.module_config.hide_frame_except_for_search, 272 | self.module_config.hide_frame_except_for_fullscreen, 273 | self.module_config.hide_frame_except_for_scroll, 274 | ), 275 | ¤t_session.tabs, 276 | ¤t_session.panes, 277 | &self.state.mode, 278 | get_plugin_ids(), 279 | false, 280 | ); 281 | } 282 | 283 | self.state.sessions = session_info; 284 | self.state.cache_mask = UpdateEventMask::Session as u8; 285 | 286 | should_render = true; 287 | } 288 | Event::TabUpdate(tab_info) => { 289 | tracing::Span::current().record("event_type", "Event::TabUpdate"); 290 | tracing::debug!(tab_count = ?tab_info.len()); 291 | 292 | self.state.cache_mask = UpdateEventMask::Tab as u8; 293 | self.state.tabs = tab_info; 294 | 295 | should_render = true; 296 | } 297 | _ => (), 298 | }; 299 | should_render 300 | } 301 | } 302 | 303 | fn register_widgets(configuration: &BTreeMap) -> BTreeMap> { 304 | let mut widget_map = BTreeMap::>::new(); 305 | 306 | widget_map.insert( 307 | "command".to_owned(), 308 | Arc::new(CommandWidget::new(configuration)), 309 | ); 310 | widget_map.insert( 311 | "datetime".to_owned(), 312 | Arc::new(DateTimeWidget::new(configuration)), 313 | ); 314 | widget_map.insert("pipe".to_owned(), Arc::new(PipeWidget::new(configuration))); 315 | widget_map.insert( 316 | "swap_layout".to_owned(), 317 | Arc::new(SwapLayoutWidget::new(configuration)), 318 | ); 319 | widget_map.insert("mode".to_owned(), Arc::new(ModeWidget::new(configuration))); 320 | widget_map.insert( 321 | "session".to_owned(), 322 | Arc::new(SessionWidget::new(configuration)), 323 | ); 324 | widget_map.insert("tabs".to_owned(), Arc::new(TabsWidget::new(configuration))); 325 | widget_map.insert( 326 | "notifications".to_owned(), 327 | Arc::new(NotificationWidget::new(configuration)), 328 | ); 329 | 330 | tracing::debug!("registered widgets: {:?}", widget_map.keys()); 331 | 332 | widget_map 333 | } 334 | -------------------------------------------------------------------------------- /src/border.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::render::FormattedPart; 4 | 5 | const DEFAULT_CHAR: &str = "─"; 6 | 7 | #[derive(Default, PartialEq, Debug)] 8 | pub enum BorderPosition { 9 | #[default] 10 | Top, 11 | Bottom, 12 | } 13 | 14 | #[derive(Debug)] 15 | pub struct BorderConfig { 16 | pub enabled: bool, 17 | pub char: String, 18 | pub format: FormattedPart, 19 | pub position: BorderPosition, 20 | } 21 | 22 | impl Default for BorderConfig { 23 | fn default() -> Self { 24 | Self { 25 | enabled: false, 26 | char: DEFAULT_CHAR.to_owned(), 27 | format: FormattedPart::default(), 28 | position: BorderPosition::default(), 29 | } 30 | } 31 | } 32 | 33 | impl BorderConfig { 34 | pub fn draw(&self, cols: usize) -> String { 35 | self.format.format_string(&self.char.repeat(cols)) 36 | } 37 | } 38 | 39 | pub fn parse_border_config(config: &BTreeMap) -> Option { 40 | let enabled = match config.get("border_enabled") { 41 | Some(e) => matches!(e.as_str(), "true"), 42 | None => { 43 | return None; 44 | } 45 | }; 46 | 47 | let char = match config.get("border_char") { 48 | Some(bc) => bc, 49 | None => DEFAULT_CHAR, 50 | }; 51 | 52 | let format = match config.get("border_format") { 53 | Some(bfs) => bfs, 54 | None => "", 55 | }; 56 | 57 | let position = match config.get("border_position") { 58 | Some(pos) => match pos.to_owned().as_str() { 59 | "bottom" => BorderPosition::Bottom, 60 | _ => BorderPosition::Top, 61 | }, 62 | None => BorderPosition::Top, 63 | }; 64 | 65 | Some(BorderConfig { 66 | enabled, 67 | char: char.to_owned(), 68 | format: FormattedPart::from_format_string(format, config), 69 | position, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, str::FromStr, sync::Arc}; 2 | 3 | use itertools::Itertools; 4 | use regex::Regex; 5 | use zellij_tile::prelude::*; 6 | 7 | use crate::{ 8 | border::{parse_border_config, BorderConfig, BorderPosition}, 9 | render::FormattedPart, 10 | widgets::{command::CommandResult, notification, widget::Widget}, 11 | }; 12 | use chrono::{DateTime, Local}; 13 | 14 | #[derive(Default, Debug, Clone)] 15 | pub struct ZellijState { 16 | pub cols: usize, 17 | pub command_results: BTreeMap, 18 | pub pipe_results: BTreeMap, 19 | pub mode: ModeInfo, 20 | pub panes: PaneManifest, 21 | pub plugin_uuid: String, 22 | pub tabs: Vec, 23 | pub sessions: Vec, 24 | pub start_time: DateTime, 25 | pub incoming_notification: Option, 26 | pub cache_mask: u8, 27 | } 28 | 29 | #[derive(Clone, Debug, Ord, Eq, PartialEq, PartialOrd, Copy)] 30 | pub enum Part { 31 | Left, 32 | Center, 33 | Right, 34 | } 35 | 36 | impl FromStr for Part { 37 | fn from_str(part: &str) -> Result { 38 | match part { 39 | "l" => Ok(Part::Left), 40 | "c" => Ok(Part::Center), 41 | "r" => Ok(Part::Right), 42 | _ => anyhow::bail!("Invalid part: {}", part), 43 | } 44 | } 45 | 46 | type Err = anyhow::Error; 47 | } 48 | 49 | pub enum UpdateEventMask { 50 | Always = 0b10000000, 51 | Mode = 0b00000001, 52 | Tab = 0b00000011, 53 | Command = 0b00000100, 54 | Session = 0b00001000, 55 | None = 0b00000000, 56 | } 57 | 58 | pub fn event_mask_from_widget_name(name: &str) -> u8 { 59 | match name { 60 | "command" => UpdateEventMask::Always as u8, 61 | "datetime" => UpdateEventMask::Always as u8, 62 | "mode" => UpdateEventMask::Mode as u8, 63 | "notifications" => UpdateEventMask::Always as u8, 64 | "session" => UpdateEventMask::Mode as u8, 65 | "swap_layout" => UpdateEventMask::Tab as u8, 66 | "tabs" => UpdateEventMask::Tab as u8, 67 | "pipe" => UpdateEventMask::Always as u8, 68 | _ => UpdateEventMask::None as u8, 69 | } 70 | } 71 | 72 | #[derive(Default, Debug)] 73 | pub struct ModuleConfig { 74 | pub left_parts_config: String, 75 | pub left_parts: Vec, 76 | pub center_parts_config: String, 77 | pub center_parts: Vec, 78 | pub right_parts_config: String, 79 | pub right_parts: Vec, 80 | pub format_space: FormattedPart, 81 | pub hide_frame_for_single_pane: bool, 82 | pub hide_frame_except_for_search: bool, 83 | pub hide_frame_except_for_fullscreen: bool, 84 | pub hide_frame_except_for_scroll: bool, 85 | pub border: BorderConfig, 86 | pub format_precedence: Vec, 87 | pub hide_on_overlength: bool, 88 | } 89 | 90 | impl ModuleConfig { 91 | pub fn new(config: &BTreeMap) -> anyhow::Result { 92 | let format_space_config = match config.get("format_space") { 93 | Some(space_config) => space_config, 94 | None => "", 95 | }; 96 | 97 | let hide_frame_for_single_pane = match config.get("hide_frame_for_single_pane") { 98 | Some(toggle) => toggle == "true", 99 | None => false, 100 | }; 101 | let hide_frame_except_for_search = match config.get("hide_frame_except_for_search") { 102 | Some(toggle) => toggle == "true", 103 | None => false, 104 | }; 105 | let hide_frame_except_for_fullscreen = match config.get("hide_frame_except_for_fullscreen") 106 | { 107 | Some(toggle) => toggle == "true", 108 | None => false, 109 | }; 110 | let hide_frame_except_for_scroll = match config.get("hide_frame_except_for_scroll") { 111 | Some(toggle) => toggle == "true", 112 | None => false, 113 | }; 114 | 115 | let left_parts_config = match config.get("format_left") { 116 | Some(conf) => conf, 117 | None => "", 118 | }; 119 | 120 | let right_parts_config = match config.get("format_right") { 121 | Some(conf) => conf, 122 | None => "", 123 | }; 124 | 125 | let center_parts_config = match config.get("format_center") { 126 | Some(conf) => conf, 127 | None => "", 128 | }; 129 | 130 | let format_precedence = match config.get("format_precedence") { 131 | Some(conf) => { 132 | let prec = conf 133 | .chars() 134 | .map(|c| Part::from_str(&c.to_string())) 135 | .collect(); 136 | 137 | match prec { 138 | Ok(prec) => prec, 139 | Err(e) => { 140 | anyhow::bail!("Invalid format_precedence: {}", e); 141 | } 142 | } 143 | } 144 | None => vec![Part::Left, Part::Center, Part::Right], 145 | }; 146 | 147 | let hide_on_overlength = match config.get("format_hide_on_overlength") { 148 | Some(opt) => opt == "true", 149 | None => false, 150 | }; 151 | 152 | let border_config = parse_border_config(config).unwrap_or_default(); 153 | 154 | Ok(Self { 155 | left_parts_config: left_parts_config.to_owned(), 156 | left_parts: parts_from_config(Some(&left_parts_config.to_owned()), config), 157 | center_parts_config: center_parts_config.to_owned(), 158 | center_parts: parts_from_config(Some(¢er_parts_config.to_owned()), config), 159 | right_parts_config: right_parts_config.to_owned(), 160 | right_parts: parts_from_config(Some(&right_parts_config.to_owned()), config), 161 | format_space: FormattedPart::from_format_string(format_space_config, config), 162 | hide_frame_for_single_pane, 163 | hide_frame_except_for_search, 164 | hide_frame_except_for_fullscreen, 165 | hide_frame_except_for_scroll, 166 | border: border_config, 167 | format_precedence, 168 | hide_on_overlength, 169 | }) 170 | } 171 | 172 | pub fn handle_mouse_action( 173 | &mut self, 174 | state: ZellijState, 175 | mouse: Mouse, 176 | widget_map: BTreeMap>, 177 | ) { 178 | let click_pos = match mouse { 179 | Mouse::ScrollUp(_) => return, 180 | Mouse::ScrollDown(_) => return, 181 | Mouse::LeftClick(_, y) => y, 182 | Mouse::RightClick(_, y) => y, 183 | Mouse::Hold(_, y) => y, 184 | Mouse::Release(_, y) => y, 185 | Mouse::Hover(_, _) => return, 186 | }; 187 | 188 | let output_left = self.left_parts.iter_mut().fold("".to_owned(), |acc, part| { 189 | format!( 190 | "{}{}", 191 | acc, 192 | part.format_string_with_widgets(&widget_map, &state) 193 | ) 194 | }); 195 | 196 | let output_center = self 197 | .center_parts 198 | .iter_mut() 199 | .fold("".to_owned(), |acc, part| { 200 | format!( 201 | "{}{}", 202 | acc, 203 | part.format_string_with_widgets(&widget_map, &state) 204 | ) 205 | }); 206 | 207 | let output_right = self 208 | .right_parts 209 | .iter_mut() 210 | .fold("".to_owned(), |acc, part| { 211 | format!( 212 | "{}{}", 213 | acc, 214 | part.format_string_with_widgets(&widget_map, &state) 215 | ) 216 | }); 217 | 218 | let (output_left, output_center, output_right) = match self.hide_on_overlength { 219 | true => self.trim_output(&output_left, &output_center, &output_right, state.cols), 220 | false => (output_left, output_center, output_right), 221 | }; 222 | 223 | let mut offset = console::measure_text_width(&output_left); 224 | 225 | self.process_widget_click(click_pos, &self.left_parts, &widget_map, &state, 0); 226 | 227 | if click_pos <= offset { 228 | return; 229 | } 230 | 231 | if !output_center.is_empty() { 232 | tracing::debug!("widgetclick center"); 233 | offset += console::measure_text_width(&self.get_spacer_left( 234 | &output_left, 235 | &output_center, 236 | state.cols, 237 | )); 238 | 239 | offset += self.process_widget_click( 240 | click_pos, 241 | &self.center_parts, 242 | &widget_map, 243 | &state, 244 | offset, 245 | ); 246 | 247 | if click_pos <= offset { 248 | return; 249 | } 250 | 251 | offset += console::measure_text_width(&self.get_spacer_right( 252 | &output_right, 253 | &output_center, 254 | state.cols, 255 | )); 256 | } else { 257 | offset += console::measure_text_width(&self.get_spacer( 258 | &output_left, 259 | &output_right, 260 | state.cols, 261 | )); 262 | } 263 | 264 | self.process_widget_click(click_pos, &self.right_parts, &widget_map, &state, offset); 265 | } 266 | 267 | fn process_widget_click( 268 | &self, 269 | click_pos: usize, 270 | widgets: &[FormattedPart], 271 | widget_map: &BTreeMap>, 272 | state: &ZellijState, 273 | offset: usize, 274 | ) -> usize { 275 | let widget_string = widgets.iter().fold(String::new(), |a, b| a + &b.content); 276 | 277 | let mut rendered_output = widget_string.clone(); 278 | 279 | let tokens: Vec = widget_map.keys().map(|k| k.to_owned()).collect(); 280 | 281 | let widgets_regex = Regex::new("(\\{[a-z_0-9]+\\})").unwrap(); 282 | for widget in widgets_regex.captures_iter(widget_string.as_str()) { 283 | let match_name = widget.get(0).unwrap().as_str(); 284 | let widget_key = match_name.trim_matches(|c| c == '{' || c == '}'); 285 | let mut widget_key_name = widget_key; 286 | 287 | if widget_key.starts_with("command_") { 288 | widget_key_name = "command"; 289 | } 290 | 291 | if widget_key.starts_with("pipe_") { 292 | widget_key_name = "pipe"; 293 | } 294 | 295 | if !tokens.contains(&widget_key_name.to_owned()) { 296 | continue; 297 | } 298 | 299 | let wid = match widget_map.get(widget_key_name) { 300 | Some(wid) => wid, 301 | None => continue, 302 | }; 303 | 304 | let pos = match rendered_output.find(match_name) { 305 | Some(_pos) => { 306 | let pref = rendered_output.split(match_name).collect::>()[0]; 307 | console::measure_text_width(pref) 308 | } 309 | None => continue, 310 | }; 311 | 312 | let wid_res = wid.process(widget_key, state); 313 | rendered_output = rendered_output.replace(match_name, &wid_res); 314 | 315 | if click_pos < pos + offset 316 | || click_pos > pos + offset + console::measure_text_width(&wid_res) 317 | { 318 | continue; 319 | } 320 | 321 | wid.process_click(widget_key, state, click_pos - (pos + offset)); 322 | } 323 | 324 | console::measure_text_width(&rendered_output) 325 | } 326 | 327 | pub fn render_bar( 328 | &mut self, 329 | state: ZellijState, 330 | widget_map: BTreeMap>, 331 | ) -> String { 332 | if self.left_parts.is_empty() && self.center_parts.is_empty() && self.right_parts.is_empty() 333 | { 334 | return "No configuration found. See https://github.com/dj95/zjstatus/wiki/3-%E2%80%90-Configuration for more info".to_string(); 335 | } 336 | 337 | let output_left = self.left_parts.iter_mut().fold("".to_owned(), |acc, part| { 338 | format!( 339 | "{acc}{}", 340 | part.format_string_with_widgets(&widget_map, &state) 341 | ) 342 | }); 343 | 344 | let output_center = self 345 | .center_parts 346 | .iter_mut() 347 | .fold("".to_owned(), |acc, part| { 348 | format!( 349 | "{acc}{}", 350 | part.format_string_with_widgets(&widget_map, &state) 351 | ) 352 | }); 353 | 354 | let output_right = self 355 | .right_parts 356 | .iter_mut() 357 | .fold("".to_owned(), |acc, part| { 358 | format!( 359 | "{acc}{}", 360 | part.format_string_with_widgets(&widget_map, &state) 361 | ) 362 | }); 363 | 364 | let (output_left, output_center, output_right) = match self.hide_on_overlength { 365 | true => self.trim_output(&output_left, &output_center, &output_right, state.cols), 366 | false => (output_left, output_center, output_right), 367 | }; 368 | 369 | if self.border.enabled { 370 | let mut border_top = "".to_owned(); 371 | if self.border.enabled && self.border.position == BorderPosition::Top { 372 | border_top = format!("{}\n", self.border.draw(state.cols)); 373 | } 374 | 375 | let mut border_bottom = "".to_owned(); 376 | if self.border.enabled && self.border.position == BorderPosition::Bottom { 377 | border_bottom = format!("\n{}", self.border.draw(state.cols)); 378 | } 379 | 380 | if !output_center.is_empty() { 381 | return format!( 382 | "{}{}{}{}{}{}{}", 383 | border_top, 384 | output_left, 385 | self.get_spacer_left(&output_left, &output_center, state.cols), 386 | output_center, 387 | self.get_spacer_right(&output_right, &output_center, state.cols), 388 | output_right, 389 | border_bottom, 390 | ); 391 | } 392 | 393 | return format!( 394 | "{}{}{}{}{}", 395 | border_top, 396 | output_left, 397 | self.get_spacer(&output_left, &output_right, state.cols), 398 | output_right, 399 | border_bottom, 400 | ); 401 | } 402 | 403 | if !output_center.is_empty() { 404 | return format!( 405 | "{}{}{}{}{}", 406 | output_left, 407 | self.get_spacer_left(&output_left, &output_center, state.cols), 408 | output_center, 409 | self.get_spacer_right(&output_right, &output_center, state.cols), 410 | output_right, 411 | ); 412 | } 413 | 414 | format!( 415 | "{}{}{}", 416 | output_left, 417 | self.get_spacer(&output_left, &output_right, state.cols), 418 | output_right, 419 | ) 420 | } 421 | 422 | fn trim_output( 423 | &self, 424 | output_left: &str, 425 | output_center: &str, 426 | output_right: &str, 427 | cols: usize, 428 | ) -> (String, String, String) { 429 | let center_pos = (cols as f32 / 2.0).floor() as usize; 430 | 431 | let mut output = BTreeMap::from([ 432 | (Part::Left, output_left.to_owned()), 433 | (Part::Center, output_center.to_owned()), 434 | (Part::Right, output_right.to_owned()), 435 | ]); 436 | 437 | let combinations = [ 438 | (self.format_precedence[2], self.format_precedence[1]), 439 | (self.format_precedence[1], self.format_precedence[0]), 440 | (self.format_precedence[2], self.format_precedence[0]), 441 | ]; 442 | 443 | for win in combinations.iter() { 444 | let (a, b) = win; 445 | 446 | let part_a = output.get(a).unwrap(); 447 | let part_b = output.get(b).unwrap(); 448 | 449 | let a_count = console::measure_text_width(part_a); 450 | let b_count = console::measure_text_width(part_b); 451 | 452 | let overlap = match (a, b) { 453 | (Part::Left, Part::Right) => a_count + b_count > cols, 454 | (Part::Right, Part::Left) => a_count + b_count > cols, 455 | (Part::Left, Part::Center) => a_count > center_pos - (b_count / 2), 456 | (Part::Center, Part::Left) => b_count > center_pos - (a_count / 2), 457 | (Part::Right, Part::Center) => a_count > center_pos - (b_count / 2), 458 | (Part::Center, Part::Right) => b_count > center_pos - (a_count / 2), 459 | _ => false, 460 | }; 461 | 462 | if overlap { 463 | output.insert(*a, "".to_owned()); 464 | } 465 | } 466 | 467 | output.values().cloned().collect_tuple().unwrap() 468 | } 469 | 470 | #[tracing::instrument(skip_all)] 471 | fn get_spacer_left(&self, output_left: &str, output_center: &str, cols: usize) -> String { 472 | let text_count = console::measure_text_width(output_left) 473 | + (console::measure_text_width(output_center) as f32 / 2.0).floor() as usize; 474 | 475 | let center_pos = (cols as f32 / 2.0).floor() as usize; 476 | 477 | // verify we are able to count the difference, since zellij sometimes drops a col 478 | // count of 0 on tab creation 479 | let space_count = center_pos.saturating_sub(text_count); 480 | 481 | tracing::debug!("space_count: {:?}", space_count); 482 | self.format_space.format_string(&" ".repeat(space_count)) 483 | } 484 | 485 | #[tracing::instrument(skip_all)] 486 | fn get_spacer_right(&self, output_right: &str, output_center: &str, cols: usize) -> String { 487 | let text_count = console::measure_text_width(output_right) 488 | + (console::measure_text_width(output_center) as f32 / 2.0).ceil() as usize; 489 | 490 | let center_pos = (cols as f32 / 2.0).ceil() as usize; 491 | 492 | // verify we are able to count the difference, since zellij sometimes drops a col 493 | // count of 0 on tab creation 494 | let space_count = center_pos.saturating_sub(text_count); 495 | 496 | tracing::debug!("space_count: {:?}", space_count); 497 | self.format_space.format_string(&" ".repeat(space_count)) 498 | } 499 | 500 | fn get_spacer(&self, output_left: &str, output_right: &str, cols: usize) -> String { 501 | let text_count = 502 | console::measure_text_width(output_left) + console::measure_text_width(output_right); 503 | 504 | // verify we are able to count the difference, since zellij sometimes drops a col 505 | // count of 0 on tab creation 506 | let space_count = cols.saturating_sub(text_count); 507 | 508 | self.format_space.format_string(&" ".repeat(space_count)) 509 | } 510 | } 511 | 512 | fn parts_from_config( 513 | format: Option<&String>, 514 | config: &BTreeMap, 515 | ) -> Vec { 516 | match format { 517 | Some(format) => match format.is_empty() { 518 | true => vec![], 519 | false => format 520 | .split("#[") 521 | .map(|s| FormattedPart::from_format_string(s, config)) 522 | .collect(), 523 | }, 524 | None => vec![], 525 | } 526 | } 527 | 528 | #[cfg(test)] 529 | mod test { 530 | use super::*; 531 | use anstyle::{Effects, RgbColor}; 532 | 533 | #[test] 534 | fn test_formatted_part_from_string() { 535 | let input = "#[fg=#ff0000,bg=#00ff00,bold,italic]foo"; 536 | 537 | let part = FormattedPart::from_format_string(input, &BTreeMap::new()); 538 | 539 | assert_eq!( 540 | part, 541 | FormattedPart { 542 | fg: Some(RgbColor(255, 0, 0).into()), 543 | bg: Some(RgbColor(0, 255, 0).into()), 544 | effects: Effects::BOLD | Effects::ITALIC, 545 | content: "foo".to_owned(), 546 | ..Default::default() 547 | }, 548 | ) 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /src/frames.rs: -------------------------------------------------------------------------------- 1 | use zellij_tile::prelude::*; 2 | 3 | pub struct FrameConfig { 4 | pub hide_frames_for_single_pane: bool, 5 | pub hide_frames_except_for_search: bool, 6 | pub hide_frames_except_for_fullscreen: bool, 7 | pub hide_frames_except_for_scroll: bool, 8 | } 9 | 10 | impl FrameConfig { 11 | pub fn new( 12 | hide_frames_for_single_pane: bool, 13 | hide_frames_except_for_search: bool, 14 | hide_frames_except_for_fullscreen: bool, 15 | hide_frames_except_for_scroll: bool, 16 | ) -> Self { 17 | Self { 18 | hide_frames_for_single_pane, 19 | hide_frames_except_for_search, 20 | hide_frames_except_for_fullscreen, 21 | hide_frames_except_for_scroll, 22 | } 23 | } 24 | 25 | pub fn is_disabled(&self) -> bool { 26 | !self.hide_frames_for_single_pane 27 | && !self.hide_frames_except_for_search 28 | && !self.hide_frames_except_for_fullscreen 29 | && !self.hide_frames_except_for_scroll 30 | } 31 | } 32 | 33 | #[tracing::instrument(skip_all)] 34 | pub fn hide_frames_conditionally( 35 | config: &FrameConfig, 36 | tabs: &[TabInfo], 37 | pane_info: &PaneManifest, 38 | mode_info: &ModeInfo, 39 | plugin_pane_id: PluginIds, 40 | is_zjframes: bool, 41 | ) { 42 | if config.is_disabled() { 43 | return; 44 | } 45 | 46 | let panes = match get_current_panes(tabs, pane_info) { 47 | Some(panes) => panes, 48 | None => return, 49 | }; 50 | 51 | // check if we are running for the current tab since one plugin will run for 52 | // each tab. If we do not prevent execution, the screen will start to flicker 53 | // 'cause every plugin will try to toggle the frames. 54 | // 55 | // This is only relevant for zjstatus, not zjframes; as zjframes only 56 | // runs once per session. 57 | if !is_plugin_for_current_tab(&panes, plugin_pane_id) && !is_zjframes { 58 | return; 59 | } 60 | 61 | let panes: Vec<&PaneInfo> = panes 62 | .iter() 63 | .filter(|p| !p.is_plugin && !p.is_floating) 64 | .collect(); 65 | 66 | tracing::debug!("panes: {:?}", panes); 67 | let frame_enabled = panes 68 | .iter() 69 | .filter(|&&p| !p.is_suppressed) 70 | .any(|p| p.pane_content_x - p.pane_x > 0); 71 | 72 | let frames_for_search = 73 | config.hide_frames_except_for_search && should_show_frames_for_search(mode_info); 74 | let frames_for_fullscreen = 75 | config.hide_frames_except_for_fullscreen && should_show_frames_for_fullscreen(&panes); 76 | let frames_for_single_pane = config.hide_frames_for_single_pane 77 | && should_show_frames_for_multiple_panes(mode_info, &panes); 78 | let frames_for_scroll = 79 | config.hide_frames_except_for_scroll && should_show_frames_for_scroll(mode_info); 80 | 81 | tracing::debug!( 82 | "search {:?} fullscreen {:?} single {:?} scroll {:?} actual {:?}", 83 | frames_for_search, 84 | frames_for_fullscreen, 85 | frames_for_single_pane, 86 | frames_for_scroll, 87 | frame_enabled, 88 | ); 89 | 90 | if (frames_for_search || frames_for_fullscreen || frames_for_single_pane || frames_for_scroll) 91 | && !frame_enabled 92 | { 93 | tracing::debug!("activate"); 94 | toggle_pane_frames(); 95 | } 96 | 97 | if (!frames_for_search 98 | && !frames_for_fullscreen 99 | && !frames_for_single_pane 100 | && !frames_for_scroll) 101 | && frame_enabled 102 | { 103 | tracing::debug!("deactivate"); 104 | toggle_pane_frames(); 105 | } 106 | } 107 | 108 | pub fn should_show_frames_for_scroll(mode_info: &ModeInfo) -> bool { 109 | mode_info.mode == InputMode::Scroll 110 | } 111 | 112 | pub fn should_show_frames_for_search(mode_info: &ModeInfo) -> bool { 113 | mode_info.mode == InputMode::EnterSearch || mode_info.mode == InputMode::Search 114 | } 115 | 116 | pub fn should_show_frames_for_fullscreen(panes: &[&PaneInfo]) -> bool { 117 | let active_pane = match panes.iter().find(|p| p.is_focused) { 118 | Some(p) => p, 119 | None => return false, 120 | }; 121 | 122 | active_pane.is_fullscreen 123 | } 124 | 125 | #[tracing::instrument(skip_all)] 126 | pub fn should_show_frames_for_multiple_panes(mode_info: &ModeInfo, panes: &[&PaneInfo]) -> bool { 127 | tracing::debug!("mode: {:?}", mode_info.mode); 128 | if mode_info.mode == InputMode::RenamePane 129 | || mode_info.mode == InputMode::Search 130 | || mode_info.mode == InputMode::EnterSearch 131 | { 132 | return true; 133 | } 134 | 135 | panes.len() > 1 136 | } 137 | 138 | fn is_plugin_for_current_tab(panes: &[PaneInfo], plugin_pane_id: PluginIds) -> bool { 139 | panes 140 | .iter() 141 | .any(|p| p.is_plugin && p.id == plugin_pane_id.plugin_id) 142 | } 143 | 144 | fn get_current_panes(tabs: &[TabInfo], pane_info: &PaneManifest) -> Option> { 145 | let active_tab = tabs.iter().find(|t| t.active); 146 | let active_tab = active_tab.as_ref()?; 147 | 148 | pane_info.panes.get(&active_tab.position).cloned() 149 | } 150 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod border; 2 | pub mod config; 3 | pub mod frames; 4 | pub mod pipe; 5 | pub mod render; 6 | pub mod widgets; 7 | -------------------------------------------------------------------------------- /src/pipe.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Sub; 2 | 3 | use chrono::{Duration, Local}; 4 | 5 | use crate::{ 6 | config::ZellijState, 7 | widgets::{command::TIMESTAMP_FORMAT, notification}, 8 | }; 9 | 10 | /// Parses the line protocol and updates the state accordingly 11 | /// 12 | /// The protocol is as follows: 13 | /// 14 | /// zjstatus::command_name::args 15 | /// 16 | /// It first starts with `zjstatus` as a prefix to indicate that the line is 17 | /// used for the line protocol and zjstatus should parse it. It is followed 18 | /// by the command name and then the arguments. The following commands are 19 | /// available: 20 | /// 21 | /// - `rerun` - Reruns the command with the given name (like in the config) as 22 | /// argument. E.g. `zjstatus::rerun::command_1` 23 | /// 24 | /// The function returns a boolean indicating whether the state has been 25 | /// changed and the UI should be re-rendered. 26 | #[tracing::instrument(skip(state))] 27 | pub fn parse_protocol(state: &mut ZellijState, input: &str) -> bool { 28 | tracing::debug!("parsing protocol"); 29 | let lines = input.split('\n').collect::>(); 30 | 31 | let mut should_render = false; 32 | for line in lines { 33 | let line_renders = process_line(state, line); 34 | 35 | if line_renders { 36 | should_render = true; 37 | } 38 | } 39 | 40 | should_render 41 | } 42 | 43 | #[tracing::instrument(skip_all)] 44 | fn process_line(state: &mut ZellijState, line: &str) -> bool { 45 | let parts = line.split("::").collect::>(); 46 | 47 | if parts.len() < 3 { 48 | return false; 49 | } 50 | 51 | if parts[0] != "zjstatus" { 52 | return false; 53 | } 54 | 55 | tracing::debug!("command: {}", parts[1]); 56 | 57 | let mut should_render = false; 58 | #[allow(clippy::single_match)] 59 | match parts[1] { 60 | "rerun" => { 61 | rerun_command(state, parts[2]); 62 | 63 | should_render = true; 64 | } 65 | "notify" => { 66 | notify(state, parts[2]); 67 | 68 | should_render = true; 69 | } 70 | "pipe" => { 71 | if parts.len() < 4 { 72 | return false; 73 | } 74 | 75 | pipe(state, parts[2], parts[3]); 76 | 77 | should_render = true; 78 | } 79 | _ => {} 80 | } 81 | 82 | should_render 83 | } 84 | 85 | fn pipe(state: &mut ZellijState, name: &str, content: &str) { 86 | tracing::debug!("saving pipe result {name} {content}"); 87 | state 88 | .pipe_results 89 | .insert(name.to_owned(), content.to_owned()); 90 | } 91 | 92 | fn notify(state: &mut ZellijState, message: &str) { 93 | state.incoming_notification = Some(notification::Message { 94 | body: message.to_string(), 95 | received_at: Local::now(), 96 | }); 97 | } 98 | 99 | fn rerun_command(state: &mut ZellijState, command_name: &str) { 100 | let command_result = state.command_results.get(command_name); 101 | 102 | if command_result.is_none() { 103 | return; 104 | } 105 | 106 | let mut command_result = command_result.unwrap().clone(); 107 | 108 | let ts = Sub::::sub(Local::now(), Duration::try_days(1).unwrap()); 109 | 110 | command_result.context.insert( 111 | "timestamp".to_string(), 112 | ts.format(TIMESTAMP_FORMAT).to_string(), 113 | ); 114 | 115 | state.command_results.remove(command_name); 116 | state 117 | .command_results 118 | .insert(command_name.to_string(), command_result.clone()); 119 | } 120 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use cached::{proc_macro::cached, SizedCache}; 2 | use lazy_static::lazy_static; 3 | use std::{collections::BTreeMap, sync::Arc}; 4 | 5 | use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor, Style}; 6 | use regex::Regex; 7 | use zellij_tile::prelude::bail; 8 | 9 | use crate::{ 10 | config::{event_mask_from_widget_name, UpdateEventMask, ZellijState}, 11 | widgets::widget::Widget, 12 | }; 13 | 14 | lazy_static! { 15 | static ref WIDGET_REGEX: Regex = Regex::new("(\\{[a-z_0-9]+\\})").unwrap(); 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub struct FormattedPart { 20 | pub fg: Option, 21 | pub bg: Option, 22 | pub us: Option, 23 | pub effects: anstyle::Effects, 24 | pub bold: bool, 25 | pub italic: bool, 26 | pub underscore: bool, 27 | pub reverse: bool, 28 | pub blink: bool, 29 | pub hidden: bool, 30 | pub dimmed: bool, 31 | pub strikethrough: bool, 32 | pub double_underscore: bool, 33 | pub curly_underscore: bool, 34 | pub dotted_underscore: bool, 35 | pub dashed_underscore: bool, 36 | pub content: String, 37 | pub cache_mask: u8, 38 | pub cached_content: String, 39 | pub cache: BTreeMap, 40 | } 41 | 42 | #[cached( 43 | ty = "SizedCache", 44 | create = "{ SizedCache::with_size(100) }", 45 | convert = r#"{ (format.to_owned()) }"# 46 | )] 47 | pub fn formatted_part_from_string_cached( 48 | format: &str, 49 | config: &BTreeMap, 50 | ) -> FormattedPart { 51 | FormattedPart::from_format_string(format, config) 52 | } 53 | 54 | #[cached( 55 | ty = "SizedCache>", 56 | create = "{ SizedCache::with_size(100) }", 57 | convert = r#"{ (config_string.to_owned()) }"# 58 | )] 59 | pub fn formatted_parts_from_string_cached( 60 | config_string: &str, 61 | config: &BTreeMap, 62 | ) -> Vec { 63 | FormattedPart::multiple_from_format_string(config_string, config) 64 | } 65 | 66 | impl FormattedPart { 67 | pub fn multiple_from_format_string( 68 | config_string: &str, 69 | config: &BTreeMap, 70 | ) -> Vec { 71 | config_string 72 | .split("#[") 73 | .map(|s| FormattedPart::from_format_string(s, config)) 74 | .collect() 75 | } 76 | 77 | pub fn from_format_string(format: &str, config: &BTreeMap) -> Self { 78 | let format = match format.starts_with("#[") { 79 | true => format.strip_prefix("#[").unwrap(), 80 | false => format, 81 | }; 82 | 83 | let mut result = FormattedPart { 84 | cache_mask: cache_mask_from_content(format), 85 | ..Default::default() 86 | }; 87 | 88 | let mut format_content_split = format.split(']').collect::>(); 89 | 90 | if format_content_split.len() == 1 { 91 | format.clone_into(&mut result.content); 92 | 93 | return result; 94 | } 95 | 96 | let parts = format_content_split[0].split(','); 97 | 98 | format_content_split.remove(0); 99 | result.content = format_content_split.join("]"); 100 | 101 | for part in parts { 102 | if part.starts_with("fg=") { 103 | result.fg = parse_color(part.strip_prefix("fg=").unwrap(), config); 104 | } 105 | 106 | if part.starts_with("bg=") { 107 | result.bg = parse_color(part.strip_prefix("bg=").unwrap(), config); 108 | } 109 | 110 | if part.starts_with("us=") { 111 | result.us = parse_color(part.strip_prefix("us=").unwrap(), config); 112 | } 113 | 114 | if part.eq("reverse") { 115 | result.reverse = true; 116 | } 117 | 118 | result.parse_and_set_effect(part); 119 | } 120 | 121 | result 122 | } 123 | 124 | fn parse_and_set_effect(&mut self, part: &str) { 125 | match part { 126 | "bold" => { 127 | self.effects |= anstyle::Effects::BOLD; 128 | } 129 | "italic" | "italics" => { 130 | self.effects |= anstyle::Effects::ITALIC; 131 | } 132 | "underscore" => { 133 | self.effects |= anstyle::Effects::UNDERLINE; 134 | } 135 | "blink" => { 136 | self.effects |= anstyle::Effects::BLINK; 137 | } 138 | "hidden" => { 139 | self.effects |= anstyle::Effects::HIDDEN; 140 | } 141 | "dim" => { 142 | self.effects |= anstyle::Effects::DIMMED; 143 | } 144 | "strikethrough" => { 145 | self.effects |= anstyle::Effects::STRIKETHROUGH; 146 | } 147 | "double-underscore" => { 148 | self.effects |= anstyle::Effects::DOUBLE_UNDERLINE; 149 | } 150 | "curly-underscore" => { 151 | self.effects |= anstyle::Effects::CURLY_UNDERLINE; 152 | } 153 | "dotted-underscore" => { 154 | self.effects |= anstyle::Effects::DOTTED_UNDERLINE; 155 | } 156 | "dashed-underscore" => { 157 | self.effects |= anstyle::Effects::DASHED_UNDERLINE; 158 | } 159 | "reverse" => { 160 | self.effects |= anstyle::Effects::INVERT; 161 | } 162 | _ => {} 163 | } 164 | } 165 | 166 | pub fn format_string(&self, text: &str) -> String { 167 | let mut style = Style::new(); 168 | 169 | style = style.fg_color(self.fg); 170 | style = style.bg_color(self.bg); 171 | style = style.underline_color(self.us); 172 | style = style.effects(self.effects); 173 | 174 | format!( 175 | "{}{}{}{}", 176 | style.render_reset(), 177 | style.render(), 178 | text, 179 | style.render_reset() 180 | ) 181 | } 182 | 183 | #[tracing::instrument(skip_all)] 184 | pub fn format_string_with_widgets( 185 | &mut self, 186 | widgets: &BTreeMap>, 187 | state: &ZellijState, 188 | ) -> String { 189 | let skip_cache = self.cache_mask & UpdateEventMask::Always as u8 != 0; 190 | 191 | if !skip_cache && self.cache_mask & state.cache_mask == 0 && !self.cache.is_empty() { 192 | tracing::debug!(msg = "hit", typ = "format_string", format = self.content); 193 | return self.cached_content.to_owned(); 194 | } 195 | tracing::debug!(msg = "miss", typ = "format_string", format = self.content); 196 | 197 | let mut output = self.content.clone(); 198 | 199 | for widget in WIDGET_REGEX.captures_iter(&self.content) { 200 | let match_name = widget.get(0).unwrap().as_str(); 201 | let widget_key = match_name.trim_matches(|c| c == '{' || c == '}'); 202 | let mut widget_key_name = widget_key; 203 | 204 | if widget_key.starts_with("command_") { 205 | widget_key_name = "command"; 206 | } 207 | 208 | if widget_key.starts_with("pipe_") { 209 | widget_key_name = "pipe"; 210 | } 211 | 212 | let widget_mask = event_mask_from_widget_name(widget_key_name); 213 | let skip_widget_cache = widget_mask & UpdateEventMask::Always as u8 != 0; 214 | if !skip_widget_cache && widget_mask & state.cache_mask == 0 { 215 | if let Some(res) = self.cache.get(widget_key) { 216 | tracing::debug!(msg = "hit", typ = "widget", widget = widget_key); 217 | output = output.replace(match_name, res); 218 | continue; 219 | } 220 | } 221 | 222 | tracing::debug!( 223 | msg = "miss", 224 | typ = "widget", 225 | widget = widget_key, 226 | mask = widget_mask & state.cache_mask, 227 | skip_cache = skip_cache, 228 | ); 229 | 230 | let result = match widgets.get(widget_key_name) { 231 | Some(widget) => widget.process(widget_key, state), 232 | None => "Use of uninitialized widget".to_owned(), 233 | }; 234 | 235 | self.cache.insert(widget_key.to_owned(), result.to_owned()); 236 | 237 | output = output.replace(match_name, &result); 238 | } 239 | 240 | let res = self.format_string(&output); 241 | self.cached_content.clone_from(&res); 242 | 243 | res 244 | } 245 | } 246 | 247 | impl Default for FormattedPart { 248 | fn default() -> Self { 249 | Self { 250 | fg: None, 251 | bg: None, 252 | us: None, 253 | effects: anstyle::Effects::new(), 254 | bold: false, 255 | italic: false, 256 | underscore: false, 257 | reverse: false, 258 | blink: false, 259 | hidden: false, 260 | dimmed: false, 261 | strikethrough: false, 262 | double_underscore: false, 263 | curly_underscore: false, 264 | dotted_underscore: false, 265 | dashed_underscore: false, 266 | content: "".to_owned(), 267 | cache_mask: 0, 268 | cached_content: "".to_owned(), 269 | cache: BTreeMap::new(), 270 | } 271 | } 272 | } 273 | 274 | fn cache_mask_from_content(content: &str) -> u8 { 275 | let mut output = 0; 276 | for widget in WIDGET_REGEX.captures_iter(content) { 277 | let match_name = widget.get(0).unwrap().as_str(); 278 | let widget_key = match_name.trim_matches(|c| c == '{' || c == '}'); 279 | let mut widget_key_name = widget_key; 280 | 281 | if widget_key.starts_with("command_") { 282 | widget_key_name = "command"; 283 | } 284 | 285 | if widget_key.starts_with("pipe_") { 286 | widget_key_name = "pipe"; 287 | } 288 | 289 | output |= event_mask_from_widget_name(widget_key_name); 290 | } 291 | output 292 | } 293 | 294 | fn hex_to_rgb(s: &str) -> anyhow::Result> { 295 | if s.len() != 6 { 296 | bail!("wrong hex color length"); 297 | } 298 | 299 | (0..s.len()) 300 | .step_by(2) 301 | .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(anyhow::Error::from)) 302 | .collect() 303 | } 304 | 305 | #[cached( 306 | ty = "SizedCache>", 307 | create = "{ SizedCache::with_size(100) }", 308 | convert = r#"{ (color.to_owned()) }"# 309 | )] 310 | fn parse_color(color: &str, config: &BTreeMap) -> Option { 311 | let mut color = color; 312 | if color.starts_with('$') { 313 | let alias_name = color.strip_prefix('$').unwrap(); 314 | 315 | color = config.get(&format!("color_{alias_name}"))?; 316 | } 317 | 318 | if color.starts_with('#') { 319 | let rgb = match hex_to_rgb(color.strip_prefix('#').unwrap()) { 320 | Ok(rgb) => rgb, 321 | Err(_) => return None, 322 | }; 323 | 324 | if rgb.len() != 3 { 325 | return None; 326 | } 327 | 328 | return Some( 329 | RgbColor( 330 | *rgb.first().unwrap(), 331 | *rgb.get(1).unwrap(), 332 | *rgb.get(2).unwrap(), 333 | ) 334 | .into(), 335 | ); 336 | } 337 | 338 | if let Some(color) = color_by_name(color) { 339 | return Some(color.into()); 340 | } 341 | 342 | if color.starts_with("colour") { 343 | color = color.strip_prefix("colour").unwrap(); 344 | } 345 | 346 | if let Ok(result) = color.parse::() { 347 | return Some(Ansi256Color(result).into()); 348 | } 349 | 350 | None 351 | } 352 | 353 | fn color_by_name(color: &str) -> Option { 354 | match color { 355 | "black" => Some(AnsiColor::Black), 356 | "red" => Some(AnsiColor::Red), 357 | "green" => Some(AnsiColor::Green), 358 | "yellow" => Some(AnsiColor::Yellow), 359 | "blue" => Some(AnsiColor::Blue), 360 | "magenta" => Some(AnsiColor::Magenta), 361 | "cyan" => Some(AnsiColor::Cyan), 362 | "white" => Some(AnsiColor::White), 363 | "bright_black" => Some(AnsiColor::BrightBlack), 364 | "bright_red" => Some(AnsiColor::BrightRed), 365 | "bright_green" => Some(AnsiColor::BrightGreen), 366 | "bright_yellow" => Some(AnsiColor::BrightYellow), 367 | "bright_blue" => Some(AnsiColor::BrightBlue), 368 | "bright_magenta" => Some(AnsiColor::BrightMagenta), 369 | "bright_cyan" => Some(AnsiColor::BrightCyan), 370 | "bright_white" => Some(AnsiColor::BrightWhite), 371 | "default" => None, 372 | _ => None, 373 | } 374 | } 375 | 376 | #[cfg(test)] 377 | mod test { 378 | use super::*; 379 | 380 | #[test] 381 | fn test_hex_to_rgb() { 382 | let result = hex_to_rgb("010203"); 383 | let expected = Vec::from([1, 2, 3]); 384 | assert!(result.is_ok()); 385 | assert_eq!(result.unwrap(), expected); 386 | } 387 | 388 | #[test] 389 | fn test_hex_to_rgb_with_invalid_input() { 390 | let result = hex_to_rgb("#010203"); 391 | assert!(result.is_err()); 392 | 393 | let result = hex_to_rgb(" 010203"); 394 | assert!(result.is_err()); 395 | 396 | let result = hex_to_rgb("010"); 397 | assert!(result.is_err()); 398 | } 399 | 400 | #[test] 401 | fn test_parse_color() { 402 | let mut config: BTreeMap = BTreeMap::new(); 403 | config.insert("color_green".to_owned(), "#00ff00".to_owned()); 404 | 405 | let result = parse_color("#010203", &config); 406 | let expected = RgbColor(1, 2, 3); 407 | assert_eq!(result, Some(expected.into())); 408 | 409 | let result = parse_color("255", &config); 410 | let expected = Ansi256Color(255); 411 | assert_eq!(result, Some(expected.into())); 412 | 413 | let result = parse_color("365", &config); 414 | assert_eq!(result, None); 415 | 416 | let result = parse_color("#365", &config); 417 | assert_eq!(result, None); 418 | 419 | let result = parse_color("$green", &config); 420 | let expected = RgbColor(0, 255, 0); 421 | assert_eq!(result, Some(expected.into())); 422 | 423 | let result = parse_color("$blue", &config); 424 | assert_eq!(result, None); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/widgets/command.rs: -------------------------------------------------------------------------------- 1 | use kdl::{KdlDocument, KdlError}; 2 | use lazy_static::lazy_static; 3 | use std::{ 4 | collections::BTreeMap, 5 | fs::{remove_file, File}, 6 | ops::Sub, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use chrono::{DateTime, Duration, Local}; 11 | use regex::Regex; 12 | #[cfg(all(not(feature = "bench"), not(test)))] 13 | use zellij_tile::shim::{run_command, run_command_with_env_variables_and_cwd}; 14 | 15 | use crate::render::{formatted_parts_from_string_cached, FormattedPart}; 16 | 17 | use crate::{config::ZellijState, widgets::widget::Widget}; 18 | 19 | pub const TIMESTAMP_FORMAT: &str = "%s"; 20 | 21 | lazy_static! { 22 | static ref COMMAND_REGEX: Regex = Regex::new("_[a-zA-Z0-9]+$").unwrap(); 23 | } 24 | 25 | #[derive(Clone, Debug, PartialEq)] 26 | enum RenderMode { 27 | Static, 28 | Dynamic, 29 | Raw, 30 | } 31 | 32 | #[derive(Clone, Debug)] 33 | struct CommandConfig { 34 | command: String, 35 | format: Vec, 36 | env: Option>, 37 | cwd: Option, 38 | interval: i64, 39 | render_mode: RenderMode, 40 | click_action: String, 41 | } 42 | 43 | #[derive(Clone, Debug, Default)] 44 | pub struct CommandResult { 45 | pub exit_code: Option, 46 | pub stdout: String, 47 | pub stderr: String, 48 | pub context: BTreeMap, 49 | } 50 | 51 | pub struct CommandWidget { 52 | config: BTreeMap, 53 | zj_conf: BTreeMap, 54 | } 55 | 56 | impl CommandWidget { 57 | pub fn new(config: &BTreeMap) -> Self { 58 | Self { 59 | config: parse_config(config), 60 | zj_conf: config.clone(), 61 | } 62 | } 63 | } 64 | 65 | impl Widget for CommandWidget { 66 | fn process(&self, name: &str, state: &ZellijState) -> String { 67 | let command_config = match self.config.get(name) { 68 | Some(cc) => cc, 69 | None => { 70 | return "".to_owned(); 71 | } 72 | }; 73 | 74 | run_command_if_needed(command_config.clone(), name, state); 75 | 76 | let command_result = match state.command_results.get(name) { 77 | Some(cr) => cr, 78 | None => { 79 | return "".to_owned(); 80 | } 81 | }; 82 | 83 | let content = command_config 84 | .format 85 | .iter() 86 | .map(|f| { 87 | let mut content = f.content.clone(); 88 | 89 | if content.contains("{exit_code}") { 90 | content = content.replace( 91 | "{exit_code}", 92 | format!("{}", command_result.exit_code.unwrap_or(-1)).as_str(), 93 | ); 94 | } 95 | 96 | if content.contains("{stdout}") { 97 | content = content.replace( 98 | "{stdout}", 99 | command_result 100 | .stdout 101 | .strip_suffix('\n') 102 | .unwrap_or(&command_result.stdout), 103 | ); 104 | } 105 | 106 | if content.contains("{stderr}") { 107 | content = content.replace( 108 | "{stderr}", 109 | command_result 110 | .stderr 111 | .strip_suffix('\n') 112 | .unwrap_or(&command_result.stderr), 113 | ); 114 | } 115 | 116 | (f, content) 117 | }) 118 | .fold("".to_owned(), |acc, (f, content)| { 119 | if command_config.render_mode == RenderMode::Static { 120 | return format!("{acc}{}", f.format_string(&content)); 121 | } 122 | 123 | format!("{acc}{}", content) 124 | }); 125 | 126 | match command_config.render_mode { 127 | RenderMode::Static => content, 128 | RenderMode::Dynamic => render_dynamic_formatted_content(&content, &self.zj_conf), 129 | RenderMode::Raw => content, 130 | } 131 | } 132 | 133 | fn process_click(&self, name: &str, _state: &ZellijState, _pos: usize) { 134 | let command_config = match self.config.get(name) { 135 | Some(cc) => cc, 136 | None => { 137 | return; 138 | } 139 | }; 140 | 141 | if command_config.click_action.is_empty() { 142 | return; 143 | } 144 | 145 | let command = commandline_parser(&command_config.click_action); 146 | let context: BTreeMap = BTreeMap::new(); 147 | 148 | tracing::debug!("Running command {:?} {:?}", command, context); 149 | 150 | #[cfg(all(not(feature = "bench"), not(test)))] 151 | run_command( 152 | &command.iter().map(|x| x.as_str()).collect::>(), 153 | context, 154 | ); 155 | } 156 | } 157 | 158 | fn render_dynamic_formatted_content(content: &str, config: &BTreeMap) -> String { 159 | formatted_parts_from_string_cached(content, config) 160 | .iter() 161 | .map(|fp| fp.format_string(&fp.content)) 162 | .collect::>() 163 | .join("") 164 | } 165 | 166 | #[tracing::instrument(skip(command_config, state))] 167 | fn run_command_if_needed(command_config: CommandConfig, name: &str, state: &ZellijState) -> bool { 168 | let got_result = state.command_results.contains_key(name); 169 | if got_result && command_config.interval == 0 { 170 | return false; 171 | } 172 | 173 | let ts = Local::now(); 174 | let last_run = get_timestamp_from_event_or_default(name, state, command_config.interval); 175 | 176 | if ts.timestamp() - last_run.timestamp() >= command_config.interval { 177 | let mut context = BTreeMap::new(); 178 | context.insert("name".to_owned(), name.to_owned()); 179 | context.insert( 180 | "timestamp".to_owned(), 181 | ts.format(TIMESTAMP_FORMAT).to_string(), 182 | ); 183 | 184 | #[allow(unused_variables)] 185 | let command = commandline_parser(&command_config.command); 186 | tracing::debug!("Running command: {:?}", command); 187 | 188 | if command_config.env.is_some() || command_config.cwd.is_some() { 189 | #[cfg(all(not(feature = "bench"), not(test)))] 190 | run_command_with_env_variables_and_cwd( 191 | &command.iter().map(|x| x.as_str()).collect::>(), 192 | command_config.env.unwrap(), 193 | command_config.cwd.unwrap(), 194 | context, 195 | ); 196 | 197 | return true; 198 | } 199 | 200 | #[cfg(all(not(feature = "bench"), not(test)))] 201 | run_command( 202 | &command.iter().map(|x| x.as_str()).collect::>(), 203 | context, 204 | ); 205 | 206 | return true; 207 | } 208 | 209 | false 210 | } 211 | 212 | fn parse_config(zj_conf: &BTreeMap) -> BTreeMap { 213 | let mut keys: Vec = zj_conf 214 | .keys() 215 | .filter(|k| k.starts_with("command_")) 216 | .cloned() 217 | .collect(); 218 | keys.sort(); 219 | 220 | let mut config: BTreeMap = BTreeMap::new(); 221 | 222 | for key in keys { 223 | let command_name = COMMAND_REGEX.replace(&key, "").to_string(); 224 | 225 | let mut command_conf = CommandConfig { 226 | command: "".to_owned(), 227 | format: Vec::new(), 228 | cwd: None, 229 | env: None, 230 | interval: 1, 231 | render_mode: RenderMode::Static, 232 | click_action: "".to_owned(), 233 | }; 234 | 235 | if let Some(existing_conf) = config.get(command_name.as_str()) { 236 | command_conf = existing_conf.clone(); 237 | } 238 | 239 | if key.ends_with("command") { 240 | command_conf 241 | .command 242 | .clone_from(&zj_conf.get(&key).unwrap().to_owned()); 243 | } 244 | 245 | if key.ends_with("clickaction") { 246 | command_conf 247 | .click_action 248 | .clone_from(&zj_conf.get(&key).unwrap().to_owned()); 249 | } 250 | 251 | if key.ends_with("env") { 252 | let doc: Result = zj_conf.get(&key).unwrap().parse(); 253 | 254 | if let Ok(doc) = doc { 255 | command_conf.env = Some(get_env_vars(doc)); 256 | } 257 | } 258 | 259 | if key.ends_with("cwd") { 260 | let mut cwd = PathBuf::new(); 261 | cwd.push(zj_conf.get(&key).unwrap().to_owned().clone()); 262 | 263 | command_conf.cwd = Some(cwd); 264 | } 265 | 266 | if key.ends_with("format") { 267 | command_conf.format = 268 | FormattedPart::multiple_from_format_string(zj_conf.get(&key).unwrap(), zj_conf); 269 | } 270 | 271 | if key.ends_with("interval") { 272 | command_conf.interval = zj_conf.get(&key).unwrap().parse::().unwrap_or(1); 273 | } 274 | 275 | if key.ends_with("rendermode") { 276 | command_conf.render_mode = match zj_conf.get(&key) { 277 | Some(mode) => match mode.as_str() { 278 | "static" => RenderMode::Static, 279 | "dynamic" => RenderMode::Dynamic, 280 | "raw" => RenderMode::Raw, 281 | _ => RenderMode::Static, 282 | }, 283 | None => RenderMode::Static, 284 | }; 285 | } 286 | 287 | config.insert(command_name, command_conf); 288 | } 289 | 290 | config 291 | } 292 | 293 | fn get_env_vars(doc: KdlDocument) -> BTreeMap { 294 | let mut output = BTreeMap::new(); 295 | 296 | for n in doc.nodes() { 297 | let children = n.entries(); 298 | if children.len() != 1 { 299 | continue; 300 | } 301 | 302 | let value = match children.first().unwrap().value().as_string() { 303 | Some(value) => value, 304 | None => continue, 305 | }; 306 | 307 | output.insert(n.name().value().to_string(), value.to_string()); 308 | } 309 | 310 | output 311 | } 312 | 313 | fn get_timestamp_from_event_or_default( 314 | name: &str, 315 | state: &ZellijState, 316 | interval: i64, 317 | ) -> DateTime { 318 | let command_result = state.command_results.get(name); 319 | if command_result.is_none() { 320 | if lock(name, state.clone()) { 321 | return Local::now(); 322 | } 323 | 324 | return Sub::::sub(Local::now(), Duration::try_days(1).unwrap()); 325 | } 326 | let command_result = command_result.unwrap(); 327 | 328 | let ts_context = command_result.context.get("timestamp"); 329 | if ts_context.is_none() { 330 | return Sub::::sub(Local::now(), Duration::try_days(1).unwrap()); 331 | } 332 | let ts_context = ts_context.unwrap(); 333 | 334 | if Local::now().timestamp() - state.start_time.timestamp() < interval { 335 | release(name, state.clone()); 336 | } 337 | 338 | match DateTime::parse_from_str(ts_context, TIMESTAMP_FORMAT) { 339 | Ok(ts) => ts.into(), 340 | Err(_) => Sub::::sub(Local::now(), Duration::try_days(1).unwrap()), 341 | } 342 | } 343 | 344 | fn lock(name: &str, state: ZellijState) -> bool { 345 | let path = format!("/tmp/{}.{}.lock", state.plugin_uuid, name); 346 | 347 | if !Path::new(&path).exists() { 348 | let _ = File::create(path); 349 | 350 | return false; 351 | } 352 | 353 | true 354 | } 355 | 356 | fn release(name: &str, state: ZellijState) { 357 | let path = format!("/tmp/{}.{}.lock", state.plugin_uuid, name); 358 | 359 | if Path::new(&path).exists() { 360 | let _ = remove_file(path); 361 | } 362 | } 363 | 364 | fn commandline_parser(input: &str) -> Vec { 365 | let mut output: Vec = Vec::new(); 366 | 367 | let special_chars = ['"', '\'']; 368 | 369 | let mut found_special_char = '\0'; 370 | let mut buffer = "".to_owned(); 371 | let mut is_escaped = false; 372 | let mut is_in_group = false; 373 | 374 | for character in input.chars() { 375 | if is_escaped { 376 | is_escaped = false; 377 | buffer = format!("{}\\{}", buffer.to_owned(), character); 378 | continue; 379 | } 380 | 381 | if character == '\\' { 382 | is_escaped = true; 383 | continue; 384 | } 385 | 386 | if found_special_char == character && is_in_group { 387 | is_in_group = false; 388 | found_special_char = '\0'; 389 | output.push(buffer.clone()); 390 | "".clone_into(&mut buffer); 391 | continue; 392 | } 393 | 394 | if special_chars.contains(&character) && !is_in_group { 395 | is_in_group = true; 396 | found_special_char = character; 397 | continue; 398 | } 399 | 400 | if character == ' ' && !is_in_group { 401 | output.push(buffer.clone()); 402 | "".clone_into(&mut buffer); 403 | continue; 404 | } 405 | 406 | buffer = format!("{}{}", buffer, character); 407 | } 408 | 409 | if !buffer.is_empty() { 410 | output.push(buffer.clone()); 411 | } 412 | 413 | output 414 | } 415 | 416 | #[cfg(test)] 417 | mod test { 418 | use super::*; 419 | use rstest::rstest; 420 | 421 | #[test] 422 | pub fn test_commandline_parser() { 423 | let input = "pwd"; 424 | let result = commandline_parser(input); 425 | let expected = Vec::from(["pwd"]); 426 | assert_eq!(result, expected); 427 | 428 | let input = "bash -c \"pwd | base64 -c \\\"bla\\\"\""; 429 | let result = commandline_parser(input); 430 | let expected = Vec::from(["bash", "-c", "pwd | base64 -c \\\"bla\\\""]); 431 | assert_eq!(result, expected); 432 | 433 | let input = "bash -c \"pwd | base64 -c 'bla' | xxd\""; 434 | let result = commandline_parser(input); 435 | let expected = Vec::from(["bash", "-c", "pwd | base64 -c 'bla' | xxd"]); 436 | assert_eq!(result, expected); 437 | } 438 | 439 | #[rstest] 440 | // no result, interval 1 second 441 | #[case(1, &ZellijState::default(), true)] 442 | // only run once without a result 443 | #[case(0, &ZellijState::default(), true)] 444 | // do not run with run once and result 445 | #[case(0, &ZellijState { 446 | command_results: BTreeMap::from([( 447 | "test".to_owned(), 448 | CommandResult::default(), 449 | )]), 450 | ..ZellijState::default() 451 | }, false)] 452 | // run if interval is exceeded 453 | #[case(1, &ZellijState { 454 | command_results: BTreeMap::from([( 455 | "test".to_owned(), 456 | CommandResult{ 457 | context: BTreeMap::from([("timestamp".to_owned(), "0".to_owned())]), 458 | ..CommandResult::default() 459 | } 460 | )]), 461 | ..ZellijState::default() 462 | }, true)] 463 | // do not run if interval is not exceeded 464 | #[case(1, &ZellijState { 465 | command_results: BTreeMap::from([( 466 | "test".to_owned(), 467 | CommandResult{ 468 | context: BTreeMap::from([("timestamp".to_owned(), Local::now().format(TIMESTAMP_FORMAT).to_string())]), 469 | ..CommandResult::default() 470 | } 471 | )]), 472 | ..ZellijState::default() 473 | }, false)] 474 | pub fn test_run_command_if_needed( 475 | #[case] interval: i64, 476 | #[case] state: &ZellijState, 477 | #[case] expected: bool, 478 | ) { 479 | let res = run_command_if_needed( 480 | CommandConfig { 481 | command: "echo test".to_owned(), 482 | format: Vec::new(), 483 | env: None, 484 | cwd: None, 485 | interval, 486 | render_mode: RenderMode::Static, 487 | click_action: "".to_owned(), 488 | }, 489 | "test", 490 | state, 491 | ); 492 | assert_eq!(res, expected); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/widgets/datetime.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, str::FromStr}; 2 | 3 | use chrono::Local; 4 | use chrono_tz::Tz; 5 | 6 | use crate::render::FormattedPart; 7 | 8 | use crate::{config::ZellijState, widgets::widget::Widget}; 9 | 10 | pub struct DateTimeWidget { 11 | format: String, 12 | time_format: String, 13 | date_format: String, 14 | color_format: Vec, 15 | time_zone: Option, 16 | } 17 | 18 | impl DateTimeWidget { 19 | pub fn new(config: &BTreeMap) -> Self { 20 | let mut format = "%H:%M"; 21 | if let Some(form) = config.get("datetime_format") { 22 | format = form; 23 | } 24 | 25 | let mut time_format = "%H:%M"; 26 | if let Some(form) = config.get("datetime_time_format") { 27 | time_format = form; 28 | } 29 | 30 | let mut date_format = "%Y-%m-%d"; 31 | if let Some(form) = config.get("datetime_date_format") { 32 | date_format = form; 33 | } 34 | 35 | let mut time_zone_string = "Etc/UTC"; 36 | if let Some(tz_string) = config.get("datetime_timezone") { 37 | time_zone_string = tz_string; 38 | } 39 | 40 | let time_zone = Tz::from_str(time_zone_string).ok(); 41 | 42 | let mut color_format = ""; 43 | if let Some(form) = config.get("datetime") { 44 | color_format = form; 45 | } 46 | 47 | Self { 48 | format: format.to_owned(), 49 | date_format: date_format.to_owned(), 50 | time_format: time_format.to_owned(), 51 | color_format: FormattedPart::multiple_from_format_string(color_format, config), 52 | time_zone, 53 | } 54 | } 55 | } 56 | 57 | impl Widget for DateTimeWidget { 58 | fn process(&self, _name: &str, _state: &ZellijState) -> String { 59 | let date = Local::now(); 60 | 61 | let mut tz = Tz::UTC; 62 | if let Some(t) = self.time_zone { 63 | tz = t; 64 | } 65 | 66 | self.color_format 67 | .iter() 68 | .map(|f| { 69 | let mut content = f.content.clone(); 70 | 71 | if content.contains("{format}") { 72 | content = content.replace( 73 | "{format}", 74 | format!("{}", date.with_timezone(&tz).format(self.format.as_str())) 75 | .as_str(), 76 | ); 77 | } 78 | 79 | if content.contains("{date}") { 80 | content = content.replace( 81 | "{date}", 82 | format!( 83 | "{}", 84 | date.with_timezone(&tz).format(self.date_format.as_str()) 85 | ) 86 | .as_str(), 87 | ); 88 | } 89 | 90 | if content.contains("{time}") { 91 | content = content.replace( 92 | "{time}", 93 | format!( 94 | "{}", 95 | date.with_timezone(&tz).format(self.time_format.as_str()) 96 | ) 97 | .as_str(), 98 | ); 99 | } 100 | 101 | (f, content) 102 | }) 103 | .fold("".to_owned(), |acc, (f, content)| { 104 | format!("{acc}{}", f.format_string(&content)) 105 | }) 106 | } 107 | 108 | fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize) {} 109 | } 110 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod datetime; 3 | pub mod mode; 4 | pub mod notification; 5 | pub mod pipe; 6 | pub mod session; 7 | pub mod swap_layout; 8 | pub mod tabs; 9 | pub mod widget; 10 | -------------------------------------------------------------------------------- /src/widgets/mode.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use zellij_tile::prelude::InputMode; 4 | 5 | use crate::{config::ZellijState, render::FormattedPart}; 6 | 7 | use super::widget::Widget; 8 | 9 | #[derive(Debug)] 10 | pub struct ModeWidget { 11 | normal_format: Vec, 12 | locked_format: Vec, 13 | resize_format: Vec, 14 | pane_format: Vec, 15 | tab_format: Vec, 16 | scroll_format: Vec, 17 | enter_search_format: Vec, 18 | search_format: Vec, 19 | rename_tab_format: Vec, 20 | rename_pane_format: Vec, 21 | session_format: Vec, 22 | move_format: Vec, 23 | prompt_format: Vec, 24 | tmux_format: Vec, 25 | default_to_mode: Option, 26 | } 27 | 28 | impl ModeWidget { 29 | pub fn new(config: &BTreeMap) -> Self { 30 | let normal_format = match config.get("mode_normal") { 31 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 32 | None => vec![], 33 | }; 34 | 35 | let locked_format = match config.get("mode_locked") { 36 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 37 | None => vec![], 38 | }; 39 | 40 | let resize_format = match config.get("mode_resize") { 41 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 42 | None => vec![], 43 | }; 44 | 45 | let pane_format = match config.get("mode_pane") { 46 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 47 | None => vec![], 48 | }; 49 | 50 | let tab_format = match config.get("mode_tab") { 51 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 52 | None => vec![], 53 | }; 54 | 55 | let scroll_format = match config.get("mode_scroll") { 56 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 57 | None => vec![], 58 | }; 59 | 60 | let enter_search_format = match config.get("mode_enter_search") { 61 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 62 | None => vec![], 63 | }; 64 | 65 | let search_format = match config.get("mode_search") { 66 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 67 | None => vec![], 68 | }; 69 | 70 | let rename_tab_format = match config.get("mode_rename_tab") { 71 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 72 | None => vec![], 73 | }; 74 | 75 | let rename_pane_format = match config.get("mode_rename_pane") { 76 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 77 | None => vec![], 78 | }; 79 | 80 | let session_format = match config.get("mode_session") { 81 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 82 | None => vec![], 83 | }; 84 | 85 | let move_format = match config.get("mode_move") { 86 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 87 | None => vec![], 88 | }; 89 | 90 | let prompt_format = match config.get("mode_prompt") { 91 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 92 | None => vec![], 93 | }; 94 | 95 | let tmux_format = match config.get("mode_tmux") { 96 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 97 | None => vec![], 98 | }; 99 | 100 | let default_to_mode = config.get("mode_default_to_mode").map(|s| s.to_string()); 101 | 102 | Self { 103 | normal_format, 104 | locked_format, 105 | resize_format, 106 | pane_format, 107 | tab_format, 108 | scroll_format, 109 | enter_search_format, 110 | search_format, 111 | rename_tab_format, 112 | rename_pane_format, 113 | session_format, 114 | move_format, 115 | prompt_format, 116 | tmux_format, 117 | default_to_mode, 118 | } 119 | } 120 | } 121 | 122 | impl Widget for ModeWidget { 123 | fn process(&self, _name: &str, state: &ZellijState) -> String { 124 | self.select_format(state.mode.mode) 125 | .iter() 126 | .map(|f| { 127 | let mut content = f.content.clone(); 128 | 129 | if f.content.contains("{name}") { 130 | content = f 131 | .content 132 | .replace("{name}", format!("{:?}", state.mode.mode).as_str()); 133 | } 134 | 135 | (f, content) 136 | }) 137 | .fold("".to_owned(), |acc, (f, content)| { 138 | format!("{acc}{}", f.format_string(&content)) 139 | }) 140 | } 141 | 142 | fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize) {} 143 | } 144 | 145 | impl ModeWidget { 146 | fn get_format_by_mode(&self, mode: InputMode) -> &Vec { 147 | match mode { 148 | InputMode::Normal => &self.normal_format, 149 | InputMode::Locked => &self.locked_format, 150 | InputMode::Resize => &self.resize_format, 151 | InputMode::Pane => &self.pane_format, 152 | InputMode::Tab => &self.tab_format, 153 | InputMode::Scroll => &self.scroll_format, 154 | InputMode::EnterSearch => &self.enter_search_format, 155 | InputMode::Search => &self.search_format, 156 | InputMode::RenameTab => &self.rename_tab_format, 157 | InputMode::RenamePane => &self.rename_pane_format, 158 | InputMode::Session => &self.session_format, 159 | InputMode::Move => &self.move_format, 160 | InputMode::Prompt => &self.prompt_format, 161 | InputMode::Tmux => &self.tmux_format, 162 | } 163 | } 164 | 165 | fn select_format(&self, mode: InputMode) -> &Vec { 166 | let output = self.get_format_by_mode(mode); 167 | 168 | if output.is_empty() { 169 | return match self.default_to_mode { 170 | Some(ref mode) => match map_string_to_mode(mode) { 171 | Some(mode) => { 172 | let out = self.get_format_by_mode(mode); 173 | 174 | if out.is_empty() { 175 | return &self.normal_format; 176 | } 177 | 178 | return out; 179 | } 180 | None => &self.normal_format, 181 | }, 182 | None => &self.normal_format, 183 | }; 184 | } 185 | 186 | output 187 | } 188 | } 189 | 190 | fn map_string_to_mode(s: &str) -> Option { 191 | match s { 192 | "normal" => Some(InputMode::Normal), 193 | "locked" => Some(InputMode::Locked), 194 | "resize" => Some(InputMode::Resize), 195 | "pane" => Some(InputMode::Pane), 196 | "tab" => Some(InputMode::Tab), 197 | "scroll" => Some(InputMode::Scroll), 198 | "enter_search" => Some(InputMode::EnterSearch), 199 | "search" => Some(InputMode::Search), 200 | "rename_tab" => Some(InputMode::RenameTab), 201 | "rename_pane" => Some(InputMode::RenamePane), 202 | "session" => Some(InputMode::Session), 203 | "move" => Some(InputMode::Move), 204 | "prompt" => Some(InputMode::Prompt), 205 | "tmux" => Some(InputMode::Tmux), 206 | _ => None, 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/widgets/notification.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use chrono::{DateTime, Local}; 4 | 5 | use crate::render::FormattedPart; 6 | use crate::{config::ZellijState, widgets::widget::Widget}; 7 | 8 | #[derive(Clone, Debug, Default)] 9 | pub struct Message { 10 | pub body: String, 11 | pub received_at: DateTime, 12 | } 13 | 14 | pub struct NotificationWidget { 15 | show_interval: i64, 16 | format_unread: Vec, 17 | format_no_notifications: Vec, 18 | } 19 | 20 | impl NotificationWidget { 21 | pub fn new(config: &BTreeMap) -> Self { 22 | let format_unread = match config.get("notification_format_unread") { 23 | Some(f) => FormattedPart::multiple_from_format_string(f, config), 24 | None => FormattedPart::multiple_from_format_string("", config), 25 | }; 26 | 27 | let format_no_notifications = match config.get("notification_format_no_notifications") { 28 | Some(f) => FormattedPart::multiple_from_format_string(f, config), 29 | None => FormattedPart::multiple_from_format_string("", config), 30 | }; 31 | 32 | let show_interval = match config.get("notification_show_interval") { 33 | Some(i) => i.parse::().unwrap_or(5), 34 | None => 5, 35 | }; 36 | 37 | Self { 38 | show_interval, 39 | format_unread, 40 | format_no_notifications, 41 | } 42 | } 43 | } 44 | 45 | impl Widget for NotificationWidget { 46 | fn process(&self, _name: &str, state: &ZellijState) -> String { 47 | let message = match state.incoming_notification { 48 | Some(ref message) => message.clone(), 49 | None => Message::default(), 50 | }; 51 | 52 | let no_new = 53 | message.received_at.timestamp() + self.show_interval < Local::now().timestamp(); 54 | 55 | tracing::debug!("no_new: {}", no_new); 56 | 57 | let format = match no_new { 58 | true => self.format_no_notifications.clone(), 59 | false => self.format_unread.clone(), 60 | }; 61 | 62 | let mut output = "".to_owned(); 63 | 64 | for f in format.iter() { 65 | let mut content = f.content.clone(); 66 | 67 | if content.contains("{message}") { 68 | content = content.replace("{message}", message.body.as_str()); 69 | } 70 | 71 | output = format!("{}{}", output, f.format_string(&content)); 72 | } 73 | 74 | output.to_owned() 75 | } 76 | 77 | fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize) {} 78 | } 79 | -------------------------------------------------------------------------------- /src/widgets/pipe.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use regex::Regex; 3 | use std::collections::BTreeMap; 4 | 5 | use crate::render::FormattedPart; 6 | 7 | use super::widget::Widget; 8 | 9 | lazy_static! { 10 | static ref PIPE_REGEX: Regex = Regex::new("_[a-zA-Z0-9]+$").unwrap(); 11 | } 12 | 13 | pub struct PipeWidget { 14 | config: BTreeMap, 15 | } 16 | 17 | #[derive(Clone)] 18 | struct PipeConfig { 19 | format: Vec, 20 | } 21 | 22 | impl PipeWidget { 23 | pub fn new(config: &BTreeMap) -> Self { 24 | Self { 25 | config: parse_config(config), 26 | } 27 | } 28 | } 29 | 30 | impl Widget for PipeWidget { 31 | fn process(&self, name: &str, state: &crate::config::ZellijState) -> String { 32 | let pipe_config = match self.config.get(name) { 33 | Some(pc) => pc, 34 | None => { 35 | tracing::debug!("pipe no name {name}"); 36 | return "".to_owned(); 37 | } 38 | }; 39 | 40 | let pipe_result = match state.pipe_results.get(name) { 41 | Some(pr) => pr, 42 | None => { 43 | tracing::debug!("pipe no content {name}"); 44 | return "".to_owned(); 45 | } 46 | }; 47 | 48 | pipe_config 49 | .format 50 | .iter() 51 | .map(|f| { 52 | let mut content = f.content.clone(); 53 | 54 | if content.contains("{output}") { 55 | content = content.replace( 56 | "{output}", 57 | pipe_result.strip_suffix('\n').unwrap_or(pipe_result), 58 | ) 59 | } 60 | 61 | (f, content) 62 | }) 63 | .fold("".to_owned(), |acc, (f, content)| { 64 | format!("{acc}{}", f.format_string(&content)) 65 | }) 66 | } 67 | 68 | fn process_click(&self, _name: &str, _state: &crate::config::ZellijState, _pos: usize) {} 69 | } 70 | 71 | fn parse_config(zj_conf: &BTreeMap) -> BTreeMap { 72 | let mut keys: Vec = zj_conf 73 | .keys() 74 | .filter(|k| k.starts_with("pipe_")) 75 | .cloned() 76 | .collect(); 77 | keys.sort(); 78 | 79 | let mut config: BTreeMap = BTreeMap::new(); 80 | 81 | for key in keys { 82 | let pipe_name = PIPE_REGEX.replace(&key, "").to_string(); 83 | let mut pipe_conf = PipeConfig { format: vec![] }; 84 | 85 | if let Some(existing_conf) = config.get(pipe_name.as_str()) { 86 | pipe_conf = existing_conf.clone(); 87 | } 88 | 89 | if key.ends_with("format") { 90 | pipe_conf.format = 91 | FormattedPart::multiple_from_format_string(zj_conf.get(&key).unwrap(), zj_conf); 92 | } 93 | 94 | config.insert(pipe_name, pipe_conf); 95 | } 96 | config 97 | } 98 | -------------------------------------------------------------------------------- /src/widgets/session.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::{config::ZellijState, widgets::widget::Widget}; 4 | 5 | pub struct SessionWidget {} 6 | 7 | impl SessionWidget { 8 | pub fn new(_config: &BTreeMap) -> Self { 9 | Self {} 10 | } 11 | } 12 | 13 | impl Widget for SessionWidget { 14 | fn process(&self, _name: &str, state: &ZellijState) -> String { 15 | match &state.mode.session_name { 16 | Some(name) => name.to_owned(), 17 | None => "".to_owned(), 18 | } 19 | } 20 | 21 | fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/widgets/swap_layout.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use zellij_tile::shim::next_swap_layout; 3 | 4 | use crate::render::FormattedPart; 5 | use crate::{config::ZellijState, widgets::widget::Widget}; 6 | 7 | pub struct SwapLayoutWidget { 8 | format: Vec, 9 | hide_if_empty: bool, 10 | } 11 | 12 | impl SwapLayoutWidget { 13 | pub fn new(config: &BTreeMap) -> Self { 14 | let mut format: Vec = Vec::new(); 15 | if let Some(form) = config.get("swap_layout_format") { 16 | format = FormattedPart::multiple_from_format_string(form, config); 17 | } 18 | 19 | let hide_if_empty = match config.get("swap_layout_hide_if_empty") { 20 | Some(hide_if_empty) => hide_if_empty == "true", 21 | None => false, 22 | }; 23 | 24 | Self { 25 | format, 26 | hide_if_empty, 27 | } 28 | } 29 | } 30 | 31 | impl Widget for SwapLayoutWidget { 32 | fn process(&self, _name: &str, state: &ZellijState) -> String { 33 | let active_tab = state.tabs.iter().find(|t| t.active); 34 | 35 | if active_tab.is_none() { 36 | return "".to_owned(); 37 | } 38 | 39 | let active_tab = active_tab.unwrap(); 40 | 41 | let name = match active_tab.active_swap_layout_name.clone() { 42 | Some(n) => n, 43 | None => "".to_owned(), 44 | }; 45 | 46 | if name.is_empty() && self.hide_if_empty { 47 | return "".to_owned(); 48 | } 49 | 50 | if self.format.is_empty() { 51 | return name; 52 | } 53 | 54 | let mut output = "".to_owned(); 55 | 56 | for f in &self.format { 57 | let mut content = f.content.clone(); 58 | 59 | if content.contains("{name}") { 60 | content = content.replace("{name}", &name); 61 | } 62 | 63 | output = format!("{}{}", output, f.format_string(&content)); 64 | } 65 | 66 | output 67 | } 68 | 69 | fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize) { 70 | next_swap_layout() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/widgets/tabs.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp, collections::BTreeMap}; 2 | 3 | use zellij_tile::{ 4 | prelude::{InputMode, ModeInfo, PaneInfo, PaneManifest, TabInfo}, 5 | shim::switch_tab_to, 6 | }; 7 | 8 | use crate::{config::ZellijState, render::FormattedPart}; 9 | 10 | use super::widget::Widget; 11 | 12 | pub struct TabsWidget { 13 | active_tab_format: Vec, 14 | active_tab_fullscreen_format: Vec, 15 | active_tab_sync_format: Vec, 16 | normal_tab_format: Vec, 17 | normal_tab_fullscreen_format: Vec, 18 | normal_tab_sync_format: Vec, 19 | rename_tab_format: Vec, 20 | separator: Option, 21 | fullscreen_indicator: Option, 22 | floating_indicator: Option, 23 | sync_indicator: Option, 24 | tab_display_count: Option, 25 | tab_truncate_start_format: Vec, 26 | tab_truncate_end_format: Vec, 27 | } 28 | 29 | impl TabsWidget { 30 | pub fn new(config: &BTreeMap) -> Self { 31 | let mut normal_tab_format: Vec = Vec::new(); 32 | if let Some(form) = config.get("tab_normal") { 33 | normal_tab_format = FormattedPart::multiple_from_format_string(form, config); 34 | } 35 | 36 | let normal_tab_fullscreen_format = match config.get("tab_normal_fullscreen") { 37 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 38 | None => normal_tab_format.clone(), 39 | }; 40 | 41 | let normal_tab_sync_format = match config.get("tab_normal_sync") { 42 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 43 | None => normal_tab_format.clone(), 44 | }; 45 | 46 | let mut active_tab_format = normal_tab_format.clone(); 47 | if let Some(form) = config.get("tab_active") { 48 | active_tab_format = FormattedPart::multiple_from_format_string(form, config); 49 | } 50 | 51 | let active_tab_fullscreen_format = match config.get("tab_active_fullscreen") { 52 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 53 | None => active_tab_format.clone(), 54 | }; 55 | 56 | let active_tab_sync_format = match config.get("tab_active_sync") { 57 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 58 | None => active_tab_format.clone(), 59 | }; 60 | 61 | let rename_tab_format = match config.get("tab_rename") { 62 | Some(form) => FormattedPart::multiple_from_format_string(form, config), 63 | None => active_tab_format.clone(), 64 | }; 65 | 66 | let tab_display_count = match config.get("tab_display_count") { 67 | Some(count) => count.parse::().ok(), 68 | None => None, 69 | }; 70 | 71 | let tab_truncate_start_format = config 72 | .get("tab_truncate_start_format") 73 | .map(|form| FormattedPart::multiple_from_format_string(form, config)) 74 | .unwrap_or_default(); 75 | 76 | let tab_truncate_end_format = config 77 | .get("tab_truncate_end_format") 78 | .map(|form| FormattedPart::multiple_from_format_string(form, config)) 79 | .unwrap_or_default(); 80 | 81 | let separator = config 82 | .get("tab_separator") 83 | .map(|s| FormattedPart::from_format_string(s, config)); 84 | 85 | Self { 86 | normal_tab_format, 87 | normal_tab_fullscreen_format, 88 | normal_tab_sync_format, 89 | active_tab_format, 90 | active_tab_fullscreen_format, 91 | active_tab_sync_format, 92 | rename_tab_format, 93 | separator, 94 | floating_indicator: config.get("tab_floating_indicator").cloned(), 95 | sync_indicator: config.get("tab_sync_indicator").cloned(), 96 | fullscreen_indicator: config.get("tab_fullscreen_indicator").cloned(), 97 | tab_display_count, 98 | tab_truncate_start_format, 99 | tab_truncate_end_format, 100 | } 101 | } 102 | } 103 | 104 | impl Widget for TabsWidget { 105 | fn process(&self, _name: &str, state: &ZellijState) -> String { 106 | let mut output = "".to_owned(); 107 | let mut counter = 0; 108 | 109 | let (truncated_start, truncated_end, tabs) = 110 | get_tab_window(&state.tabs, self.tab_display_count); 111 | 112 | if truncated_start > 0 { 113 | for f in &self.tab_truncate_start_format { 114 | let mut content = f.content.clone(); 115 | 116 | if content.contains("{count}") { 117 | content = content.replace("{count}", (truncated_start).to_string().as_str()); 118 | } 119 | 120 | output = format!("{output}{}", f.format_string(&content)); 121 | } 122 | } 123 | 124 | for tab in &tabs { 125 | let content = self.render_tab(tab, &state.panes, &state.mode); 126 | counter += 1; 127 | 128 | output = format!("{}{}", output, content); 129 | 130 | if counter < tabs.len() { 131 | if let Some(sep) = &self.separator { 132 | output = format!("{}{}", output, sep.format_string(&sep.content)); 133 | } 134 | } 135 | } 136 | 137 | if truncated_end > 0 { 138 | for f in &self.tab_truncate_end_format { 139 | let mut content = f.content.clone(); 140 | 141 | if content.contains("{count}") { 142 | content = content.replace("{count}", (truncated_end).to_string().as_str()); 143 | } 144 | 145 | output = format!("{output}{}", f.format_string(&content)); 146 | } 147 | } 148 | 149 | output 150 | } 151 | 152 | fn process_click(&self, _name: &str, state: &ZellijState, pos: usize) { 153 | let mut offset = 0; 154 | let mut counter = 0; 155 | 156 | let (truncated_start, truncated_end, tabs) = 157 | get_tab_window(&state.tabs, self.tab_display_count); 158 | 159 | let active_pos = &state 160 | .tabs 161 | .iter() 162 | .find(|t| t.active) 163 | .expect("no active tab") 164 | .position 165 | + 1; 166 | 167 | if truncated_start > 0 { 168 | for f in &self.tab_truncate_start_format { 169 | let mut content = f.content.clone(); 170 | 171 | if content.contains("{count}") { 172 | content = content.replace("{count}", (truncated_end).to_string().as_str()); 173 | } 174 | 175 | offset += console::measure_text_width(&f.format_string(&content)); 176 | 177 | if pos <= offset { 178 | switch_tab_to(active_pos.saturating_sub(1) as u32); 179 | } 180 | } 181 | } 182 | 183 | for tab in &tabs { 184 | counter += 1; 185 | 186 | let mut rendered_content = self.render_tab(tab, &state.panes, &state.mode); 187 | 188 | if counter < tabs.len() { 189 | if let Some(sep) = &self.separator { 190 | rendered_content = 191 | format!("{}{}", rendered_content, sep.format_string(&sep.content)); 192 | } 193 | } 194 | 195 | let content_len = console::measure_text_width(&rendered_content); 196 | 197 | if pos > offset && pos < offset + content_len { 198 | switch_tab_to(tab.position as u32 + 1); 199 | 200 | break; 201 | } 202 | 203 | offset += content_len; 204 | } 205 | 206 | if truncated_end > 0 { 207 | for f in &self.tab_truncate_end_format { 208 | let mut content = f.content.clone(); 209 | 210 | if content.contains("{count}") { 211 | content = content.replace("{count}", (truncated_end).to_string().as_str()); 212 | } 213 | 214 | offset += console::measure_text_width(&f.format_string(&content)); 215 | 216 | if pos <= offset { 217 | switch_tab_to(cmp::min(active_pos + 1, state.tabs.len()) as u32); 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | impl TabsWidget { 225 | fn select_format(&self, info: &TabInfo, mode: &ModeInfo) -> &Vec { 226 | if info.active && mode.mode == InputMode::RenameTab { 227 | return &self.rename_tab_format; 228 | } 229 | 230 | if info.active && info.is_fullscreen_active { 231 | return &self.active_tab_fullscreen_format; 232 | } 233 | 234 | if info.active && info.is_sync_panes_active { 235 | return &self.active_tab_sync_format; 236 | } 237 | 238 | if info.active { 239 | return &self.active_tab_format; 240 | } 241 | 242 | if info.is_fullscreen_active { 243 | return &self.normal_tab_fullscreen_format; 244 | } 245 | 246 | if info.is_sync_panes_active { 247 | return &self.normal_tab_sync_format; 248 | } 249 | 250 | &self.normal_tab_format 251 | } 252 | 253 | fn render_tab(&self, tab: &TabInfo, panes: &PaneManifest, mode: &ModeInfo) -> String { 254 | let formatters = self.select_format(tab, mode); 255 | let mut output = "".to_owned(); 256 | 257 | for f in formatters.iter() { 258 | let mut content = f.content.clone(); 259 | 260 | let tab_name = match mode.mode { 261 | InputMode::RenameTab => match tab.name.is_empty() { 262 | true => "Enter name...", 263 | false => tab.name.as_str(), 264 | }, 265 | _name => tab.name.as_str(), 266 | }; 267 | 268 | if content.contains("{name}") { 269 | content = content.replace("{name}", tab_name); 270 | } 271 | 272 | if content.contains("{index}") { 273 | content = content.replace("{index}", (tab.position + 1).to_string().as_str()); 274 | } 275 | 276 | if content.contains("{floating_total_count}") { 277 | let panes_for_tab: Vec = 278 | panes.panes.get(&tab.position).cloned().unwrap_or_default(); 279 | 280 | content = content.replace( 281 | "{floating_total_count}", 282 | &format!("{}", panes_for_tab.iter().filter(|p| p.is_floating).count()), 283 | ); 284 | } 285 | 286 | content = self.replace_indicators(content, tab, panes); 287 | 288 | output = format!("{}{}", output, f.format_string(&content)); 289 | } 290 | 291 | output.to_owned() 292 | } 293 | 294 | fn replace_indicators(&self, content: String, tab: &TabInfo, panes: &PaneManifest) -> String { 295 | let mut content = content; 296 | if content.contains("{fullscreen_indicator}") && self.fullscreen_indicator.is_some() { 297 | content = content.replace( 298 | "{fullscreen_indicator}", 299 | if tab.is_fullscreen_active { 300 | self.fullscreen_indicator.as_ref().unwrap() 301 | } else { 302 | "" 303 | }, 304 | ); 305 | } 306 | 307 | if content.contains("{sync_indicator}") && self.sync_indicator.is_some() { 308 | content = content.replace( 309 | "{sync_indicator}", 310 | if tab.is_sync_panes_active { 311 | self.sync_indicator.as_ref().unwrap() 312 | } else { 313 | "" 314 | }, 315 | ); 316 | } 317 | 318 | if content.contains("{floating_indicator}") && self.floating_indicator.is_some() { 319 | let panes_for_tab: Vec = 320 | panes.panes.get(&tab.position).cloned().unwrap_or_default(); 321 | 322 | let is_floating = panes_for_tab.iter().any(|p| p.is_floating); 323 | 324 | content = content.replace( 325 | "{floating_indicator}", 326 | if is_floating { 327 | self.floating_indicator.as_ref().unwrap() 328 | } else { 329 | "" 330 | }, 331 | ); 332 | } 333 | 334 | content 335 | } 336 | } 337 | 338 | pub fn get_tab_window( 339 | tabs: &Vec, 340 | max_count: Option, 341 | ) -> (usize, usize, Vec) { 342 | let max_count = match max_count { 343 | Some(count) => count, 344 | None => return (0, 0, tabs.to_vec()), 345 | }; 346 | 347 | if tabs.len() <= max_count { 348 | return (0, 0, tabs.to_vec()); 349 | } 350 | 351 | let active_index = tabs.iter().position(|t| t.active).expect("no active tab"); 352 | 353 | // active tab is in the last #max_count tabs, so return the last #max_count 354 | if active_index > tabs.len().saturating_sub(max_count) { 355 | return ( 356 | tabs.len().saturating_sub(max_count), 357 | 0, 358 | tabs.iter() 359 | .cloned() 360 | .rev() 361 | .take(max_count) 362 | .rev() 363 | .collect::>(), 364 | ); 365 | } 366 | 367 | // tabs must be truncated 368 | let first_index = active_index.saturating_sub(1); 369 | let last_index = cmp::min(first_index + max_count, tabs.len()); 370 | 371 | ( 372 | first_index, 373 | tabs.len().saturating_sub(last_index), 374 | tabs.as_slice()[first_index..last_index].to_vec(), 375 | ) 376 | } 377 | 378 | #[cfg(test)] 379 | mod test { 380 | use zellij_tile::prelude::TabInfo; 381 | 382 | use super::get_tab_window; 383 | use rstest::rstest; 384 | 385 | #[rstest] 386 | #[case( 387 | vec![ 388 | TabInfo { 389 | active: false, 390 | name: "1".to_owned(), 391 | ..TabInfo::default() 392 | }, 393 | TabInfo { 394 | active: false, 395 | name: "2".to_owned(), 396 | ..TabInfo::default() 397 | }, 398 | TabInfo { 399 | active: true, 400 | name: "3".to_owned(), 401 | ..TabInfo::default() 402 | }, 403 | TabInfo { 404 | active: false, 405 | name: "4".to_owned(), 406 | ..TabInfo::default() 407 | }, 408 | TabInfo { 409 | active: false, 410 | name: "5".to_owned(), 411 | ..TabInfo::default() 412 | }, 413 | ], 414 | Some(3), 415 | (1, 1, vec![ 416 | TabInfo { 417 | active: false, 418 | name: "2".to_owned(), 419 | ..TabInfo::default() 420 | }, 421 | TabInfo { 422 | active: true, 423 | name: "3".to_owned(), 424 | ..TabInfo::default() 425 | }, 426 | TabInfo { 427 | active: false, 428 | name: "4".to_owned(), 429 | ..TabInfo::default() 430 | }, 431 | ] 432 | ) 433 | )] 434 | #[case( 435 | vec![ 436 | TabInfo { 437 | active: true, 438 | name: "1".to_owned(), 439 | ..TabInfo::default() 440 | }, 441 | TabInfo { 442 | active: false, 443 | name: "2".to_owned(), 444 | ..TabInfo::default() 445 | }, 446 | TabInfo { 447 | active: false, 448 | name: "3".to_owned(), 449 | ..TabInfo::default() 450 | }, 451 | TabInfo { 452 | active: false, 453 | name: "4".to_owned(), 454 | ..TabInfo::default() 455 | }, 456 | TabInfo { 457 | active: false, 458 | name: "5".to_owned(), 459 | ..TabInfo::default() 460 | }, 461 | ], 462 | Some(3), 463 | (0, 2, vec![ 464 | TabInfo { 465 | active: true, 466 | name: "1".to_owned(), 467 | ..TabInfo::default() 468 | }, 469 | TabInfo { 470 | active: false, 471 | name: "2".to_owned(), 472 | ..TabInfo::default() 473 | }, 474 | TabInfo { 475 | active: false, 476 | name: "3".to_owned(), 477 | ..TabInfo::default() 478 | }, 479 | ] 480 | ) 481 | )] 482 | #[case( 483 | vec![ 484 | TabInfo { 485 | active: false, 486 | name: "1".to_owned(), 487 | ..TabInfo::default() 488 | }, 489 | TabInfo { 490 | active: true, 491 | name: "2".to_owned(), 492 | ..TabInfo::default() 493 | }, 494 | TabInfo { 495 | active: false, 496 | name: "3".to_owned(), 497 | ..TabInfo::default() 498 | }, 499 | TabInfo { 500 | active: false, 501 | name: "4".to_owned(), 502 | ..TabInfo::default() 503 | }, 504 | TabInfo { 505 | active: false, 506 | name: "5".to_owned(), 507 | ..TabInfo::default() 508 | }, 509 | ], 510 | Some(3), 511 | (0, 2, vec![ 512 | TabInfo { 513 | active: false, 514 | name: "1".to_owned(), 515 | ..TabInfo::default() 516 | }, 517 | TabInfo { 518 | active: true, 519 | name: "2".to_owned(), 520 | ..TabInfo::default() 521 | }, 522 | TabInfo { 523 | active: false, 524 | name: "3".to_owned(), 525 | ..TabInfo::default() 526 | }, 527 | ] 528 | ) 529 | )] 530 | #[case( 531 | vec![ 532 | TabInfo { 533 | active: false, 534 | name: "1".to_owned(), 535 | ..TabInfo::default() 536 | }, 537 | TabInfo { 538 | active: false, 539 | name: "2".to_owned(), 540 | ..TabInfo::default() 541 | }, 542 | TabInfo { 543 | active: false, 544 | name: "3".to_owned(), 545 | ..TabInfo::default() 546 | }, 547 | TabInfo { 548 | active: false, 549 | name: "4".to_owned(), 550 | ..TabInfo::default() 551 | }, 552 | TabInfo { 553 | active: true, 554 | name: "5".to_owned(), 555 | ..TabInfo::default() 556 | }, 557 | ], 558 | Some(3), 559 | (2, 0, vec![ 560 | TabInfo { 561 | active: false, 562 | name: "3".to_owned(), 563 | ..TabInfo::default() 564 | }, 565 | TabInfo { 566 | active: false, 567 | name: "4".to_owned(), 568 | ..TabInfo::default() 569 | }, 570 | TabInfo { 571 | active: true, 572 | name: "5".to_owned(), 573 | ..TabInfo::default() 574 | }, 575 | ] 576 | ) 577 | )] 578 | #[case( 579 | vec![ 580 | TabInfo { 581 | active: false, 582 | name: "1".to_owned(), 583 | ..TabInfo::default() 584 | }, 585 | TabInfo { 586 | active: false, 587 | name: "2".to_owned(), 588 | ..TabInfo::default() 589 | }, 590 | TabInfo { 591 | active: false, 592 | name: "3".to_owned(), 593 | ..TabInfo::default() 594 | }, 595 | TabInfo { 596 | active: true, 597 | name: "4".to_owned(), 598 | ..TabInfo::default() 599 | }, 600 | TabInfo { 601 | active: false, 602 | name: "5".to_owned(), 603 | ..TabInfo::default() 604 | }, 605 | ], 606 | Some(3), 607 | (2, 0, vec![ 608 | TabInfo { 609 | active: false, 610 | name: "3".to_owned(), 611 | ..TabInfo::default() 612 | }, 613 | TabInfo { 614 | active: true, 615 | name: "4".to_owned(), 616 | ..TabInfo::default() 617 | }, 618 | TabInfo { 619 | active: false, 620 | name: "5".to_owned(), 621 | ..TabInfo::default() 622 | }, 623 | ] 624 | ) 625 | )] 626 | #[case( 627 | vec![ 628 | TabInfo { 629 | active: false, 630 | name: "1".to_owned(), 631 | ..TabInfo::default() 632 | }, 633 | TabInfo { 634 | active: false, 635 | name: "2".to_owned(), 636 | ..TabInfo::default() 637 | }, 638 | TabInfo { 639 | active: true, 640 | name: "3".to_owned(), 641 | ..TabInfo::default() 642 | }, 643 | TabInfo { 644 | active: false, 645 | name: "4".to_owned(), 646 | ..TabInfo::default() 647 | }, 648 | TabInfo { 649 | active: false, 650 | name: "5".to_owned(), 651 | ..TabInfo::default() 652 | }, 653 | ], 654 | None, 655 | (0, 0, vec![ 656 | TabInfo { 657 | active: false, 658 | name: "1".to_owned(), 659 | ..TabInfo::default() 660 | }, 661 | TabInfo { 662 | active: false, 663 | name: "2".to_owned(), 664 | ..TabInfo::default() 665 | }, 666 | TabInfo { 667 | active: true, 668 | name: "3".to_owned(), 669 | ..TabInfo::default() 670 | }, 671 | TabInfo { 672 | active: false, 673 | name: "4".to_owned(), 674 | ..TabInfo::default() 675 | }, 676 | TabInfo { 677 | active: false, 678 | name: "5".to_owned(), 679 | ..TabInfo::default() 680 | }, 681 | ] 682 | ) 683 | )] 684 | #[case( 685 | vec![ 686 | TabInfo { 687 | active: false, 688 | name: "1".to_owned(), 689 | ..TabInfo::default() 690 | }, 691 | TabInfo { 692 | active: true, 693 | name: "2".to_owned(), 694 | ..TabInfo::default() 695 | }, 696 | ], 697 | Some(3), 698 | (0, 0, vec![ 699 | TabInfo { 700 | active: false, 701 | name: "1".to_owned(), 702 | ..TabInfo::default() 703 | }, 704 | TabInfo { 705 | active: true, 706 | name: "2".to_owned(), 707 | ..TabInfo::default() 708 | }, 709 | ] 710 | ) 711 | )] 712 | #[case( 713 | vec![ 714 | TabInfo { 715 | active: false, 716 | name: "1".to_owned(), 717 | ..TabInfo::default() 718 | }, 719 | TabInfo { 720 | active: true, 721 | name: "2".to_owned(), 722 | ..TabInfo::default() 723 | }, 724 | TabInfo { 725 | active: false, 726 | name: "3".to_owned(), 727 | ..TabInfo::default() 728 | }, 729 | ], 730 | Some(3), 731 | (0, 0, vec![ 732 | TabInfo { 733 | active: false, 734 | name: "1".to_owned(), 735 | ..TabInfo::default() 736 | }, 737 | TabInfo { 738 | active: true, 739 | name: "2".to_owned(), 740 | ..TabInfo::default() 741 | }, 742 | TabInfo { 743 | active: false, 744 | name: "3".to_owned(), 745 | ..TabInfo::default() 746 | }, 747 | ] 748 | ) 749 | )] 750 | pub fn test_get_tab_window( 751 | #[case] tabs: Vec, 752 | #[case] max_count: Option, 753 | #[case] expected: (usize, usize, Vec), 754 | ) { 755 | let res = get_tab_window(&tabs, max_count); 756 | 757 | assert_eq!(res, expected); 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /src/widgets/widget.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ZellijState; 2 | 3 | pub trait Widget { 4 | fn process(&self, name: &str, state: &ZellijState) -> String; 5 | fn process_click(&self, name: &str, state: &ZellijState, pos: usize); 6 | } 7 | -------------------------------------------------------------------------------- /tests/zjframes/config.kdl: -------------------------------------------------------------------------------- 1 | // If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true" 2 | keybinds { 3 | normal { 4 | // uncomment this and adjust key if using copy_on_select=false 5 | // bind "Alt c" { Copy; } 6 | } 7 | locked { 8 | bind "Ctrl g" { SwitchToMode "Normal"; } 9 | } 10 | resize { 11 | bind "Ctrl n" { SwitchToMode "Normal"; } 12 | bind "h" "Left" { Resize "Increase Left"; } 13 | bind "j" "Down" { Resize "Increase Down"; } 14 | bind "k" "Up" { Resize "Increase Up"; } 15 | bind "l" "Right" { Resize "Increase Right"; } 16 | bind "H" { Resize "Decrease Left"; } 17 | bind "J" { Resize "Decrease Down"; } 18 | bind "K" { Resize "Decrease Up"; } 19 | bind "L" { Resize "Decrease Right"; } 20 | bind "=" "+" { Resize "Increase"; } 21 | bind "-" { Resize "Decrease"; } 22 | } 23 | pane { 24 | bind "Ctrl p" { SwitchToMode "Normal"; } 25 | bind "h" "Left" { MoveFocus "Left"; } 26 | bind "l" "Right" { MoveFocus "Right"; } 27 | bind "j" "Down" { MoveFocus "Down"; } 28 | bind "k" "Up" { MoveFocus "Up"; } 29 | bind "p" { SwitchFocus; } 30 | bind "n" { NewPane; SwitchToMode "Normal"; } 31 | bind "d" { NewPane "Down"; SwitchToMode "Normal"; } 32 | bind "r" { NewPane "Right"; SwitchToMode "Normal"; } 33 | bind "x" { CloseFocus; SwitchToMode "Normal"; } 34 | bind "f" { ToggleFocusFullscreen; SwitchToMode "Normal"; } 35 | bind "z" { TogglePaneFrames; SwitchToMode "Normal"; } 36 | bind "w" { ToggleFloatingPanes; SwitchToMode "Normal"; } 37 | bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "Normal"; } 38 | bind "c" { SwitchToMode "RenamePane"; PaneNameInput 0;} 39 | } 40 | move { 41 | bind "Ctrl h" { SwitchToMode "Normal"; } 42 | bind "n" "Tab" { MovePane; } 43 | bind "p" { MovePaneBackwards; } 44 | bind "h" "Left" { MovePane "Left"; } 45 | bind "j" "Down" { MovePane "Down"; } 46 | bind "k" "Up" { MovePane "Up"; } 47 | bind "l" "Right" { MovePane "Right"; } 48 | } 49 | tab { 50 | bind "Ctrl t" { SwitchToMode "Normal"; } 51 | bind "r" { SwitchToMode "RenameTab"; TabNameInput 0; } 52 | bind "h" "Left" "Up" "k" { GoToPreviousTab; } 53 | bind "l" "Right" "Down" "j" { GoToNextTab; } 54 | bind "n" { NewTab; SwitchToMode "Normal"; } 55 | bind "x" { CloseTab; SwitchToMode "Normal"; } 56 | bind "s" { ToggleActiveSyncTab; SwitchToMode "Normal"; } 57 | bind "b" { BreakPane; SwitchToMode "Normal"; } 58 | bind "]" { BreakPaneRight; SwitchToMode "Normal"; } 59 | bind "[" { BreakPaneLeft; SwitchToMode "Normal"; } 60 | bind "1" { GoToTab 1; SwitchToMode "Normal"; } 61 | bind "2" { GoToTab 2; SwitchToMode "Normal"; } 62 | bind "3" { GoToTab 3; SwitchToMode "Normal"; } 63 | bind "4" { GoToTab 4; SwitchToMode "Normal"; } 64 | bind "5" { GoToTab 5; SwitchToMode "Normal"; } 65 | bind "6" { GoToTab 6; SwitchToMode "Normal"; } 66 | bind "7" { GoToTab 7; SwitchToMode "Normal"; } 67 | bind "8" { GoToTab 8; SwitchToMode "Normal"; } 68 | bind "9" { GoToTab 9; SwitchToMode "Normal"; } 69 | bind "Tab" { ToggleTab; } 70 | } 71 | scroll { 72 | bind "Ctrl s" { SwitchToMode "Normal"; } 73 | bind "e" { EditScrollback; SwitchToMode "Normal"; } 74 | bind "s" { SwitchToMode "EnterSearch"; SearchInput 0; } 75 | bind "Ctrl c" { ScrollToBottom; SwitchToMode "Normal"; } 76 | bind "j" "Down" { ScrollDown; } 77 | bind "k" "Up" { ScrollUp; } 78 | bind "Ctrl f" "PageDown" "Right" "l" { PageScrollDown; } 79 | bind "Ctrl b" "PageUp" "Left" "h" { PageScrollUp; } 80 | bind "d" { HalfPageScrollDown; } 81 | bind "u" { HalfPageScrollUp; } 82 | // uncomment this and adjust key if using copy_on_select=false 83 | // bind "Alt c" { Copy; } 84 | } 85 | search { 86 | bind "Ctrl s" { SwitchToMode "Normal"; } 87 | bind "Ctrl c" { ScrollToBottom; SwitchToMode "Normal"; } 88 | bind "j" "Down" { ScrollDown; } 89 | bind "k" "Up" { ScrollUp; } 90 | bind "Ctrl f" "PageDown" "Right" "l" { PageScrollDown; } 91 | bind "Ctrl b" "PageUp" "Left" "h" { PageScrollUp; } 92 | bind "d" { HalfPageScrollDown; } 93 | bind "u" { HalfPageScrollUp; } 94 | bind "n" { Search "down"; } 95 | bind "p" { Search "up"; } 96 | bind "c" { SearchToggleOption "CaseSensitivity"; } 97 | bind "w" { SearchToggleOption "Wrap"; } 98 | bind "o" { SearchToggleOption "WholeWord"; } 99 | } 100 | entersearch { 101 | bind "Ctrl c" "Esc" { SwitchToMode "Scroll"; } 102 | bind "Enter" { SwitchToMode "Search"; } 103 | } 104 | renametab { 105 | bind "Ctrl c" { SwitchToMode "Normal"; } 106 | bind "Esc" { UndoRenameTab; SwitchToMode "Tab"; } 107 | } 108 | renamepane { 109 | bind "Ctrl c" { SwitchToMode "Normal"; } 110 | bind "Esc" { UndoRenamePane; SwitchToMode "Pane"; } 111 | } 112 | session { 113 | bind "Ctrl o" { SwitchToMode "Normal"; } 114 | bind "Ctrl s" { SwitchToMode "Scroll"; } 115 | bind "d" { Detach; } 116 | bind "w" { 117 | LaunchOrFocusPlugin "session-manager" { 118 | floating true 119 | move_to_focused_tab true 120 | }; 121 | SwitchToMode "Normal" 122 | } 123 | bind "c" { 124 | LaunchOrFocusPlugin "configuration" { 125 | floating true 126 | move_to_focused_tab true 127 | }; 128 | SwitchToMode "Normal" 129 | } 130 | bind "p" { 131 | LaunchOrFocusPlugin "plugin-manager" { 132 | floating true 133 | move_to_focused_tab true 134 | }; 135 | SwitchToMode "Normal" 136 | } 137 | } 138 | tmux { 139 | bind "[" { SwitchToMode "Scroll"; } 140 | bind "Ctrl b" { Write 2; SwitchToMode "Normal"; } 141 | bind "\"" { NewPane "Down"; SwitchToMode "Normal"; } 142 | bind "%" { NewPane "Right"; SwitchToMode "Normal"; } 143 | bind "z" { ToggleFocusFullscreen; SwitchToMode "Normal"; } 144 | bind "c" { NewTab; SwitchToMode "Normal"; } 145 | bind "," { SwitchToMode "RenameTab"; } 146 | bind "p" { GoToPreviousTab; SwitchToMode "Normal"; } 147 | bind "n" { GoToNextTab; SwitchToMode "Normal"; } 148 | bind "Left" { MoveFocus "Left"; SwitchToMode "Normal"; } 149 | bind "Right" { MoveFocus "Right"; SwitchToMode "Normal"; } 150 | bind "Down" { MoveFocus "Down"; SwitchToMode "Normal"; } 151 | bind "Up" { MoveFocus "Up"; SwitchToMode "Normal"; } 152 | bind "h" { MoveFocus "Left"; SwitchToMode "Normal"; } 153 | bind "l" { MoveFocus "Right"; SwitchToMode "Normal"; } 154 | bind "j" { MoveFocus "Down"; SwitchToMode "Normal"; } 155 | bind "k" { MoveFocus "Up"; SwitchToMode "Normal"; } 156 | bind "o" { FocusNextPane; } 157 | bind "d" { Detach; } 158 | bind "Space" { NextSwapLayout; } 159 | bind "x" { CloseFocus; SwitchToMode "Normal"; } 160 | } 161 | shared_except "locked" { 162 | bind "Ctrl g" { SwitchToMode "Locked"; } 163 | bind "Ctrl q" { Quit; } 164 | bind "Alt f" { ToggleFloatingPanes; } 165 | bind "Alt n" { NewPane; } 166 | bind "Alt i" { MoveTab "Left"; } 167 | bind "Alt o" { MoveTab "Right"; } 168 | bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } 169 | bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } 170 | bind "Alt j" "Alt Down" { MoveFocus "Down"; } 171 | bind "Alt k" "Alt Up" { MoveFocus "Up"; } 172 | bind "Alt =" "Alt +" { Resize "Increase"; } 173 | bind "Alt -" { Resize "Decrease"; } 174 | bind "Alt [" { PreviousSwapLayout; } 175 | bind "Alt ]" { NextSwapLayout; } 176 | } 177 | shared_except "normal" "locked" { 178 | bind "Enter" "Esc" { SwitchToMode "Normal"; } 179 | } 180 | shared_except "pane" "locked" { 181 | bind "Ctrl p" { SwitchToMode "Pane"; } 182 | } 183 | shared_except "resize" "locked" { 184 | bind "Ctrl n" { SwitchToMode "Resize"; } 185 | } 186 | shared_except "scroll" "locked" { 187 | bind "Ctrl s" { SwitchToMode "Scroll"; } 188 | } 189 | shared_except "session" "locked" { 190 | bind "Ctrl o" { SwitchToMode "Session"; } 191 | } 192 | shared_except "tab" "locked" { 193 | bind "Ctrl t" { SwitchToMode "Tab"; } 194 | } 195 | shared_except "move" "locked" { 196 | bind "Ctrl h" { SwitchToMode "Move"; } 197 | } 198 | shared_except "tmux" "locked" { 199 | bind "Ctrl b" { SwitchToMode "Tmux"; } 200 | } 201 | } 202 | 203 | // Plugin aliases - can be used to change the implementation of Zellij 204 | // changing these requires a restart to take effect 205 | plugins { 206 | tab-bar location="zellij:tab-bar" 207 | status-bar location="zellij:status-bar" 208 | strider location="zellij:strider" 209 | compact-bar location="zellij:compact-bar" 210 | session-manager location="zellij:session-manager" 211 | welcome-screen location="zellij:session-manager" { 212 | welcome_screen true 213 | } 214 | filepicker location="zellij:strider" { 215 | cwd "/" 216 | } 217 | configuration location="zellij:configuration" 218 | plugin-manager location="zellij:plugin-manager" 219 | } 220 | 221 | // Plugins to load in the background when a new session starts 222 | load_plugins { 223 | "file:./target/wasm32-wasip1/debug/zjframes.wasm" { 224 | hide_frame_for_single_pane "true" 225 | hide_frame_except_for_search "true" 226 | hide_frame_except_for_fullscreen "true" 227 | } 228 | // "file:/path/to/my-plugin.wasm" 229 | // "https://example.com/my-plugin.wasm" 230 | } 231 | 232 | // Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP 233 | // eg. when terminal window with an active zellij session is closed 234 | // (Requires restart) 235 | // Options: 236 | // - detach (Default) 237 | // - quit 238 | // 239 | // on_force_close "quit" 240 | 241 | // Send a request for a simplified ui (without arrow fonts) to plugins 242 | // Options: 243 | // - true 244 | // - false (Default) 245 | // 246 | // simplified_ui true 247 | 248 | // Choose the path to the default shell that zellij will use for opening new panes 249 | // Default: $SHELL 250 | // 251 | // default_shell "fish" 252 | 253 | // Choose the path to override cwd that zellij will use for opening new panes 254 | // 255 | // default_cwd "" 256 | 257 | // Toggle between having pane frames around the panes 258 | // Options: 259 | // - true (default) 260 | // - false 261 | // 262 | pane_frames true 263 | 264 | 265 | ui { 266 | pane_frames { 267 | hide_session_name true 268 | rounded_corners true 269 | } 270 | } 271 | 272 | // Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible 273 | // Options: 274 | // - true (default) 275 | // - false 276 | // 277 | // auto_layout true 278 | 279 | // Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected 280 | // (Requires restart) 281 | // Options: 282 | // - true (default) 283 | // - false 284 | // 285 | session_serialization false 286 | 287 | // Whether pane viewports are serialized along with the session, default is false 288 | // (Requires restart) 289 | // Options: 290 | // - true 291 | // - false (default) 292 | // 293 | // serialize_pane_viewport true 294 | 295 | // Scrollback lines to serialize along with the pane viewport when serializing sessions, 0 296 | // defaults to the scrollback size. If this number is higher than the scrollback size, it will 297 | // also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true. 298 | // (Requires restart) 299 | // 300 | // scrollback_lines_to_serialize 10000 301 | 302 | // Define color themes for Zellij 303 | // For more examples, see: https://github.com/zellij-org/zellij/tree/main/example/themes 304 | // Once these themes are defined, one of them should to be selected in the "theme" section of this file 305 | // 306 | // themes { 307 | // dracula { 308 | // fg 248 248 242 309 | // bg 40 42 54 310 | // red 255 85 85 311 | // green 80 250 123 312 | // yellow 241 250 140 313 | // blue 98 114 164 314 | // magenta 255 121 198 315 | // orange 255 184 108 316 | // cyan 139 233 253 317 | // black 0 0 0 318 | // white 255 255 255 319 | // } 320 | // } 321 | 322 | // Choose the theme that is specified in the themes section. 323 | // Default: default 324 | // 325 | // theme "default" 326 | 327 | // The name of the default layout to load on startup 328 | // Default: "default" 329 | // (Requires restart) 330 | // 331 | // default_layout "compact" 332 | 333 | // Choose the mode that zellij uses when starting up. 334 | // Default: normal 335 | // 336 | // default_mode "locked" 337 | 338 | // Toggle enabling the mouse mode. 339 | // On certain configurations, or terminals this could 340 | // potentially interfere with copying text. 341 | // (Requires restart) 342 | // Options: 343 | // - true (default) 344 | // - false 345 | // 346 | // mouse_mode false 347 | 348 | // Configure the scroll back buffer size 349 | // This is the number of lines zellij stores for each pane in the scroll back 350 | // buffer. Excess number of lines are discarded in a FIFO fashion. 351 | // (Requires restart) 352 | // Valid values: positive integers 353 | // Default value: 10000 354 | // 355 | // scroll_buffer_size 10000 356 | 357 | // Provide a command to execute when copying text. The text will be piped to 358 | // the stdin of the program to perform the copy. This can be used with 359 | // terminal emulators which do not support the OSC 52 ANSI control sequence 360 | // that will be used by default if this option is not set. 361 | // Examples: 362 | // 363 | // copy_command "xclip -selection clipboard" // x11 364 | // copy_command "wl-copy" // wayland 365 | // copy_command "pbcopy" // osx 366 | 367 | // Choose the destination for copied text 368 | // Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. 369 | // Does not apply when using copy_command. 370 | // Options: 371 | // - system (default) 372 | // - primary 373 | // 374 | // copy_clipboard "primary" 375 | 376 | // Enable or disable automatic copy (and clear) of selection when releasing mouse 377 | // Default: true 378 | // 379 | // copy_on_select false 380 | 381 | // Path to the default editor to use to edit pane scrollbuffer 382 | // Default: $EDITOR or $VISUAL 383 | // 384 | // scrollback_editor "/usr/bin/vim" 385 | 386 | // When attaching to an existing session with other users, 387 | // should the session be mirrored (true) 388 | // or should each user have their own cursor (false) 389 | // (Requires restart) 390 | // Default: false 391 | // 392 | // mirror_session true 393 | 394 | // The folder in which Zellij will look for layouts 395 | // (Requires restart) 396 | // 397 | // layout_dir "/path/to/my/layout_dir" 398 | 399 | // The folder in which Zellij will look for themes 400 | // (Requires restart) 401 | // 402 | // theme_dir "/path/to/my/theme_dir" 403 | 404 | // Enable or disable the rendering of styled and colored underlines (undercurl). 405 | // May need to be disabled for certain unsupported terminals 406 | // (Requires restart) 407 | // Default: true 408 | // 409 | // styled_underlines false 410 | 411 | // Enable or disable writing of session metadata to disk (if disabled, other sessions might not know 412 | // metadata info on this session) 413 | // (Requires restart) 414 | // Default: false 415 | // 416 | // disable_session_metadata true 417 | 418 | // Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it) 419 | // (Requires restart) 420 | // Default: true (if the host terminal supports it) 421 | // 422 | // support_kitty_keyboard_protocol false 423 | -------------------------------------------------------------------------------- /tests/zjframes/layout.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane size=1 borderless=true { 3 | plugin location="tab-bar" 4 | } 5 | pane 6 | pane size=1 borderless=true { 7 | plugin location="status-bar" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/zjstatus/config.kdl: -------------------------------------------------------------------------------- 1 | // If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true" 2 | keybinds { 3 | normal { 4 | // uncomment this and adjust key if using copy_on_select=false 5 | // bind "Alt c" { Copy; } 6 | } 7 | locked { 8 | bind "Ctrl g" { SwitchToMode "Normal"; } 9 | } 10 | resize { 11 | bind "Ctrl n" { SwitchToMode "Normal"; } 12 | bind "h" "Left" { Resize "Increase Left"; } 13 | bind "j" "Down" { Resize "Increase Down"; } 14 | bind "k" "Up" { Resize "Increase Up"; } 15 | bind "l" "Right" { Resize "Increase Right"; } 16 | bind "H" { Resize "Decrease Left"; } 17 | bind "J" { Resize "Decrease Down"; } 18 | bind "K" { Resize "Decrease Up"; } 19 | bind "L" { Resize "Decrease Right"; } 20 | bind "=" "+" { Resize "Increase"; } 21 | bind "-" { Resize "Decrease"; } 22 | } 23 | pane { 24 | bind "Ctrl p" { SwitchToMode "Normal"; } 25 | bind "h" "Left" { MoveFocus "Left"; } 26 | bind "l" "Right" { MoveFocus "Right"; } 27 | bind "j" "Down" { MoveFocus "Down"; } 28 | bind "k" "Up" { MoveFocus "Up"; } 29 | bind "p" { SwitchFocus; } 30 | bind "n" { NewPane; SwitchToMode "Normal"; } 31 | bind "d" { NewPane "Down"; SwitchToMode "Normal"; } 32 | bind "r" { NewPane "Right"; SwitchToMode "Normal"; } 33 | bind "x" { CloseFocus; SwitchToMode "Normal"; } 34 | bind "f" { ToggleFocusFullscreen; SwitchToMode "Normal"; } 35 | bind "z" { TogglePaneFrames; SwitchToMode "Normal"; } 36 | bind "w" { ToggleFloatingPanes; SwitchToMode "Normal"; } 37 | bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "Normal"; } 38 | bind "c" { SwitchToMode "RenamePane"; PaneNameInput 0;} 39 | } 40 | move { 41 | bind "Ctrl h" { SwitchToMode "Normal"; } 42 | bind "n" "Tab" { MovePane; } 43 | bind "p" { MovePaneBackwards; } 44 | bind "h" "Left" { MovePane "Left"; } 45 | bind "j" "Down" { MovePane "Down"; } 46 | bind "k" "Up" { MovePane "Up"; } 47 | bind "l" "Right" { MovePane "Right"; } 48 | } 49 | tab { 50 | bind "Ctrl t" { SwitchToMode "Normal"; } 51 | bind "r" { SwitchToMode "RenameTab"; TabNameInput 0; } 52 | bind "h" "Left" "Up" "k" { GoToPreviousTab; } 53 | bind "l" "Right" "Down" "j" { GoToNextTab; } 54 | bind "n" { NewTab; SwitchToMode "Normal"; } 55 | bind "x" { CloseTab; SwitchToMode "Normal"; } 56 | bind "s" { ToggleActiveSyncTab; SwitchToMode "Normal"; } 57 | bind "b" { BreakPane; SwitchToMode "Normal"; } 58 | bind "]" { BreakPaneRight; SwitchToMode "Normal"; } 59 | bind "[" { BreakPaneLeft; SwitchToMode "Normal"; } 60 | bind "1" { GoToTab 1; SwitchToMode "Normal"; } 61 | bind "2" { GoToTab 2; SwitchToMode "Normal"; } 62 | bind "3" { GoToTab 3; SwitchToMode "Normal"; } 63 | bind "4" { GoToTab 4; SwitchToMode "Normal"; } 64 | bind "5" { GoToTab 5; SwitchToMode "Normal"; } 65 | bind "6" { GoToTab 6; SwitchToMode "Normal"; } 66 | bind "7" { GoToTab 7; SwitchToMode "Normal"; } 67 | bind "8" { GoToTab 8; SwitchToMode "Normal"; } 68 | bind "9" { GoToTab 9; SwitchToMode "Normal"; } 69 | bind "Tab" { ToggleTab; } 70 | } 71 | scroll { 72 | bind "Ctrl s" { SwitchToMode "Normal"; } 73 | bind "e" { EditScrollback; SwitchToMode "Normal"; } 74 | bind "s" { SwitchToMode "EnterSearch"; SearchInput 0; } 75 | bind "Ctrl c" { ScrollToBottom; SwitchToMode "Normal"; } 76 | bind "j" "Down" { ScrollDown; } 77 | bind "k" "Up" { ScrollUp; } 78 | bind "Ctrl f" "PageDown" "Right" "l" { PageScrollDown; } 79 | bind "Ctrl b" "PageUp" "Left" "h" { PageScrollUp; } 80 | bind "d" { HalfPageScrollDown; } 81 | bind "u" { HalfPageScrollUp; } 82 | // uncomment this and adjust key if using copy_on_select=false 83 | // bind "Alt c" { Copy; } 84 | } 85 | search { 86 | bind "Ctrl s" { SwitchToMode "Normal"; } 87 | bind "Ctrl c" { ScrollToBottom; SwitchToMode "Normal"; } 88 | bind "j" "Down" { ScrollDown; } 89 | bind "k" "Up" { ScrollUp; } 90 | bind "Ctrl f" "PageDown" "Right" "l" { PageScrollDown; } 91 | bind "Ctrl b" "PageUp" "Left" "h" { PageScrollUp; } 92 | bind "d" { HalfPageScrollDown; } 93 | bind "u" { HalfPageScrollUp; } 94 | bind "n" { Search "down"; } 95 | bind "p" { Search "up"; } 96 | bind "c" { SearchToggleOption "CaseSensitivity"; } 97 | bind "w" { SearchToggleOption "Wrap"; } 98 | bind "o" { SearchToggleOption "WholeWord"; } 99 | } 100 | entersearch { 101 | bind "Ctrl c" "Esc" { SwitchToMode "Scroll"; } 102 | bind "Enter" { SwitchToMode "Search"; } 103 | } 104 | renametab { 105 | bind "Ctrl c" { SwitchToMode "Normal"; } 106 | bind "Esc" { UndoRenameTab; SwitchToMode "Tab"; } 107 | } 108 | renamepane { 109 | bind "Ctrl c" { SwitchToMode "Normal"; } 110 | bind "Esc" { UndoRenamePane; SwitchToMode "Pane"; } 111 | } 112 | session { 113 | bind "Ctrl o" { SwitchToMode "Normal"; } 114 | bind "Ctrl s" { SwitchToMode "Scroll"; } 115 | bind "d" { Detach; } 116 | bind "w" { 117 | LaunchOrFocusPlugin "session-manager" { 118 | floating true 119 | move_to_focused_tab true 120 | }; 121 | SwitchToMode "Normal" 122 | } 123 | bind "c" { 124 | LaunchOrFocusPlugin "configuration" { 125 | floating true 126 | move_to_focused_tab true 127 | }; 128 | SwitchToMode "Normal" 129 | } 130 | bind "p" { 131 | LaunchOrFocusPlugin "plugin-manager" { 132 | floating true 133 | move_to_focused_tab true 134 | }; 135 | SwitchToMode "Normal" 136 | } 137 | } 138 | tmux { 139 | bind "[" { SwitchToMode "Scroll"; } 140 | bind "Ctrl b" { Write 2; SwitchToMode "Normal"; } 141 | bind "\"" { NewPane "Down"; SwitchToMode "Normal"; } 142 | bind "%" { NewPane "Right"; SwitchToMode "Normal"; } 143 | bind "z" { ToggleFocusFullscreen; SwitchToMode "Normal"; } 144 | bind "c" { NewTab; SwitchToMode "Normal"; } 145 | bind "," { SwitchToMode "RenameTab"; } 146 | bind "p" { GoToPreviousTab; SwitchToMode "Normal"; } 147 | bind "n" { GoToNextTab; SwitchToMode "Normal"; } 148 | bind "Left" { MoveFocus "Left"; SwitchToMode "Normal"; } 149 | bind "Right" { MoveFocus "Right"; SwitchToMode "Normal"; } 150 | bind "Down" { MoveFocus "Down"; SwitchToMode "Normal"; } 151 | bind "Up" { MoveFocus "Up"; SwitchToMode "Normal"; } 152 | bind "h" { MoveFocus "Left"; SwitchToMode "Normal"; } 153 | bind "l" { MoveFocus "Right"; SwitchToMode "Normal"; } 154 | bind "j" { MoveFocus "Down"; SwitchToMode "Normal"; } 155 | bind "k" { MoveFocus "Up"; SwitchToMode "Normal"; } 156 | bind "o" { FocusNextPane; } 157 | bind "d" { Detach; } 158 | bind "Space" { NextSwapLayout; } 159 | bind "x" { CloseFocus; SwitchToMode "Normal"; } 160 | } 161 | shared_except "locked" { 162 | bind "Ctrl g" { SwitchToMode "Locked"; } 163 | bind "Ctrl q" { Quit; } 164 | bind "Alt f" { ToggleFloatingPanes; } 165 | bind "Alt n" { NewPane; } 166 | bind "Alt i" { MoveTab "Left"; } 167 | bind "Alt o" { MoveTab "Right"; } 168 | bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } 169 | bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } 170 | bind "Alt j" "Alt Down" { MoveFocus "Down"; } 171 | bind "Alt k" "Alt Up" { MoveFocus "Up"; } 172 | bind "Alt =" "Alt +" { Resize "Increase"; } 173 | bind "Alt -" { Resize "Decrease"; } 174 | bind "Alt [" { PreviousSwapLayout; } 175 | bind "Alt ]" { NextSwapLayout; } 176 | } 177 | shared_except "normal" "locked" { 178 | bind "Enter" "Esc" { SwitchToMode "Normal"; } 179 | } 180 | shared_except "pane" "locked" { 181 | bind "Ctrl p" { SwitchToMode "Pane"; } 182 | } 183 | shared_except "resize" "locked" { 184 | bind "Ctrl n" { SwitchToMode "Resize"; } 185 | } 186 | shared_except "scroll" "locked" { 187 | bind "Ctrl s" { SwitchToMode "Scroll"; } 188 | } 189 | shared_except "session" "locked" { 190 | bind "Ctrl o" { SwitchToMode "Session"; } 191 | } 192 | shared_except "tab" "locked" { 193 | bind "Ctrl t" { SwitchToMode "Tab"; } 194 | } 195 | shared_except "move" "locked" { 196 | bind "Ctrl h" { SwitchToMode "Move"; } 197 | } 198 | shared_except "tmux" "locked" { 199 | bind "Ctrl b" { SwitchToMode "Tmux"; } 200 | } 201 | } 202 | 203 | // Plugin aliases - can be used to change the implementation of Zellij 204 | // changing these requires a restart to take effect 205 | plugins { 206 | tab-bar location="zellij:tab-bar" 207 | status-bar location="zellij:status-bar" 208 | strider location="zellij:strider" 209 | compact-bar location="zellij:compact-bar" 210 | session-manager location="zellij:session-manager" 211 | welcome-screen location="zellij:session-manager" { 212 | welcome_screen true 213 | } 214 | filepicker location="zellij:strider" { 215 | cwd "/" 216 | } 217 | configuration location="zellij:configuration" 218 | plugin-manager location="zellij:plugin-manager" 219 | } 220 | 221 | // Plugins to load in the background when a new session starts 222 | load_plugins { 223 | // "file:/path/to/my-plugin.wasm" 224 | // "https://example.com/my-plugin.wasm" 225 | } 226 | 227 | // Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP 228 | // eg. when terminal window with an active zellij session is closed 229 | // (Requires restart) 230 | // Options: 231 | // - detach (Default) 232 | // - quit 233 | // 234 | // on_force_close "quit" 235 | 236 | // Send a request for a simplified ui (without arrow fonts) to plugins 237 | // Options: 238 | // - true 239 | // - false (Default) 240 | // 241 | // simplified_ui true 242 | 243 | // Choose the path to the default shell that zellij will use for opening new panes 244 | // Default: $SHELL 245 | // 246 | // default_shell "fish" 247 | 248 | // Choose the path to override cwd that zellij will use for opening new panes 249 | // 250 | // default_cwd "" 251 | 252 | // Toggle between having pane frames around the panes 253 | // Options: 254 | // - true (default) 255 | // - false 256 | // 257 | pane_frames false 258 | 259 | // Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible 260 | // Options: 261 | // - true (default) 262 | // - false 263 | // 264 | // auto_layout true 265 | 266 | // Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected 267 | // (Requires restart) 268 | // Options: 269 | // - true (default) 270 | // - false 271 | // 272 | session_serialization false 273 | 274 | // Whether pane viewports are serialized along with the session, default is false 275 | // (Requires restart) 276 | // Options: 277 | // - true 278 | // - false (default) 279 | // 280 | // serialize_pane_viewport true 281 | 282 | // Scrollback lines to serialize along with the pane viewport when serializing sessions, 0 283 | // defaults to the scrollback size. If this number is higher than the scrollback size, it will 284 | // also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true. 285 | // (Requires restart) 286 | // 287 | // scrollback_lines_to_serialize 10000 288 | 289 | // Define color themes for Zellij 290 | // For more examples, see: https://github.com/zellij-org/zellij/tree/main/example/themes 291 | // Once these themes are defined, one of them should to be selected in the "theme" section of this file 292 | // 293 | // themes { 294 | // dracula { 295 | // fg 248 248 242 296 | // bg 40 42 54 297 | // red 255 85 85 298 | // green 80 250 123 299 | // yellow 241 250 140 300 | // blue 98 114 164 301 | // magenta 255 121 198 302 | // orange 255 184 108 303 | // cyan 139 233 253 304 | // black 0 0 0 305 | // white 255 255 255 306 | // } 307 | // } 308 | 309 | // Choose the theme that is specified in the themes section. 310 | // Default: default 311 | // 312 | // theme "default" 313 | 314 | // The name of the default layout to load on startup 315 | // Default: "default" 316 | // (Requires restart) 317 | // 318 | // default_layout "compact" 319 | 320 | // Choose the mode that zellij uses when starting up. 321 | // Default: normal 322 | // 323 | // default_mode "locked" 324 | 325 | // Toggle enabling the mouse mode. 326 | // On certain configurations, or terminals this could 327 | // potentially interfere with copying text. 328 | // (Requires restart) 329 | // Options: 330 | // - true (default) 331 | // - false 332 | // 333 | // mouse_mode false 334 | 335 | // Configure the scroll back buffer size 336 | // This is the number of lines zellij stores for each pane in the scroll back 337 | // buffer. Excess number of lines are discarded in a FIFO fashion. 338 | // (Requires restart) 339 | // Valid values: positive integers 340 | // Default value: 10000 341 | // 342 | // scroll_buffer_size 10000 343 | 344 | // Provide a command to execute when copying text. The text will be piped to 345 | // the stdin of the program to perform the copy. This can be used with 346 | // terminal emulators which do not support the OSC 52 ANSI control sequence 347 | // that will be used by default if this option is not set. 348 | // Examples: 349 | // 350 | // copy_command "xclip -selection clipboard" // x11 351 | // copy_command "wl-copy" // wayland 352 | // copy_command "pbcopy" // osx 353 | 354 | // Choose the destination for copied text 355 | // Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. 356 | // Does not apply when using copy_command. 357 | // Options: 358 | // - system (default) 359 | // - primary 360 | // 361 | // copy_clipboard "primary" 362 | 363 | // Enable or disable automatic copy (and clear) of selection when releasing mouse 364 | // Default: true 365 | // 366 | // copy_on_select false 367 | 368 | // Path to the default editor to use to edit pane scrollbuffer 369 | // Default: $EDITOR or $VISUAL 370 | // 371 | // scrollback_editor "/usr/bin/vim" 372 | 373 | // When attaching to an existing session with other users, 374 | // should the session be mirrored (true) 375 | // or should each user have their own cursor (false) 376 | // (Requires restart) 377 | // Default: false 378 | // 379 | // mirror_session true 380 | 381 | // The folder in which Zellij will look for layouts 382 | // (Requires restart) 383 | // 384 | // layout_dir "/path/to/my/layout_dir" 385 | 386 | // The folder in which Zellij will look for themes 387 | // (Requires restart) 388 | // 389 | // theme_dir "/path/to/my/theme_dir" 390 | 391 | // Enable or disable the rendering of styled and colored underlines (undercurl). 392 | // May need to be disabled for certain unsupported terminals 393 | // (Requires restart) 394 | // Default: true 395 | // 396 | // styled_underlines false 397 | 398 | // Enable or disable writing of session metadata to disk (if disabled, other sessions might not know 399 | // metadata info on this session) 400 | // (Requires restart) 401 | // Default: false 402 | // 403 | // disable_session_metadata true 404 | 405 | // Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it) 406 | // (Requires restart) 407 | // Default: true (if the host terminal supports it) 408 | // 409 | // support_kitty_keyboard_protocol false 410 | -------------------------------------------------------------------------------- /tests/zjstatus/layout.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane 4 | } 5 | 6 | pane size=2 borderless=true { 7 | plugin location="file:target/wasm32-wasip1/debug/zjstatus.wasm" { 8 | color_blue "#89B4FA" 9 | color_yellow "yellow" 10 | color_bg "#181825" 11 | 12 | format_left "{mode}#[fg=$blue,bg=$bg,bold] {session}" 13 | format_center "{tabs} {command_3} {pipe_1}" 14 | format_right "{notifications}{datetime}" 15 | format_space "#[bg=$bg]" 16 | format_precedence "lrc" 17 | format_hide_on_overlength "false" 18 | 19 | notification_format_unread "#[fg=$blue,bg=$bg,blink]  #[fg=$blue,bg=$bg] {message} " 20 | notification_format_no_notifications "#[fg=$blue,bg=$bg,dim]  " 21 | notification_show_interval "10" 22 | 23 | pipe_1_format "#[fg=red] {output}" 24 | 25 | border_enabled "true" 26 | border_char "─" 27 | border_format "#[fg=#6C7086]{char}" 28 | border_position "top" 29 | 30 | swap_layout_format "#[bg=blue,fg=#000000] {name} #[bg=red,fg=black] foo " 31 | swap_layout_hide_if_empty "false" 32 | 33 | hide_frame_for_single_pane "false" 34 | hide_frame_except_for_search "true" 35 | hide_frame_except_for_fullscreen "false" 36 | hide_frame_except_for_scroll "true" 37 | 38 | mode_normal "#[bg=$blue] #[bg=yellow] " 39 | mode_tmux "#[bg=$yellow] " 40 | mode_default_to_mode "tmux" 41 | 42 | tab_normal "#[fg=#6C7086,bg=$bg] {index} {name} {floating_indicator} " 43 | tab_rename "#[fg=#eba0ac,bg=$bg] {index} {name} {floating_indicator} " 44 | tab_normal_fullscreen "#[fg=#6C7086,bg=$bg] {index} {name} [] " 45 | tab_normal_sync "#[fg=#6C7086,bg=$bg] {index} {name} <> " 46 | tab_active "#[fg=#9399B2,bg=$bg,bold,italic] {index} {name} {floating_total_count}{floating_indicator}{sync_indicator}{fullscreen_indicator}" 47 | tab_separator "#[fg=#6C7086,bg=$bg] | " 48 | tab_floating_indicator "F" 49 | tab_sync_indicator "S" 50 | tab_fullscreen_indicator "FS" 51 | tab_display_count "3" 52 | tab_truncate_start_format "#[fg=$blue,bg=$bg]#[bg=$blue,fg=black] +{count} ... #[fg=$bg,bg=$blue]" 53 | tab_truncate_end_format "#[fg=red,bg=$bg] ... +{count} > #[bg=$yellow] " 54 | 55 | command_0_command "echo \"平仮名, ひらがな 📦\"" 56 | command_0_clickaction "bash -c \"zellij --session zjstatus-dev pipe 'zjstatus::notify::hello world!' -n zjstatus\"" 57 | command_0_format "#[fg=colour80] {exit_code} #[fg=colour90] {stdout} " 58 | command_0_interval "1" 59 | 60 | command_1_command "date" 61 | command_1_format "#[fg=blue,reverse,bg=default,us=red,blink,dim,strikethrough] {exit_code} {stdout} " 62 | command_1_interval "1" 63 | 64 | command_git_branch_command "bash -c \"echo $FOO\"" 65 | command_git_branch_cwd "/Users/daniel" 66 | command_git_branch_env { 67 | FOO "1" 68 | BAR "foo" 69 | } 70 | command_git_branch_format "#[fg=red] {stdout} " 71 | command_git_branch_interval "2" 72 | 73 | command_3_command "echo -e \"#[fg=$yellow,italic,bold] foo #[dim,bold,italic] bar \"" 74 | command_3_format "{stdout}" 75 | command_3_interval "10" 76 | command_3_rendermode "dynamic" 77 | 78 | datetime "#[fg=#6C7086,bg=$bg,bold] {format} #[bg=#6C7086,fg=$bg,bold] {time}" 79 | datetime_time_format "%H:%M:%S" 80 | datetime_format "%A, %d %b %Y %H:%M" 81 | datetime_timezone "Europe/Berlin" 82 | } 83 | } 84 | } 85 | --------------------------------------------------------------------------------