41 Commits

Author SHA1 Message Date
Jake Stanger
7bf44ca75d chore(release): v0.11.0 2023-04-01 18:36:24 +01:00
Jake Stanger
fb04ceab7d Merge pull request #95 from JakeStanger/feat/module-hover
feat: module hover options
2023-04-01 18:17:52 +01:00
Jake Stanger
102d2478a9 feat: module hover options
Resolves #70.
2023-04-01 13:29:40 +01:00
Jake Stanger
80a414ab67 build: update deps
Resolves #93
2023-04-01 13:12:26 +01:00
Jake Stanger
72ba17add3 Merge pull request #92 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-04-01 11:14:03 +01:00
github-actions[bot]
2b07620847 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/7f5639fa3b68054ca0b062866dc62b22c3f11505' (2023-02-26)
  → 'github:nixos/nixpkgs/e3652e0735fbec227f342712f180f4f21f0594f2' (2023-03-30)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c1df023b1aaded1b65a1f4ad604a98a58ab4db97' (2023-02-28)
  → 'github:oxalica/rust-overlay/aa480d799023141e1b9e5d6108700de63d9ad002' (2023-03-31)
2023-04-01 00:57:22 +00:00
Jake Stanger
ba488ad38f Merge pull request #89 from yavko/fix-hm-module
Fix home manager module, and features
2023-03-29 12:51:55 +01:00
yavko
d0b7bdbafc fix(nix): home manager module, and features 2023-03-29 01:45:40 -07:00
Jake Stanger
0f5ec1fe34 Merge pull request #85 from JakeStanger/refactor/config
Use `universal-config` crate for config
2023-03-19 16:37:20 +00:00
Jake Stanger
6221f7454a refactor: fix new clippy warnings 2023-03-19 16:22:40 +00:00
Jake Stanger
ecdd71a43d refactor(config): use universal-config crate.
XML config is not supported.
2023-03-19 16:22:40 +00:00
Jake Stanger
01a36a9476 build: update gtk deps 2023-03-19 00:14:59 +00:00
Jake Stanger
d4dd8c41ea chore: improve image provider logging 2023-03-04 23:13:35 +00:00
Jake Stanger
83c5dceaa7 chore: clean up println calls 2023-03-04 23:13:22 +00:00
Jake Stanger
711644e190 Merge pull request #81 from JakeStanger/fix/dynamic-string-ordering
Fix dynamic string ordering
2023-03-01 23:20:41 +00:00
Jake Stanger
8cbb73b75e fix(dynamic string): dynamic sections not respecting ordering
Fixes #69.
2023-03-01 23:09:34 +00:00
Jake Stanger
7212bbcf61 refactor(dynamic string): use vec instead of indexmap 2023-03-01 23:09:01 +00:00
Jake Stanger
0125ce5916 docs(examples): update styles example 2023-03-01 20:35:41 +00:00
Jake Stanger
2b26eaf410 docs(clipboard): fix incorrect setting description 2023-03-01 20:35:31 +00:00
Jake Stanger
33676fc4dc ci(nix): fix cachix error 2023-03-01 20:35:12 +00:00
Jake Stanger
7978c48d5c Merge pull request #79 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-03-01 12:55:52 +00:00
Jake Stanger
1d37e010c8 Merge pull request #66 from yavko/add-nix-flags
Add initial nix flags impl
2023-03-01 12:55:25 +00:00
yavko
54b9b28c75 fix: make readme more concise 2023-02-28 19:11:18 -08:00
yavko
3a44d74cf3 style(nix): fmt flake.nix 2023-02-28 19:08:43 -08:00
yavko
b1475a1aff feat(nix): use cargo default features 2023-02-28 19:07:12 -08:00
yavko
b2749fee92 style(nix): fmt flake.nix 2023-02-28 19:07:03 -08:00
yavko
9984b638b5 feat(nix): initial nix feature flags impl 2023-02-28 19:06:53 -08:00
github-actions[bot]
207b60db7e flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/2caf4ef5005ecc68141ecb4aac271079f7371c44' (2023-01-30)
  → 'github:nixos/nixpkgs/7f5639fa3b68054ca0b062866dc62b22c3f11505' (2023-02-26)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/48b1403150c3f5a9aeee8bc4c77c8926f29c6501' (2023-01-31)
  → 'github:oxalica/rust-overlay/c1df023b1aaded1b65a1f4ad604a98a58ab4db97' (2023-02-28)
2023-03-01 01:09:08 +00:00
Jake Stanger
7779c33e0c Merge pull request #77 from JakeStanger/feat/clipboard-manager
Clipboard manager module
2023-02-28 17:54:06 +00:00
Jake Stanger
575d6cc30f feat: new clipboard manager module 2023-02-26 13:42:53 +00:00
Jake Stanger
5bbe64bb86 docs(clock): format table 2023-02-25 14:29:38 +00:00
Jake Stanger
83a49165c4 docs(compiling): add info about build deps 2023-02-25 14:29:38 +00:00
Jake Stanger
d84139a914 refactor: general tidy up
fix clippy warnings from latest stable rust
2023-02-25 14:26:02 +00:00
Jake Stanger
ca4fe422f2 feat(truncate): ability to set fixed length
BREAKING CHANGE: This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
2023-02-25 14:26:02 +00:00
Jake Stanger
1ad1961396 Merge pull request #67 from ttoino/feature/margin
Add configurable margins around bar
2023-02-08 19:42:15 +00:00
toino
d253c4bd7f feat: add configurable margins around bar 2023-02-08 18:47:21 +00:00
Jake Stanger
fbee6e8bd4 style: run fmt 2023-02-08 17:30:09 +00:00
Jake Stanger
7c36f5cb0c docs: fix a couple of issues 2023-02-02 20:37:16 +00:00
Jake Stanger
7dff3e6f8b fix(image): widgets missing names 2023-02-02 20:37:02 +00:00
Jake Stanger
2ac507144b fix: not setting layer shell namespace 2023-02-02 20:36:31 +00:00
JakeStanger
82875cde68 docs: update CHANGELOG.md for v0.10.0 [skip ci] 2023-02-01 22:22:19 +00:00
49 changed files with 2453 additions and 533 deletions

View File

@@ -58,14 +58,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v17
- uses: cachix/install-nix-action@v20
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v11
- uses: cachix/cachix-action@v12
with:
name: jakestanger
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'

View File

