├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 | 
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 |
32 |
33 | simple style
34 |
35 |
36 | slanted style
37 |
38 |
39 | example for swapping layouts with zjstatus
40 |
41 |
42 | compact style (thanks to @segaja)
43 |
44 |
45 | conky status (thanks to @benzwt)
46 | conky.conf
47 |
48 |
49 | Demo GIF
50 |
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 = [[[0m[38;2;24;24;37m[48;2;122;168;159m${uptime_short}[38;2;122;168;159m[48;2;152;187;108m[38;2;0;0;0m[48;2;152;187;108m${fs_used_perc}% ${fs_free}[38;2;152;187;108m[48;2;126;156;216m[38;2;0;0;0m[48;2;126;156;216m${loadavg 1}[38;2;126;156;216m[48;2;156;134;191m[38;2;0;0;0m[48;2;156;134;191m${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 |
--------------------------------------------------------------------------------