@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v16
uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.10.0] - 2023-02-01
### :boom: BREAKING CHANGES
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
This removes the `icon_theme` option from `launcher` and `focused`. You will need to set this at the top of your config instead.
- due to [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
(Missed from #96141d4) The `{icon}` token has been removed from the `music` module due to incompatibility with the new image/icon support. The icon now always displays as a separate widget before the label and should be removed from your formatting string.
### :sparkles: New Features
- [`8691824`](https://github.com/JakeStanger/ironbar/commit/8691824db1a12c3f3589ff8b5315b8dba5cb8aec) - **music**: ability to truncate button text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`07dbf78`](https://github.com/JakeStanger/ironbar/commit/07dbf780105027b533b0bb34c9ae3e4e96f29f4a) - **focused**: ability to truncate label text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`393800a`](https://github.com/JakeStanger/ironbar/commit/393800aaa2093b9257c43fde8bdb8399f26ebc74) - **custom**: image widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b054c17`](https://github.com/JakeStanger/ironbar/commit/b054c17d14628c9188bfa9aed506ea1de3051f9c) - **workspaces**: support for using images in `name_map` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`96141d4`](https://github.com/JakeStanger/ironbar/commit/96141d49907412ea26d23ef30c10ade8b32b89b9) - **music**: support for using images in `name_map`, additional icon options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c347b6c`](https://github.com/JakeStanger/ironbar/commit/c347b6c9449ce4e16e2e133d7dd35544ab9a533c) - add feature flags *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`5772711`](https://github.com/JakeStanger/ironbar/commit/57727111923a419f9b7613103283aa4cf6bd082c) - **music**: remote mpris album art not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5fb4125`](https://github.com/JakeStanger/ironbar/commit/5fb412572f3da60ac482a1960d891f70bc29287b) - **tray**: some init issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`058c8f4`](https://github.com/JakeStanger/ironbar/commit/058c8f4228f9f7faa66cda9dd1636ea32e9de68b) - **hyprland**: issues with tracking workspaces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`51d2c22`](https://github.com/JakeStanger/ironbar/commit/51d2c2279f50add992def0d58cfaa9890ea3d041) - **images**: incorrectly resolving non-files *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`012762e`](https://github.com/JakeStanger/ironbar/commit/012762e10203fb2d58160acdae4dc7ca7689b131) - swap out some code for existing macros *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9750255`](https://github.com/JakeStanger/ironbar/commit/97502559b30c51e77c1dd9a7d794a88756294c83) - **music**: split config code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`15f0857`](https://github.com/JakeStanger/ironbar/commit/15f0857859d5d4a590b60b6b1a4347b4b84a58a1) - replace icon loading with improved general image loading *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1ed3220`](https://github.com/JakeStanger/ironbar/commit/1ed3220733c2dcb7c5e5cbf377b3324d3183609e) - update CHANGELOG.md for v0.9.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - **music**: remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6a39905`](https://github.com/JakeStanger/ironbar/commit/6a39905b4333582fbcda81a66a9b91055333d698) - **compiling**: add missing full stop *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7b23e61`](https://github.com/JakeStanger/ironbar/commit/7b23e61e7dedf2736a30580b6c1aa84e002c462c) - **wiki**: update screenshots and examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.9.0] - 2023-01-28
### :boom: BREAKING CHANGES
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
@@ -194,4 +232,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0

451
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "ironbar"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar"
@@ -9,6 +9,7 @@ description = "Customisable GTK Layer Shell wlroots/sway bar"
default = [
"http",
"config+all",
"clipboard",
"clock",
"music+all",
"sys_info",
@@ -19,10 +20,12 @@ default = [
http = ["dep:reqwest"]
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
"config+json" = ["serde_json"]
"config+yaml" = ["serde_yaml"]
"config+toml" = ["toml"]
"config+corn" = ["libcorn"]
"config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"]
"config+toml" = ["universal-config/toml"]
"config+corn" = ["universal-config/corn"]
clipboard = ["nix"]
clock = ["chrono"]
@@ -42,9 +45,9 @@ workspaces = ["futures-util"]
[dependencies]
# core
gtk = "0.16.0"
gtk-layer-shell = "0.5.0"
glib = "0.16.2"
gtk = "0.17.0"
gtk-layer-shell = "0.6.0"
glib = "0.17.5"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
@@ -54,12 +57,14 @@ strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2"
serde = { version = "1.0.141", features = ["derive"] }
indexmap = "1.9.1"
dirs = "4.0.0"
dirs = "5.0.0"
walkdir = "2.3.2"
notify = { version = "5.0.0", default-features = false }
wayland-client = "0.29.5"
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
universal-config = { version = "0.3.0", default_features = false }
lazy_static = "1.4.0"
async_once = "0.2.6"
cfg-if = "1.0.0"
@@ -67,11 +72,8 @@ cfg-if = "1.0.0"
# http
reqwest = { version = "0.11.14", optional = true }
# config
serde_json = { version = "1.0.82", optional = true }
serde_yaml = { version = "0.9.4", optional = true }
toml = { version = "0.7.0", optional = true }
libcorn = { version = "0.6.1", optional = true }
# clipboard
nix = { version = "0.26.2", optional = true }
# clock
chrono = { version = "0.4.19", optional = true }
@@ -81,15 +83,15 @@ mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.0", optional = true }
# sys_info
sysinfo = { version = "0.27.0", optional = true }
sysinfo = { version = "0.28.4", optional = true }
# tray
stray = { version = "0.1.3", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.0", optional = true }
hyprland = { version = "0.3.1", optional = true }
futures-util = { version = "0.3.21", optional = true }
# shared
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info

View File

@@ -6,12 +6,23 @@ It uses GTK3 and gtk-layer-shell.
The bar can be styled to your liking using CSS and hot-loads style changes.
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
![Screenshot of fully configured bar with MPD widget open](https://f.jstanger.dev/github/ironbar/bar.png)
![Screenshot of fully configured bar with MPD widget open](https://f.jstanger.dev/github/ironbar/bar.png?raw)
## Features
- First-class support for Sway and Hyprland, but should (mostly) work on any wlroots compositor.
- Fully themeable with CSS and hot-loaded styles.
- Support for multiple configuration languages.
- Popups used by widgets to show rich content and controls on click.
- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
## Installation
### Cargo
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh
cargo install ironbar
```
@@ -59,6 +70,8 @@ Here is an example nix flake that uses Ironbar.
enable = true;
config = {};
style = "";
package = inputs.ironbar;
features = ["feature" "another_feature"];
};
}
];
@@ -74,6 +87,8 @@ in case you don't want to compile Ironbar.
### Source
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
@@ -83,7 +98,7 @@ install target/release/ironbar ~/.local/bin/ironbar
```
By default, all features are enabled.
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling) for controlling which features are included.
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
[repo](https://github.com/jakestanger/ironbar)

View File

@@ -9,6 +9,28 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar
```
## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
### Arch
```shell
pacman -S gtk3 gtk-layer-shell
```
### Ubuntu/Debian
```shell
apt install libgtk-3-dev libgtk-layer-shell-dev
```
### Fedora
```shell
dnf install gtk3 gtk-layer-shell
```
## Features
By default, all features are enabled for convenience. This can result in a significant compile time.
@@ -39,6 +61,7 @@ cargo build --release --no-default-features \
| config+toml | Enables configuration support for TOML. |
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
| **Modules** | |
| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |

View File

@@ -272,6 +272,10 @@ The following table lists each of the top-level bar config options:
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
@@ -292,4 +296,6 @@ For information on the `Script` type, and embedding scripts in strings, see [her
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |

View File

@@ -1,10 +1,10 @@
# Guides
- [Compiling from source](compiling)
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
- [Examples](https://github.com/JakeStanger/ironbar/tree/master/examples)
# Examples
@@ -17,6 +17,7 @@
# Modules
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)
- [Focused](focused)

93
docs/modules/Clipboard.md Normal file
View File

@@ -0,0 +1,93 @@
Shows recent clipboard items, allowing you to switch between them to re-copy previous values.
Clicking the icon button opens the popup containing all functionality.
Supports plain text and images.
![Screenshot of clipboard popup open, with two textual values and an image copied. Several other unrelated widgets are visible on the bar.](https://f.jstanger.dev/github/ironbar/clipboard.png?raw)
## Configuration
> Type: `clipboard`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string/image` | `󰨸` | Icon to show on the widget button. |
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
See [here](images) for information on images.
<details>
<summary>JSON</summary>
```json
{
"end": {
"type": "clipboard",
"max_items": 3,
"truncate": {
"mode": "end",
"length": 50
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "clipboard"
max_items = 3
[[end.truncate]]
mode = "end"
length = 50
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: 'clipboard'
max_items: 3
truncate:
mode: 'end'
length: 50
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [ {
type = "clipboard"
max_items = 3
truncate.mode = "end"
truncate.length = 50
} ]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------------------|------------------------------------------------------|
| `#clipboard` | Clipboard widget. |
| `#clipboard .btn` | Clipboard widget button. |
| `#popup-clipboard` | Clipboard popup box. |
| `#popup-clipboard .item` | Clipboard row item inside the popup. |
| `#popup-clipboard .item .btn` | Clipboard row item radio button. |
| `#popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
| `#popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
| `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. |

View File

@@ -69,9 +69,9 @@ end:
## Styling
| Selector | Description |
|-------------------------------|------------------------------------------------------------------------------------|
| `#clock` | Clock widget button |
| `#popup-clock` | Clock popup box |
| Selector | Description |
|--------------------------------|------------------------------------------------------------------------------------|
| `#clock` | Clock widget button |
| `#popup-clock` | Clock popup box |
| `#popup-clock #calendar-clock` | Clock inside the popup |
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |

View File

@@ -41,7 +41,7 @@ For example, the following label would output your system uptime, updated every
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
```
Both polling and watching mode are supported. For more information on script syntax, see [here](script).
Both polling and watching mode are supported. For more information on script syntax, see [here](scripts).
### Commands

View File

@@ -7,14 +7,15 @@ Displays the title and/or icon of the currently focused window.
> Type: `focused`
| Name | Type | Default | Description |
|-------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| Name | Type | Default | Description |
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>

View File

@@ -11,23 +11,24 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
> Type: `music`
| | Type | Default | Description |
|-------------------|---------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string/image` | `` | Icon to show when playing. |
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
| `icons.prev` | `string/image` | `` | Icon to show on previous button. |
| `icons.next` | `string/image` | `` | Icon to show on next button. |
| `icons.volume` | `string/image` | `` | Icon to show under popup volume slider. |
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `` | Icon to show next to artist name. |
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
| | Type | Default | Description |
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string/image` | `` | Icon to show when playing. |
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
| `icons.prev` | `string/image` | `` | Icon to show on previous button. |
| `icons.next` | `string/image` | `` | Icon to show on next button. |
| `icons.volume` | `string/image` | `` | Icon to show under popup volume slider. |
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
See [here](images) for information on images.

View File

@@ -20,8 +20,18 @@ let {
show_icons = true
}
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
$mpd_server = { type = "mpd" host = "chloe:6600" }
$mpris = {
type = "music"
player_type = "mpris"
on_click_middle = "playerctl play-pause"
on_scroll_up = "playerctl volume +5"
on_scroll_down = "playerctl volume -5"
}
$mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
$mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
$sys_info = {
type = "sys_info"
@@ -55,6 +65,8 @@ let {
show_if.interval = 500
}
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
// -- begin custom --
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
@@ -86,10 +98,13 @@ let {
// -- end custom --
$left = [ $workspaces $launcher ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $power_menu $clock ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
}
in {
anchor_to_edges = true
position = "top"
start = $left end = $right
position = "bottom"
icon_theme = "Paper"
start = $left
end = $right
}

View File

@@ -5,7 +5,7 @@
"music_dir": "/home/jake/Music",
"player_type": "mpd",
"truncate": {
"length": 100,
"max_length": 100,
"mode": "end"
},
"type": "music"
@@ -43,6 +43,14 @@
},
"type": "sys_info"
},
{
"max_items": 3,
"truncate": {
"length": 50,
"mode": "end"
},
"type": "clipboard"
},
{
"bar": [
{
@@ -98,16 +106,6 @@
"icon_theme": "Paper",
"position": "bottom",
"start": [
{
"bar": [
{
"size": 32,
"src": "file:///path/to/image.jpg",
"type": "image"
}
],
"type": "custom"
},
{
"all_monitors": false,
"name_map": {

View File

@@ -8,7 +8,7 @@ player_type = 'mpd'
type = 'music'
[end.truncate]
length = 100
max_length = 100
mode = 'end'
[[end]]
@@ -44,6 +44,14 @@ memory = 30
networks = 3
temps = 5
[[end]]
max_items = 3
type = 'clipboard'
[end.truncate]
length = 50
mode = 'end'
[[end]]
class = 'power-menu'
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
@@ -87,14 +95,6 @@ type = 'label'
[[end]]
type = 'clock'
[[start]]
type = 'custom'
[[start.bar]]
size = 32
src = 'file:///path/to/image.jpg'
type = 'image'
[[start]]
all_monitors = false
type = 'workspaces'

View File

@@ -1,50 +1,20 @@
anchor_to_edges: true
icon_theme: Paper
position: bottom
start:
- bar:
- size: 32
src: file:///path/to/image.jpg
type: image
type: custom
- all_monitors: false
name_map:
'1':
'2': icon:firefox
'3':
Code:
Games: icon:steam
type: workspaces
- favorites:
- firefox
- discord
- Steam
show_icons: true
show_names: false
type: launcher
end:
- music_dir: /home/jake/Music
player_type: mpd
truncate:
length: 100
max_length: 100
mode: end
type: music
- host: chloe:6600
player_type: mpd
truncate: end
type: music
- cmd: /home/jake/bin/phone-battery
show_if:
cmd: /home/jake/bin/phone-connected
interval: 500
type: script
- format:
-  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%)
@@ -60,7 +30,11 @@ end:
networks: 3
temps: 5
type: sys_info
- max_items: 3
truncate:
length: 50
mode: end
type: clipboard
- bar:
- label:
name: power-btn
@@ -89,9 +63,23 @@ end:
type: label
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
type: custom
- type: clock
icon_theme: Paper
position: bottom
start:
- all_monitors: false
name_map:
'1':
'2': icon:firefox
'3':
Code:
Games: icon:steam
type: workspaces
- favorites:
- firefox
- discord
- Steam
show_icons: true
show_names: false
type: launcher

View File

@@ -210,3 +210,31 @@
.popup-power-menu .power-btn:hover {
background-color: #1c1c1c;
}
#clipboard * {
font-size: 1.8em;
}
#popup-clipboard {
padding: 1em;
color: white;
}
#popup-clipboard .item {
border-bottom: 1px solid #424242;
}
#popup-clipboard .btn > *:nth-child(2) {
padding: 10px;
}
#popup-clipboard .btn-remove {
background-color: #2d2d2d;
color: white;
font-size: 1.2em;
border-left: 1px solid #424242;
}
#popup-clipboard .btn-remove:hover {
color: #fcc;
}

12
flake.lock generated
View File

@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1675115703,
"narHash": "sha256-4zetAPSyY0D77x+Ww9QBe8RHn1akvIvHJ/kgg8kGDbk=",
"lastModified": 1680213900,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2caf4ef5005ecc68141ecb4aac271079f7371c44",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1675132198,
"narHash": "sha256-izOVjdIfdv0OzcfO9rXX0lfGkQn4tdJ0eNm3P3LYo/o=",
"lastModified": 1680229280,
"narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "48b1403150c3f5a9aeee8bc4c77c8926f29c6501",
"rev": "aa480d799023141e1b9e5d6108700de63d9ad002",
"type": "github"
},
"original": {

View File

@@ -6,9 +6,6 @@
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
#nci.url = "github:yusdacra/nix-cargo-integration";
#nci.inputs.nixpkgs.follows = "nixpkgs";
#nci.inputs.rust-overlay.follows = "rust-overlay";
};
outputs = {
self,
@@ -39,18 +36,16 @@
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
in {
ironbar = rustPlatform.buildRustPackage {
pname = "ironbar";
version = self.rev or "dirty";
src = builtins.path {
name = "ironbar";
path = prev.lib.cleanSource ./.;
};
cargoDeps = rustPlatform.importCargoLock {lockFile = ./Cargo.lock;};
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with prev; [pkg-config];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl];
ironbar = prev.callPackage ./nix/default.nix {
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
inherit rustPlatform;
};
};
packages = genSystems (
@@ -95,49 +90,61 @@
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use";
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar";
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The config to pass to ironbar";
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [];
description = "The features to be used.";
};
};
config = lib.mkIf cfg.enable {
home.packages = [cfg.package];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
config = let
pkg = cfg.package.override {features = cfg.features;};
in
lib.mkIf cfg.enable {
home.packages = [pkg];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
};
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
];
};
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
];
};
};
};
};
nixConfig = {
extra-substituters = ["https://jakestanger.cachix.org"];
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
};
}

36
nix/default.nix Normal file
View File

@@ -0,0 +1,36 @@
{
gtk3,
gdk-pixbuf,
gtk-layer-shell,
libxkbcommon,
openssl,
pkg-config,
rustPlatform,
lib,
version ? "git",
features ? [],
}:
rustPlatform.buildRustPackage {
inherit version;
pname = "ironbar";
src = builtins.path {
name = "ironbar";
path = lib.cleanSource ../.;
};
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
nativeBuildInputs = [pkg-config];
buildInputs = [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl];
meta = with lib; {
homepage = "https://github.com/JakeStanger/ironbar";
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "Hyprland";
};
}

View File

@@ -1,5 +1,5 @@
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, ModuleConfig};
use crate::config::{BarPosition, CommonConfig, MarginConfig, ModuleConfig};
use crate::dynamic_string::DynamicString;
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext};
use crate::popup::Popup;
@@ -24,7 +24,13 @@ pub fn create_bar(
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
setup_layer_shell(&win, monitor, config.position, config.anchor_to_edges);
setup_layer_shell(
&win,
monitor,
config.position,
config.anchor_to_edges,
config.margin,
);
let orientation = config.position.get_orientation();
@@ -79,16 +85,18 @@ fn setup_layer_shell(
monitor: &Monitor,
position: BarPosition,
anchor_to_edges: bool,
margin: MarginConfig,
) {
gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor);
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
gtk_layer_shell::auto_exclusive_zone_enable(win);
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
let bar_orientation = position.get_orientation();
@@ -185,7 +193,7 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist");
let widget = create_module($module, $id, &info, &Arc::clone(&popup))?;
let widget = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
let container = wrap_widget(&widget);
content.add(&container);
@@ -195,6 +203,8 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
for (id, config) in modules.into_iter().enumerate() {
match config {
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
@@ -281,6 +291,10 @@ fn setup_receiver<TSend>(
) where
TSend: Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
channel.recv(move |ev| {
match ev {
ModuleUpdateEvent::Update(update) => {
@@ -298,6 +312,12 @@ fn setup_receiver<TSend>(
} else {
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
@@ -307,6 +327,12 @@ fn setup_receiver<TSend>(
popup.hide();
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
@@ -371,12 +397,7 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
if let Some(script) = script {
trace!("Running on-click script: {}", event.button());
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
run_script(script);
}
Inhibit(false)
@@ -386,8 +407,6 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
container.connect_scroll_event(move |_, event| {
println!("{:?}", event.direction());
let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(),
@@ -396,12 +415,27 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
if let Some(script) = script {
trace!("Running on-scroll script: {}", event.direction());
run_script(script);
}
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
Inhibit(false)
});
let mouse_enter_script = common.on_mouse_enter.map(Script::new_polling);
container.connect_enter_notify_event(move |_, _| {
if let Some(ref script) = mouse_enter_script {
trace!("Running enter script");
run_script(script);
}
Inhibit(false)
});
let mouse_exit_script = common.on_mouse_exit.map(Script::new_polling);
container.connect_leave_notify_event(move |_, _| {
if let Some(ref script) = mouse_exit_script {
trace!("Running exit script");
run_script(script);
}
Inhibit(false)
@@ -414,3 +448,11 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
});
}
}
fn run_script(script: &Script) {
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
}

244
src/clients/clipboard.rs Normal file
View File

@@ -0,0 +1,244 @@
use super::wayland::{self, ClipboardItem};
use crate::{lock, try_send};
use indexmap::map::Iter;
use indexmap::IndexMap;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Debug)]
pub enum ClipboardEvent {
Add(Arc<ClipboardItem>),
Remove(usize),
Activate(usize),
}
type EventSender = mpsc::Sender<ClipboardEvent>;
/// Clipboard client singleton,
/// to ensure bars don't duplicate requests to the compositor.
pub struct ClipboardClient {
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
cache: Arc<Mutex<ClipboardCache>>,
}
impl ClipboardClient {
fn new() -> Self {
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
{
let senders = senders.clone();
let cache = cache.clone();
spawn(async move {
let mut rx = {
let wl = wayland::get_client().await;
wl.subscribe_clipboard()
};
while let Ok(item) = rx.recv().await {
debug!("Received clipboard item (ID: {})", item.id);
let (existing_id, cache_size) = {
let cache = lock!(cache);
(cache.contains(&item), cache.len())
};
existing_id.map_or_else(
|| {
{
let mut cache = lock!(cache);
let senders = lock!(senders);
cache.insert(item.clone(), senders.len());
}
let senders = lock!(senders);
let iter = senders.iter();
for (tx, sender_cache_size) in iter {
if cache_size == *sender_cache_size {
let mut cache = lock!(cache);
let removed_id = cache
.remove_ref_first()
.expect("Clipboard cache unexpectedly empty");
try_send!(tx, ClipboardEvent::Remove(removed_id));
}
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
},
|existing_id| {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(existing_id));
}
},
);
}
});
}
Self { senders, cache }
}
pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
let (tx, rx) = mpsc::channel(16);
let wl = wayland::get_client().await;
wl.roundtrip();
{
let mut cache = lock!(self.cache);
if let Some(item) = wl.get_clipboard() {
cache.insert_or_inc_ref(item);
}
let iter = cache.iter();
for (_, (item, _)) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
}
{
let mut senders = lock!(self.senders);
senders.push((tx, cache_size));
}
rx
}
pub async fn copy(&self, id: usize) {
debug!("Copying item with id {id}");
let item = {
let cache = lock!(self.cache);
cache.get(id)
};
if let Some(item) = item {
let wl = wayland::get_client().await;
wl.copy_to_clipboard(item);
}
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(id));
}
}
pub fn remove(&self, id: usize) {
let mut cache = lock!(self.cache);
cache.remove(id);
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Remove(id));
}
}
}
/// Shared clipboard item cache.
///
/// Items are stored with a number of references,
/// allowing different consumers to 'remove' cached items
/// at different times.
#[derive(Debug)]
struct ClipboardCache {
cache: IndexMap<usize, (Arc<ClipboardItem>, usize)>,
}
impl ClipboardCache {
/// Creates a new empty cache.
fn new() -> Self {
Self {
cache: IndexMap::new(),
}
}
/// Gets the entry with key `id` from the cache.
fn get(&self, id: usize) -> Option<Arc<ClipboardItem>> {
self.cache.get(&id).map(|(item, _)| item).cloned()
}
/// Inserts an entry with `ref_count` initial references.
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
self.cache
.insert(item.id, (item, ref_count))
.map(|(item, _)| item)
}
/// Inserts an entry with `ref_count` initial references,
/// or increments the `ref_count` by 1 if it already exists.
fn insert_or_inc_ref(&mut self, item: Arc<ClipboardItem>) {
let mut item = self.cache.entry(item.id).or_insert((item, 0));
item.1 += 1;
}
/// Removes the entry with key `id`.
/// This ignores references.
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
self.cache.shift_remove(&id).map(|(item, _)| item)
}
/// Removes a reference to the entry with key `id`.
///
/// If the reference count reaches zero, the entry
/// is removed from the cache.
fn remove_ref(&mut self, id: usize) {
if let Some(entry) = self.cache.get_mut(&id) {
entry.1 -= 1;
if entry.1 == 0 {
self.cache.shift_remove(&id);
}
}
}
/// Removes a reference to the first entry.
///
/// If the reference count reaches zero, the entry
/// is removed from the cache.
fn remove_ref_first(&mut self) -> Option<usize> {
if let Some((id, _)) = self.cache.first() {
let id = *id;
self.remove_ref(id);
Some(id)
} else {
None
}
}
/// Checks if an item with matching mime type and value
/// already exists in the cache.
fn contains(&self, item: &ClipboardItem) -> Option<usize> {
self.cache.values().find_map(|(it, _)| {
if it.mime_type == item.mime_type && it.value == item.value {
Some(it.id)
} else {
None
}
})
}
/// Gets the current number of items in the cache.
fn len(&self) -> usize {
self.cache.len()
}
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, usize)> {
self.cache.iter()
}
}
lazy_static! {
static ref CLIENT: ClipboardClient = ClipboardClient::new();
}
pub fn get_client() -> &'static ClipboardClient {
&CLIENT
}

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "music")]

View File

@@ -1,31 +1,61 @@
use super::toplevel::{ToplevelEvent, ToplevelInfo};
use super::toplevel_manager::listen_for_toplevels;
use super::ToplevelChange;
use super::{Env, ToplevelHandler};
use crate::{error as err, send, write_lock};
use super::wlr_foreign_toplevel::{
handle::{ToplevelEvent, ToplevelInfo},
manager::listen_for_toplevels,
};
use super::{DData, Env, ToplevelHandler};
use crate::{error as err, send};
use cfg_if::cfg_if;
use color_eyre::Report;
use indexmap::IndexMap;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
use smithay_client_toolkit::reexports::calloop;
use smithay_client_toolkit::{new_default_environment, WaylandSource};
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::WaylandSource;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::sync::{broadcast, oneshot};
use tokio::task::spawn_blocking;
use tracing::{error, trace};
use tracing::{debug, error};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{ConnectError, Display, EventQueue};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
};
cfg_if! {
if #[cfg(feature = "clipboard")] {
use super::{ClipboardItem};
use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler};
use crate::{read_lock, write_lock};
use tokio::spawn;
}
}
#[derive(Debug)]
pub enum Request {
/// Copies the value to the clipboard
#[cfg(feature = "clipboard")]
CopyToClipboard(Arc<ClipboardItem>),
/// Forces a dispatch, flushing any currently queued events
Refresh,
}
pub struct WaylandClient {
pub outputs: Vec<OutputInfo>,
pub seats: Vec<WlSeat>,
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
#[cfg(feature = "clipboard")]
clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>,
request_tx: Sender<Request>,
}
impl WaylandClient {
@@ -35,21 +65,44 @@ impl WaylandClient {
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
let toplevel_tx2 = toplevel_tx.clone();
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
let toplevels2 = toplevels.clone();
// `queue` is not send so we need to handle everything inside the task
let toplevel_tx2 = toplevel_tx.clone();
cfg_if! {
if #[cfg(feature = "clipboard")] {
let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32);
let clipboard = Arc::new(RwLock::new(None));
let clipboard_tx2 = clipboard_tx.clone();
}
}
let (ev_tx, ev_rx) = channel::<Request>();
// `queue` is not `Send` so we need to handle everything inside the task
spawn_blocking(move || {
let toplevels = toplevels2;
let toplevel_tx = toplevel_tx2;
let (env, _display, queue) =
new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()])
.expect("Failed to connect to Wayland compositor");
Self::new_environment().expect("Failed to connect to Wayland compositor");
let mut event_loop =
EventLoop::<DData>::try_new().expect("Failed to create new event loop");
WaylandSource::new(queue)
.quick_insert(event_loop.handle())
.expect("Failed to insert Wayland event queue into event loop");
let outputs = Self::get_outputs(&env);
send!(output_tx, outputs);
let seats = env.get_all_seats();
// TODO: Actually handle seats properly
#[cfg(feature = "clipboard")]
let default_seat = seats[0].detach();
send!(
seat_tx,
seats
@@ -58,30 +111,56 @@ impl WaylandClient {
.collect::<Vec<WlSeat>>()
);
let handle = event_loop.handle();
handle
.insert_source(ev_rx, move |event, _metadata, ddata| {
// let env = &ddata.env;
match event {
Event::Msg(Request::Refresh) => debug!("Received refresh event"),
#[cfg(feature = "clipboard")]
Event::Msg(Request::CopyToClipboard(value)) => {
super::wlr_data_control::copy_to_clipboard(
&ddata.env,
&default_seat,
&value,
)
.expect("Failed to copy to clipboard");
}
Event::Closed => panic!("Channel unexpectedly closed"),
}
})
.expect("Failed to insert channel into event queue");
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
let _listener = listen_for_toplevels(env, move |handle, event, _ddata| {
trace!("Received toplevel event: {:?}", event);
if event.change == ToplevelChange::Close {
write_lock!(toplevels2).remove(&event.toplevel.id);
} else {
write_lock!(toplevels2)
.insert(event.toplevel.id, (event.toplevel.clone(), handle));
}
send!(toplevel_tx2, event);
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
super::wlr_foreign_toplevel::update_toplevels(
&toplevels,
handle,
event,
&toplevel_tx,
);
});
let mut event_loop =
calloop::EventLoop::<()>::try_new().expect("Failed to create new event loop");
WaylandSource::new(queue)
.quick_insert(event_loop.handle())
.expect("Failed to insert event loop into wayland event queue");
cfg_if! {
if #[cfg(feature = "clipboard")] {
let clipboard_tx = clipboard_tx2;
let handle = event_loop.handle();
let _offer_listener = listen_to_devices(&env, move |_seat, event, ddata| {
debug!("Received clipboard event");
super::wlr_data_control::receive_offer(event, &handle, clipboard_tx.clone(), ddata);
});
}
}
let mut data = DData {
env,
offer_tokens: HashMap::new(),
};
loop {
// TODO: Avoid need for duration here - can we force some event when sending requests?
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
if let Err(err) = event_loop.dispatch(None, &mut data) {
error!(
"{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
@@ -90,6 +169,18 @@ impl WaylandClient {
}
});
// keep track of current clipboard item
#[cfg(feature = "clipboard")]
{
let clipboard = clipboard.clone();
spawn(async move {
while let Ok(item) = clipboard_rx.recv().await {
let mut clipboard = write_lock!(clipboard);
clipboard.replace(item);
}
});
}
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
@@ -97,9 +188,14 @@ impl WaylandClient {
Self {
outputs,
seats,
#[cfg(feature = "clipboard")]
clipboard,
toplevels,
toplevel_tx,
_toplevel_rx: toplevel_rx,
#[cfg(feature = "clipboard")]
clipboard_tx,
request_tx: ev_tx,
}
}
@@ -107,6 +203,26 @@ impl WaylandClient {
self.toplevel_tx.subscribe()
}
#[cfg(feature = "clipboard")]
pub fn subscribe_clipboard(&self) -> broadcast::Receiver<Arc<ClipboardItem>> {
self.clipboard_tx.subscribe()
}
pub fn roundtrip(&self) {
send!(self.request_tx, Request::Refresh);
}
#[cfg(feature = "clipboard")]
pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> {
let clipboard = read_lock!(self.clipboard);
clipboard.as_ref().cloned()
}
#[cfg(feature = "clipboard")]
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
send!(self.request_tx, Request::CopyToClipboard(item));
}
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
let outputs = env.get_all_outputs();
@@ -115,4 +231,57 @@ impl WaylandClient {
.filter_map(|output| with_output_info(output, Clone::clone))
.collect()
}
fn new_environment() -> Result<(Environment<Env>, Display, EventQueue), ConnectError> {
Display::connect_to_env().and_then(|display| {
let mut queue = display.create_event_queue();
let ret = {
let mut sctk_seats = smithay_client_toolkit::seat::SeatHandler::new();
let sctk_data_device_manager =
smithay_client_toolkit::data_device::DataDeviceHandler::init(&mut sctk_seats);
#[cfg(feature = "clipboard")]
let data_control_device = DataControlDeviceHandler::init(&mut sctk_seats);
let sctk_primary_selection_manager =
smithay_client_toolkit::primary_selection::PrimarySelectionHandler::init(
&mut sctk_seats,
);
let display = ::smithay_client_toolkit::reexports::client::Proxy::clone(&display);
let env = Environment::new(
&display.attach(queue.token()),
&mut queue,
Env {
sctk_compositor: smithay_client_toolkit::environment::SimpleGlobal::new(),
sctk_subcompositor: smithay_client_toolkit::environment::SimpleGlobal::new(
),
sctk_shm: smithay_client_toolkit::shm::ShmHandler::new(),
sctk_outputs: smithay_client_toolkit::output::OutputHandler::new(),
sctk_seats,
sctk_data_device_manager,
sctk_primary_selection_manager,
toplevel: ToplevelHandler::init(),
#[cfg(feature = "clipboard")]
data_control_device,
},
);
if let Ok(env) = env.as_ref() {
let _psm = env.get_primary_selection_manager();
}
env
};
match ret {
Ok(env) => Ok((env, display, queue)),
Err(_e) => display.protocol_error().map_or_else(
|| Err(ConnectError::NoCompositorListening),
|perr| {
panic!("[SCTK] A protocol error occured during initial setup: {perr}");
},
),
}
})
}
}

View File

@@ -1,21 +1,32 @@
mod client;
mod toplevel;
mod toplevel_manager;
extern crate smithay_client_toolkit as sctk;
mod wlr_foreign_toplevel;
use std::collections::HashMap;
use async_once::AsyncOnce;
use lazy_static::lazy_static;
pub use toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
use toplevel_manager::{ToplevelHandler, ToplevelHandling, ToplevelStatusListener};
use wayland_client::{Attached, DispatchData, Interface};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
};
use std::fmt::Debug;
use cfg_if::cfg_if;
use smithay_client_toolkit::default_environment;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
use wayland_client::{Attached, Interface};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
pub use wlr_foreign_toplevel::handle::{ToplevelChange, ToplevelEvent, ToplevelInfo};
use wlr_foreign_toplevel::manager::{ToplevelHandler};
pub use client::WaylandClient;
cfg_if! {
if #[cfg(feature = "clipboard")] {
mod wlr_data_control;
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use wlr_data_control::manager::DataControlDeviceHandler;
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
}
}
/// A utility for lazy-loading globals.
/// Taken from `smithay_client_toolkit` where it's not exposed
#[derive(Debug)]
@@ -25,21 +36,32 @@ enum LazyGlobal<I: Interface> {
Bound(Attached<I>),
}
sctk::default_environment!(Env,
fields = [
toplevel: ToplevelHandler
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel
],
);
pub struct DData {
env: Environment<Env>,
offer_tokens: HashMap<u128, RegistrationToken>,
}
impl ToplevelHandling for Env {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
self.toplevel.listen(f)
cfg_if! {
if #[cfg(feature = "clipboard")] {
default_environment!(Env,
fields = [
toplevel: ToplevelHandler,
data_control_device: DataControlDeviceHandler
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
ZwlrDataControlManagerV1 => data_control_device
],
);
} else {
default_environment!(Env,
fields = [
toplevel: ToplevelHandler,
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
],
);
}
}

View File

@@ -0,0 +1,88 @@
use super::offer::DataControlOffer;
use super::source::DataControlSource;
use crate::lock;
use std::sync::{Arc, Mutex};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Attached, DispatchData, Main};
use wayland_protocols::wlr::unstable::data_control::v1::client::{
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
};
#[derive(Debug)]
struct Inner {
offer: Option<Arc<DataControlOffer>>,
}
impl Inner {
fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) {
self.offer.replace(Arc::new(DataControlOffer::new(offer)));
}
}
#[derive(Debug, Clone)]
pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>);
fn data_control_device_implem<F>(
event: Event,
inner: &mut Inner,
implem: &mut F,
ddata: DispatchData,
) where
F: FnMut(DataControlDeviceEvent, DispatchData),
{
match event {
Event::DataOffer { id } => {
inner.new_offer(&id);
}
Event::Selection { id: Some(offer) } => {
let inner_offer = inner
.offer
.clone()
.expect("Offer should exist at this stage");
if offer == inner_offer.offer {
implem(DataControlDeviceEvent(inner_offer), ddata);
}
}
_ => {}
}
}
pub struct DataControlDevice {
device: ZwlrDataControlDeviceV1,
_inner: Arc<Mutex<Inner>>,
}
impl DataControlDevice {
pub fn init_for_seat<F>(
manager: &Attached<ZwlrDataControlManagerV1>,
seat: &WlSeat,
mut callback: F,
) -> Self
where
F: FnMut(DataControlDeviceEvent, DispatchData) + 'static,
{
let inner = Arc::new(Mutex::new(Inner { offer: None }));
let device = manager.get_data_device(seat);
{
let inner = inner.clone();
device.quick_assign(move |_handle, event, ddata| {
let mut inner = lock!(inner);
data_control_device_implem(event, &mut inner, &mut callback, ddata);
});
}
Self {
device: device.detach(),
_inner: inner,
}
}
pub fn set_selection(&self, source: &Option<DataControlSource>) {
self.device
.set_selection(source.as_ref().map(|s| &s.source));
}
}

View File

@@ -0,0 +1,253 @@
use super::device::{DataControlDevice, DataControlDeviceEvent};
use super::source::DataControlSource;
use smithay_client_toolkit::data_device::WritePipe;
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use smithay_client_toolkit::seat::{SeatHandling, SeatListener};
use smithay_client_toolkit::MissingGlobal;
use std::cell::RefCell;
use std::rc::{self, Rc};
use tracing::warn;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Attached, DispatchData};
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
enum DataControlDeviceHandlerInner {
Ready {
manager: Attached<ZwlrDataControlManagerV1>,
devices: Vec<(WlSeat, DataControlDevice)>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
Pending {
seats: Vec<WlSeat>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
}
impl DataControlDeviceHandlerInner {
fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
let (seats, status_listeners) = if let Self::Pending {
seats,
status_listeners,
} = self
{
(std::mem::take(seats), status_listeners.clone())
} else {
warn!("Ignoring second zwlr_data_control_manager_v1");
return;
};
let mut devices = Vec::new();
for seat in seats {
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(&manager, &seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
}
*self = Self::Ready {
manager,
devices,
status_listeners,
};
}
fn get_manager(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
match self {
Self::Ready { manager, .. } => Some(manager.clone()),
Self::Pending { .. } => None,
}
}
fn new_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready {
manager,
devices,
status_listeners,
} => {
if devices.iter().any(|(s, _)| s == seat) {
// the seat already exists, nothing to do
return;
}
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(manager, seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
}
Self::Pending { seats, .. } => {
seats.push(seat.clone());
}
}
}
fn remove_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready { devices, .. } => devices.retain(|(s, _)| s != seat),
Self::Pending { seats, .. } => seats.retain(|s| s != seat),
}
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
match self {
Self::Ready { manager, .. } => {
let source = DataControlSource::new(manager, mime_types, callback);
Some(source)
}
Self::Pending { .. } => None,
}
}
fn with_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
match self {
Self::Ready { devices, .. } => {
let device = devices
.iter()
.find_map(|(s, device)| if s == seat { Some(device) } else { None });
device.map_or(Err(MissingGlobal), |device| {
f(device);
Ok(())
})
}
Self::Pending { .. } => Err(MissingGlobal),
}
}
}
pub struct DataControlDeviceHandler {
inner: Rc<RefCell<DataControlDeviceHandlerInner>>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
_seat_listener: SeatListener,
}
impl DataControlDeviceHandler {
pub fn init<S>(seat_handler: &mut S) -> Self
where
S: SeatHandling,
{
let status_listeners = Rc::new(RefCell::new(Vec::new()));
let inner = Rc::new(RefCell::new(DataControlDeviceHandlerInner::Pending {
seats: Vec::new(),
status_listeners: status_listeners.clone(),
}));
let seat_inner = inner.clone();
let seat_listener = seat_handler.listen(move |seat, seat_data, _| {
if seat_data.defunct {
seat_inner.borrow_mut().remove_seat(&seat);
} else {
seat_inner.borrow_mut().new_seat(&seat);
}
});
Self {
inner,
_seat_listener: seat_listener,
status_listeners,
}
}
}
impl GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
fn created(
&mut self,
registry: Attached<WlRegistry>,
id: u32,
version: u32,
_ddata: DispatchData,
) {
// data control manager is supported until version 2
let version = std::cmp::min(version, 2);
let manager = registry.bind::<ZwlrDataControlManagerV1>(version, id);
self.inner.borrow_mut().init_manager((*manager).clone());
}
fn get(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
RefCell::borrow(&self.inner).get_manager()
}
}
type DataControlDeviceStatusCallback =
dyn FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
/// Notifies the callbacks of an event on the data device
fn notify_status_listeners(
seat: &WlSeat,
event: &DataControlDeviceEvent,
mut ddata: DispatchData,
listeners: &RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>,
) {
listeners.borrow_mut().retain(|lst| {
rc::Weak::upgrade(lst).map_or(false, |cb| {
(cb.borrow_mut())(seat.clone(), event.clone(), ddata.reborrow());
true
})
});
}
pub struct DataControlDeviceStatusListener {
_cb: Rc<RefCell<DataControlDeviceStatusCallback>>,
}
pub trait DataControlDeviceHandling {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice);
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static;
}
impl DataControlDeviceHandling for DataControlDeviceHandler {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
DataControlDeviceStatusListener { _cb: rc }
}
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
RefCell::borrow(&self.inner).with_device(seat, f)
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
RefCell::borrow(&self.inner).create_source(mime_types, callback)
}
}
pub fn listen_to_devices<E, F>(env: &Environment<E>, f: F) -> DataControlDeviceStatusListener
where
E: DataControlDeviceHandling,
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
}

View File

@@ -0,0 +1,258 @@
pub mod device;
pub mod manager;
pub mod offer;
pub mod source;
use super::Env;
use crate::clients::wayland::DData;
use crate::send;
use color_eyre::Report;
use device::{DataControlDevice, DataControlDeviceEvent};
use glib::Bytes;
use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener};
use smithay_client_toolkit::data_device::WritePipe;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::reexports::calloop::LoopHandle;
use smithay_client_toolkit::MissingGlobal;
use source::DataControlSource;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use tokio::sync::broadcast;
use tracing::{debug, error, trace};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::DispatchData;
static COUNTER: AtomicUsize = AtomicUsize::new(1);
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone, Eq)]
pub struct ClipboardItem {
pub id: usize,
pub value: ClipboardValue,
pub mime_type: String,
}
impl PartialEq<Self> for ClipboardItem {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClipboardValue {
Text(String),
Image(Bytes),
Other,
}
impl DataControlDeviceHandling for Env {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
self.data_control_device.listen(f)
}
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
self.data_control_device.with_data_control_device(seat, f)
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
self.data_control_device.create_source(mime_types, callback)
}
}
pub fn copy_to_clipboard<E>(
env: &Environment<E>,
seat: &WlSeat,
item: &ClipboardItem,
) -> Result<(), MissingGlobal>
where
E: DataControlDeviceHandling,
{
debug!("Copying item with id {} [{}]", item.id, item.mime_type);
trace!("Copying: {item:?}");
let item = item.clone();
env.with_inner(|env| {
let mime_types = vec![INTERNAL_MIME_TYPE.to_string(), item.mime_type];
let source = env.create_source(mime_types, move |mime_type, mut pipe, _ddata| {
debug!(
"Triggering source callback for item with id {} [{}]",
item.id, mime_type
);
// FIXME: Not working for large (buffered) values in xwayland
let bytes = match &item.value {
ClipboardValue::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(
io::ErrorKind::Other,
"Attempted to copy unsupported mime type",
)
),
};
if let Err(err) = pipe.write_all(bytes) {
error!("{err:?}");
}
});
env.with_data_control_device(seat, |device| device.set_selection(&source))
})
}
#[derive(Debug)]
struct MimeType {
value: String,
category: MimeTypeCategory,
}
#[derive(Debug)]
enum MimeTypeCategory {
Text,
Image,
}
impl MimeType {
fn parse(mime_types: &[String]) -> Option<Self> {
mime_types
.iter()
.map(|s| s.to_lowercase())
.find_map(|mime_type| match mime_type.as_str() {
"text"
| "string"
| "utf8_string"
| "text/plain"
| "text/plain;charset=utf-8"
| "text/plain;charset=iso-8859-1"
| "text/plain;charset=us-ascii"
| "text/plain;charset=unicode" => Some(Self {
value: mime_type,
category: MimeTypeCategory::Text,
}),
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
| "image/x-bmp" | "image/icon" => Some(Self {
value: mime_type,
category: MimeTypeCategory::Image,
}),
_ => None,
})
}
}
pub fn receive_offer(
event: DataControlDeviceEvent,
handle: &LoopHandle<DData>,
tx: broadcast::Sender<Arc<ClipboardItem>>,
mut ddata: DispatchData,
) {
let timestamp = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Could not get epoch, system time is probably very wrong")
.as_nanos();
let offer = event.0;
let ddata = ddata
.get::<DData>()
.expect("Expected dispatch data to exist");
let handle2 = handle.clone();
let res = offer.with_mime_types(|mime_types| {
debug!("Offer mime types: {mime_types:?}");
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
debug!("Skipping value provided by bar");
return Ok(());
}
let mime_type = MimeType::parse(mime_types);
debug!("Detected mime type: {mime_type:?}");
match mime_type {
Some(mime_type) => {
debug!("[{timestamp}] Sending clipboard read request ({mime_type:?})");
let read_pipe = offer.receive(mime_type.value.clone())?;
let source = handle.insert_source(read_pipe, move |(), file, ddata| {
debug!(
"[{timestamp}] Reading clipboard contents ({:?})",
&mime_type.category
);
match read_file(&mime_type, file) {
Ok(item) => {
send!(tx, Arc::new(item));
}
Err(err) => error!("{err:?}"),
}
if let Some(src) = ddata.offer_tokens.remove(&timestamp) {
handle2.remove(src);
}
})?;
ddata.offer_tokens.insert(timestamp, source);
}
None => {
// send an event so the clipboard module is aware it's changed
send!(
tx,
Arc::new(ClipboardItem {
id: usize::MAX,
mime_type: String::new(),
value: ClipboardValue::Other
})
);
}
}
Ok::<(), Report>(())
});
if let Err(err) = res {
error!("{err:?}");
}
}
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
let value = match mime_type.category {
MimeTypeCategory::Text => {
let mut txt = String::new();
file.read_to_string(&mut txt)?;
ClipboardValue::Text(txt)
}
MimeTypeCategory::Image => {
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
let bytes = Bytes::from(&bytes);
ClipboardValue::Image(bytes)
}
};
Ok(ClipboardItem {
id: get_id(),
value,
mime_type: mime_type.value.clone(),
})
}

View File

@@ -0,0 +1,74 @@
use crate::lock;
use nix::fcntl::OFlag;
use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device::ReadPipe;
use std::io;
use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex};
use tracing::warn;
use wayland_client::Main;
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1,
};
#[derive(Debug, Clone)]
struct Inner {
mime_types: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DataControlOffer {
inner: Arc<Mutex<Inner>>,
pub(crate) offer: ZwlrDataControlOfferV1,
}
impl DataControlOffer {
pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> Self {
let inner = Arc::new(Mutex::new(Inner {
mime_types: Vec::new(),
}));
{
let inner = inner.clone();
offer.quick_assign(move |_, event, _| {
let mut inner = lock!(inner);
if let Event::Offer { mime_type } = event {
inner.mime_types.push(mime_type);
}
});
}
Self {
offer: offer.detach(),
inner,
}
}
pub fn with_mime_types<F, T>(&self, f: F) -> T
where
F: FnOnce(&[String]) -> T,
{
let inner = lock!(self.inner);
f(&inner.mime_types)
}
pub fn receive(&self, mime_type: String) -> io::Result<ReadPipe> {
// create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
self.offer.receive(mime_type, writefd);
if let Err(err) = close(writefd) {
warn!("Failed to close write pipe: {}", err);
}
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
}
}
impl Drop for DataControlOffer {
fn drop(&mut self) {
self.offer.destroy();
}
}

View File

@@ -0,0 +1,54 @@
use smithay_client_toolkit::data_device::WritePipe;
use std::os::fd::FromRawFd;
use wayland_client::{Attached, DispatchData};
use wayland_protocols::wlr::unstable::data_control::v1::client::{
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1},
};
fn data_control_source_impl<F>(
source: &ZwlrDataControlSourceV1,
event: Event,
implem: &mut F,
ddata: DispatchData,
) where
F: FnMut(String, WritePipe, DispatchData),
{
match event {
Event::Send { mime_type, fd } => {
let pipe = unsafe { FromRawFd::from_raw_fd(fd) };
implem(mime_type, pipe, ddata);
}
Event::Cancelled => source.destroy(),
_ => unreachable!(),
}
}
pub struct DataControlSource {
pub(crate) source: ZwlrDataControlSourceV1,
}
impl DataControlSource {
pub fn new<F>(
manager: &Attached<ZwlrDataControlManagerV1>,
mime_types: Vec<String>,
mut callback: F,
) -> Self
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
let source = manager.create_data_source();
source.quick_assign(move |source, evt, ddata| {
data_control_source_impl(&source, evt, &mut callback, ddata);
});
for mime_type in mime_types {
source.offer(mime_type);
}
Self {
source: source.detach(),
}
}
}

View File

@@ -1,9 +1,8 @@
use super::toplevel::{Toplevel, ToplevelEvent};
use super::LazyGlobal;
use super::handle::{Toplevel, ToplevelEvent};
use crate::wayland::LazyGlobal;
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use std::cell::RefCell;
use std::rc;
use std::rc::Rc;
use std::rc::{self, Rc};
use tracing::warn;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::{Attached, DispatchData};
@@ -155,7 +154,7 @@ impl ToplevelHandling for ToplevelHandler {
}
}
pub fn listen_for_toplevels<E, F>(env: Environment<E>, f: F) -> ToplevelStatusListener
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener
where
E: ToplevelHandling,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,

View File

@@ -0,0 +1,39 @@
use std::sync::RwLock;
use indexmap::IndexMap;
use tokio::sync::broadcast::Sender;
use tracing::trace;
use super::Env;
use handle::{ToplevelEvent, ToplevelChange, ToplevelInfo};
use manager::{ToplevelHandling, ToplevelStatusListener};
use wayland_client::DispatchData;
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use crate::{send, write_lock};
pub mod handle;
pub mod manager;
impl ToplevelHandling for Env {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
self.toplevel.listen(f)
}
}
pub fn update_toplevels(
toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>,
handle: ZwlrForeignToplevelHandleV1,
event: ToplevelEvent,
tx: &Sender<ToplevelEvent>,
) {
trace!("Received toplevel event: {:?}", event);
if event.change == ToplevelChange::Close {
write_lock!(toplevels).remove(&event.toplevel.id);
} else {
write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
}
send!(tx, event);
}

View File

@@ -1,18 +1,12 @@
use super::{BarPosition, Config, MonitorConfig};
use color_eyre::eyre::Result;
use color_eyre::eyre::{ContextCompat, WrapErr};
use color_eyre::{Help, Report};
use dirs::config_dir;
use gtk::Orientation;
use serde::{Deserialize, Deserializer};
use std::path::{Path, PathBuf};
use std::{env, fs};
use tracing::instrument;
// Manually implement for better untagged enum error handling:
// currently open pr: https://github.com/serde-rs/serde/pull/1544
impl<'de> Deserialize<'de> for MonitorConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
@@ -62,87 +56,3 @@ impl BarPosition {
}
}
}
impl Config {
/// Attempts to load the config file from file,
/// parse it and return a new instance of `Self`.
pub fn load() -> Result<Self> {
let config_path = env::var("IRONBAR_CONFIG").map_or_else(
|_| Self::try_find_config(),
|config_path| {
let path = PathBuf::from(config_path);
if path.exists() {
Ok(path)
} else {
Err(Report::msg(format!(
"Specified config file does not exist: {}",
path.display()
))
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
}
},
)?;
Self::load_file(&config_path)
}
/// Attempts to discover the location of the config file
/// by checking each valid format's extension.
///
/// Returns the path of the first valid match, if any.
#[instrument]
fn try_find_config() -> Result<PathBuf> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
let file = extensions.into_iter().find_map(|extension| {
let full_path = config_dir
.join("ironbar")
.join(format!("config.{extension}"));
if Path::exists(&full_path) {
Some(full_path)
} else {
None
}
});
file.map_or_else(
|| {
Err(Report::msg("Could not find config file")
.suggestion("Ironbar does not include a configuration out of the box")
.suggestion("A guide on writing a config can be found on the wiki:")
.suggestion("https://github.com/JakeStanger/ironbar/wiki/configuration-guide"))
},
Ok,
)
}
/// Loads the config file at the specified path
/// and parses it into `Self` based on its extension.
fn load_file(path: &Path) -> Result<Self> {
let file = fs::read(path).wrap_err("Failed to read config file")?;
let str = std::str::from_utf8(&file)?;
let extension = path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
match extension {
#[cfg(feature = "config+json")]
"json" => serde_json::from_str(str).wrap_err("Invalid JSON config"),
#[cfg(feature = "config+toml")]
"toml" => toml::from_str(str).wrap_err("Invalid TOML config"),
#[cfg(feature = "config+yaml")]
"yaml" | "yml" => serde_yaml::from_str(str).wrap_err("Invalid YAML config"),
#[cfg(feature = "config+corn")]
"corn" => libcorn::from_str(str).wrap_err("Invalid Corn config"),
_ => Err(Report::msg(format!("Unsupported config type: {extension}"))
.note("You may need to recompile with support if available")),
}
}
}

View File

@@ -1,6 +1,8 @@
mod r#impl;
mod truncate;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule;
@@ -30,6 +32,8 @@ pub struct CommonConfig {
pub on_click_middle: Option<ScriptInput>,
pub on_scroll_up: Option<ScriptInput>,
pub on_scroll_down: Option<ScriptInput>,
pub on_mouse_enter: Option<ScriptInput>,
pub on_mouse_exit: Option<ScriptInput>,
pub tooltip: Option<String>,
}
@@ -38,19 +42,21 @@ pub struct CommonConfig {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "clock")]
Clock(ClockModule),
Custom(CustomModule),
Focused(FocusedModule),
Launcher(LauncherModule),
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
Clock(Box<ClockModule>),
Custom(Box<CustomModule>),
Focused(Box<FocusedModule>),
Launcher(Box<LauncherModule>),
#[cfg(feature = "music")]
Music(MusicModule),
Script(ScriptModule),
Music(Box<MusicModule>),
Script(Box<ScriptModule>),
#[cfg(feature = "sys_info")]
SysInfo(SysInfoModule),
SysInfo(Box<SysInfoModule>),
#[cfg(feature = "tray")]
Tray(TrayModule),
Tray(Box<TrayModule>),
#[cfg(feature = "workspaces")]
Workspaces(WorkspacesModule),
Workspaces(Box<WorkspacesModule>),
}
#[derive(Debug, Clone)]
@@ -74,14 +80,28 @@ impl Default for BarPosition {
}
}
#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
pub struct MarginConfig {
#[serde(default)]
pub bottom: i32,
#[serde(default)]
pub left: i32,
#[serde(default)]
pub right: i32,
#[serde(default)]
pub top: i32,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default_bar_position")]
#[serde(default)]
pub position: BarPosition,
#[serde(default = "default_true")]
pub anchor_to_edges: bool,
#[serde(default = "default_bar_height")]
pub height: i32,
#[serde(default)]
pub margin: MarginConfig,
/// GTK icon theme to use.
pub icon_theme: Option<String>,
@@ -93,10 +113,6 @@ pub struct Config {
pub monitors: Option<HashMap<String, MonitorConfig>>,
}
const fn default_bar_position() -> BarPosition {
BarPosition::Bottom
}
const fn default_bar_height() -> i32 {
42
}

View File

@@ -24,31 +24,43 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
#[serde(untagged)]
pub enum TruncateMode {
Auto(EllipsizeMode),
MaxLength {
Length {
mode: EllipsizeMode,
length: Option<i32>,
max_length: Option<i32>,
},
}
impl TruncateMode {
const fn mode(&self) -> EllipsizeMode {
match self {
Self::MaxLength { mode, .. } | Self::Auto(mode) => *mode,
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
}
}
const fn length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::MaxLength { length, .. } => *length,
Self::Length { length, .. } => *length,
}
}
const fn max_length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::Length { max_length, .. } => *max_length,
}
}
pub fn truncate_label(&self, label: &gtk::Label) {
label.set_ellipsize(self.mode().into());
if let Some(max_length) = self.length() {
label.set_max_width_chars(max_length);
if let Some(length) = self.length() {
label.set_width_chars(length);
}
if let Some(length) = self.max_length() {
label.set_max_width_chars(length);
}
}
}

View File

@@ -1,7 +1,6 @@
use crate::script::{OutputStream, Script};
use crate::{lock, send};
use gtk::prelude::*;
use indexmap::IndexMap;
use std::sync::{Arc, Mutex};
use tokio::spawn;
@@ -60,31 +59,30 @@ impl DynamicString {
chars.drain(..skip);
}
let label_parts = Arc::new(Mutex::new(IndexMap::new()));
let label_parts = Arc::new(Mutex::new(Vec::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in segments.into_iter().enumerate() {
match segment {
DynamicStringSegment::Static(str) => {
lock!(label_parts).insert(i, str);
lock!(label_parts).push(str);
}
DynamicStringSegment::Dynamic(script) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
// insert blank value to preserve segment order
lock!(label_parts).push(String::new());
spawn(async move {
script
.run(|(out, _)| {
if let OutputStream::Stdout(out) = out {
let mut label_parts = lock!(label_parts);
label_parts.insert(i, out);
let string = label_parts
.iter()
.map(|(_, part)| part.as_str())
.collect::<String>();
let _ = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join("");
send!(tx, string);
}
})
@@ -96,11 +94,7 @@ impl DynamicString {
// initialize
{
let label_parts = lock!(label_parts)
.iter()
.map(|(_, part)| part.as_str())
.collect::<String>();
let label_parts = lock!(label_parts).join("");
send!(tx, label_parts);
}

View File

@@ -3,12 +3,14 @@ use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error;
#[cfg(any(feature = "music", feature = "workspaces"))]
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
let button = Button::new();
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
image.set_widget_name("image");
match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone()))
{
@@ -34,6 +36,8 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
image.set_widget_name("image");
container.add(&image);
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
@@ -43,6 +47,8 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
}
} else {
let label = Label::new(Some(input));
label.set_widget_name("label");
container.add(&label);
}

View File

@@ -1,4 +1,4 @@
#[cfg(any(feature = "music", feature = "workspaces"))]
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
mod gtk;
mod provider;

View File

@@ -89,7 +89,7 @@ impl<'a> ImageProvider<'a> {
Ok(ImageLocation::Local(PathBuf::from(input_name)))
}
None => get_desktop_icon_name(input_name).map_or_else(
|| Err(Report::msg("Unknown image type")),
|| Err(Report::msg(format!("Unknown image type: '{input}'"))),
|input| Self::get_location(&input, theme, size),
),
}
@@ -132,16 +132,16 @@ impl<'a> ImageProvider<'a> {
});
}
} else {
self.load_into_image_sync(image)?;
self.load_into_image_sync(&image)?;
};
#[cfg(not(feature = "http"))]
self.load_into_image_sync(image)?;
self.load_into_image_sync(&image)?;
Ok(())
}
fn load_into_image_sync(&self, image: gtk::Image) -> Result<()> {
fn load_into_image_sync(&self, image: &gtk::Image) -> Result<()> {
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
ImageLocation::Local(path) => self.get_from_file(path),

View File

@@ -32,6 +32,7 @@ use tokio::task::block_in_place;
use crate::error::ExitCode;
use clients::wayland::{self, WaylandClient};
use tracing::{debug, error, info};
use universal_config::ConfigLoader;
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -57,13 +58,19 @@ async fn main() -> Result<()> {
|display| display,
);
let config = match Config::load() {
let config_res = match env::var("IRONBAR_CONFIG") {
Ok(path) => ConfigLoader::load(path),
Err(_) => ConfigLoader::new("ironbar").find_and_load(),
};
let config = match config_res {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
exit(ExitCode::Config as i32)
}
};
debug!("Loaded config file");
if let Err(err) = create_bars(app, &display, wayland_client, &config) {

315
src/modules/clipboard.rs Normal file
View File

@@ -0,0 +1,315 @@
use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct ClipboardModule {
#[serde(default = "default_icon")]
icon: String,
#[serde(default = "default_max_items")]
max_items: usize,
// -- Common --
truncate: Option<TruncateMode>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_icon() -> String {
String::from("󰨸")
}
const fn default_max_items() -> usize {
10
}
#[derive(Debug, Clone)]
pub enum ControllerEvent {
Add(usize, Arc<ClipboardItem>),
Remove(usize),
Activate(usize),
Deactivate,
}
#[derive(Debug, Clone)]
pub enum UIEvent {
Copy(usize),
Remove(usize),
}
impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent;
fn name() -> &'static str {
"clipboard"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()> {
let max_items = self.max_items;
// listen to clipboard events
spawn(async move {
let mut rx = {
let client = clipboard::get_client();
client.subscribe(max_items).await
};
while let Some(event) = rx.recv().await {
match event {
ClipboardEvent::Add(item) => {
let msg = match &item.value {
ClipboardValue::Other => {
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
}
_ => ModuleUpdateEvent::Update(ControllerEvent::Add(item.id, item)),
};
try_send!(tx, msg);
}
ClipboardEvent::Remove(id) => {
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Remove(id)));
}
ClipboardEvent::Activate(id) => {
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Activate(id)));
}
}
}
error!("Clipboard client unexpectedly closed");
});
// listen to ui events
spawn(async move {
while let Some(event) = rx.recv().await {
let client = clipboard::get_client();
match event {
UIEvent::Copy(id) => client.copy(id).await,
UIEvent::Remove(id) => client.remove(id),
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
let button = new_icon_button(&self.icon, info.icon_theme, 32);
button.style_context().add_class("btn");
button.connect_clicked(move |button| {
let pos = Popup::button_pos(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
});
// we need to bind to the receiver as the channel does not open
// until the popup is first opened.
context.widget_rx.attach(None, |_| Continue(true));
Ok(ModuleWidget {
widget: button,
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
.spacing(10)
.name("popup-clipboard")
.build();
let entries = gtk::Box::new(Orientation::Vertical, 5);
container.add(&entries);
let hidden_option = RadioButton::new();
entries.add(&hidden_option);
let mut items = HashMap::new();
{
let hidden_option = hidden_option.clone();
rx.attach(None, move |event| {
match event {
ControllerEvent::Add(id, item) => {
debug!("Adding new value with ID {}", id);
let row = gtk::Box::new(Orientation::Horizontal, 0);
row.style_context().add_class("item");
let button = match &item.value {
ClipboardValue::Text(value) => {
let button = RadioButton::from_widget(&hidden_option);
let label = Label::new(Some(value));
button.add(&label);
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
button.style_context().add_class("text");
button
}
ClipboardValue::Image(bytes) => {
let stream = MemoryInputStream::from_bytes(bytes);
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
128,
64,
true,
Some(&Cancellable::new()),
)
.expect("Failed to read Pixbuf from stream");
let image = Image::from_pixbuf(Some(&pixbuf));
let button = RadioButton::from_widget(&hidden_option);
button.set_image(Some(&image));
button.set_always_show_image(true);
button.style_context().add_class("image");
button
}
ClipboardValue::Other => unreachable!(),
};
button.style_context().add_class("btn");
button.set_active(true); // if just added, should be on clipboard
let button_wrapper = EventBox::new();
button_wrapper.add(&button);
button_wrapper.set_widget_name(&format!("copy-{id}"));
button_wrapper.set_above_child(true);
{
let tx = tx.clone();
button_wrapper.connect_button_press_event(
move |button_wrapper, event| {
// left click
if event.button() == 1 {
let id = get_button_id(button_wrapper)
.expect("Failed to get id from button name");
debug!("Copying item with id: {id}");
try_send!(tx, UIEvent::Copy(id));
}
Inhibit(true)
},
);
}
let remove_button = Button::with_label("x");
remove_button.set_widget_name(&format!("remove-{id}"));
remove_button.style_context().add_class("btn-remove");
{
let tx = tx.clone();
let entries = entries.clone();
let row = row.clone();
remove_button.connect_clicked(move |button| {
let id = get_button_id(button)
.expect("Failed to get id from button name");
debug!("Removing item with id: {id}");
try_send!(tx, UIEvent::Remove(id));
entries.remove(&row);
});
}
row.add(&button_wrapper);
row.pack_end(&remove_button, false, false, 0);
entries.add(&row);
entries.reorder_child(&row, 0);
row.show_all();
items.insert(id, (row, button));
}
ControllerEvent::Remove(id) => {
debug!("Removing option with ID {id}");
let row = items.remove(&id);
if let Some((row, button)) = row {
if button.is_active() {
hidden_option.set_active(true);
}
entries.remove(&row);
}
}
ControllerEvent::Activate(id) => {
debug!("Activating option with ID {id}");
hidden_option.set_active(false);
let row = items.get(&id);
if let Some((_, button)) = row {
button.set_active(true);
}
}
ControllerEvent::Deactivate => {
debug!("Deactivating current option");
hidden_option.set_active(true);
}
}
Continue(true)
});
}
container.show_all();
hidden_option.hide();
Some(container)
}
}
/// Gets the ID from a widget's name.
///
/// This expects the button name to be
/// in the format `<purpose>-<id>`.
fn get_button_id<W>(button_wrapper: &W) -> Option<usize>
where
W: IsA<Widget>,
{
button_wrapper
.widget_name()
.split_once('-')
.and_then(|(_, id)| id.parse().ok())
}

View File

@@ -116,7 +116,7 @@ impl Widget {
let mut builder = Label::builder().use_markup(true);
if let Some(name) = self.name {
builder = builder.name(&name);
builder = builder.name(name);
}
let label = builder.build();
@@ -143,7 +143,7 @@ impl Widget {
let mut builder = Button::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
builder = builder.name(name);
}
let button = builder.build();

View File

@@ -110,9 +110,9 @@ impl Module<gtk::Box> for LauncherModule {
let wl = wayland::get_client().await;
let open_windows = read_lock!(wl.toplevels);
let mut items = lock!(items);
for (_, (window, _)) in open_windows.clone() {
let open_windows = open_windows.clone();
for (_, (window, _)) in open_windows {
let mut items = lock!(items);
let item = items.get_mut(&window.app_id);
match item {
Some(item) => {
@@ -124,6 +124,7 @@ impl Module<gtk::Box> for LauncherModule {
}
}
let items = lock!(items);
let items = items.iter();
for (_, item) in items {
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
@@ -281,7 +282,7 @@ impl Module<gtk::Box> for LauncherModule {
ItemEvent::FocusItem(app_id) => items
.get(&app_id)
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
ItemEvent::FocusWindow(id) => Some(id),
ItemEvent::FocusWindow(id) => Some(id), // FIXME: Broken on wlroots-git
ItemEvent::OpenItem(_) => unreachable!(),
};
@@ -292,6 +293,9 @@ impl Module<gtk::Box> for LauncherModule {
handle.activate(seat);
};
}
// roundtrip to immediately send activate event
wl.roundtrip();
}
}
});
@@ -434,7 +438,7 @@ impl Module<gtk::Box> for LauncherModule {
.into_iter()
.map(|(_, win)| {
let button = Button::builder()
.label(&clamp(&win.name))
.label(clamp(&win.name))
.height_request(40)
.build();
@@ -464,7 +468,7 @@ impl Module<gtk::Box> for LauncherModule {
if let Some(buttons) = buttons.get_mut(&app_id) {
let button = Button::builder()
.height_request(40)
.label(&clamp(&win.name))
.label(clamp(&win.name))
.build();
{

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
///
/// A custom date/time format string can be set in the config.

View File

@@ -361,16 +361,19 @@ fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System
// no refresh required for these tokens
let load_average = sys.load_average();
format_info.insert(String::from("load_average:1"), load_average.one.to_string());
format_info.insert(
String::from("load_average:1"),
format!("{:.2}", load_average.one),
);
format_info.insert(
String::from("load_average:5"),
load_average.five.to_string(),
format!("{:.2}", load_average.five),
);
format_info.insert(
String::from("load_average:15"),
load_average.fifteen.to_string(),
format!("{:.2}", load_average.fifteen),
);
let uptime = Duration::from_secs(sys.uptime()).as_secs();

View File

@@ -29,6 +29,7 @@ impl Popup {
gtk_layer_shell::init_for_window(&win);
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
gtk_layer_shell::set_margin(
&win,