Compare commits
38 Commits
v0.10.0
...
feat/volum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7a761484 | ||
|
|
72ba17add3 | ||
|
|
2b07620847 | ||
|
|
ba488ad38f | ||
|
|
d0b7bdbafc | ||
|
|
0f5ec1fe34 | ||
|
|
6221f7454a | ||
|
|
ecdd71a43d | ||
|
|
01a36a9476 | ||
|
|
d4dd8c41ea | ||
|
|
83c5dceaa7 | ||
|
|
711644e190 | ||
|
|
8cbb73b75e | ||
|
|
7212bbcf61 | ||
|
|
0125ce5916 | ||
|
|
2b26eaf410 | ||
|
|
33676fc4dc | ||
|
|
7978c48d5c | ||
|
|
1d37e010c8 | ||
|
|
54b9b28c75 | ||
|
|
3a44d74cf3 | ||
|
|
b1475a1aff | ||
|
|
b2749fee92 | ||
|
|
9984b638b5 | ||
|
|
207b60db7e | ||
|
|
7779c33e0c | ||
|
|
575d6cc30f | ||
|
|
5bbe64bb86 | ||
|
|
83a49165c4 | ||
|
|
d84139a914 | ||
|
|
ca4fe422f2 | ||
|
|
1ad1961396 | ||
|
|
d253c4bd7f | ||
|
|
fbee6e8bd4 | ||
|
|
7c36f5cb0c | ||
|
|
7dff3e6f8b | ||
|
|
2ac507144b | ||
|
|
82875cde68 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -58,14 +58,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@v17
|
- uses: cachix/install-nix-action@v20
|
||||||
with:
|
with:
|
||||||
install_url: https://nixos.org/nix/install
|
install_url: https://nixos.org/nix/install
|
||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
auto-optimise-store = true
|
auto-optimise-store = true
|
||||||
experimental-features = nix-command flakes
|
experimental-features = nix-command flakes
|
||||||
|
|
||||||
- uses: cachix/cachix-action@v11
|
- uses: cachix/cachix-action@v12
|
||||||
with:
|
with:
|
||||||
name: jakestanger
|
name: jakestanger
|
||||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||||
|
|||||||
2
.github/workflows/update-nix-flake-lock.yml
vendored
2
.github/workflows/update-nix-flake-lock.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Nix
|
- name: Install Nix
|
||||||
uses: cachix/install-nix-action@v16
|
uses: cachix/install-nix-action@v20
|
||||||
with:
|
with:
|
||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -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/),
|
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).
|
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
|
## [v0.9.0] - 2023-01-28
|
||||||
### :boom: BREAKING CHANGES
|
### :boom: BREAKING CHANGES
|
||||||
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||||
@@ -195,3 +233,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.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.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
|
||||||
1064
Cargo.lock
generated
1064
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@@ -9,20 +9,24 @@ description = "Customisable GTK Layer Shell wlroots/sway bar"
|
|||||||
default = [
|
default = [
|
||||||
"http",
|
"http",
|
||||||
"config+all",
|
"config+all",
|
||||||
|
"clipboard",
|
||||||
"clock",
|
"clock",
|
||||||
"music+all",
|
"music+all",
|
||||||
"sys_info",
|
"sys_info",
|
||||||
"tray",
|
"tray",
|
||||||
|
"volume+all",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
|
|
||||||
http = ["dep:reqwest"]
|
http = ["dep:reqwest"]
|
||||||
|
|
||||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
|
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
|
||||||
"config+json" = ["serde_json"]
|
"config+json" = ["universal-config/json"]
|
||||||
"config+yaml" = ["serde_yaml"]
|
"config+yaml" = ["universal-config/yaml"]
|
||||||
"config+toml" = ["toml"]
|
"config+toml" = ["universal-config/toml"]
|
||||||
"config+corn" = ["libcorn"]
|
"config+corn" = ["universal-config/corn"]
|
||||||
|
|
||||||
|
clipboard = ["nix"]
|
||||||
|
|
||||||
clock = ["chrono"]
|
clock = ["chrono"]
|
||||||
|
|
||||||
@@ -35,6 +39,10 @@ sys_info = ["sysinfo", "regex"]
|
|||||||
|
|
||||||
tray = ["stray"]
|
tray = ["stray"]
|
||||||
|
|
||||||
|
volume = []
|
||||||
|
"volume+all" = ["volume", "volume+pulse"]
|
||||||
|
"volume+pulse" = ["libpulse-binding", "libpulse-glib-binding"]
|
||||||
|
|
||||||
workspaces = ["futures-util"]
|
workspaces = ["futures-util"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||||
@@ -42,9 +50,9 @@ workspaces = ["futures-util"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# core
|
# core
|
||||||
gtk = "0.16.0"
|
gtk = "0.17.0"
|
||||||
gtk-layer-shell = "0.5.0"
|
gtk-layer-shell = "0.6.0"
|
||||||
glib = "0.16.2"
|
glib = "0.17.5"
|
||||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
@@ -60,6 +68,8 @@ notify = { version = "5.0.0", default-features = false }
|
|||||||
wayland-client = "0.29.5"
|
wayland-client = "0.29.5"
|
||||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||||
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
||||||
|
universal-config = { version = "0.2.1", default_features = false }
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
@@ -67,11 +77,8 @@ cfg-if = "1.0.0"
|
|||||||
# http
|
# http
|
||||||
reqwest = { version = "0.11.14", optional = true }
|
reqwest = { version = "0.11.14", optional = true }
|
||||||
|
|
||||||
# config
|
# clipboard
|
||||||
serde_json = { version = "1.0.82", optional = true }
|
nix = { version = "0.26.2", optional = true }
|
||||||
serde_yaml = { version = "0.9.4", optional = true }
|
|
||||||
toml = { version = "0.7.0", optional = true }
|
|
||||||
libcorn = { version = "0.6.1", optional = true }
|
|
||||||
|
|
||||||
# clock
|
# clock
|
||||||
chrono = { version = "0.4.19", optional = true }
|
chrono = { version = "0.4.19", optional = true }
|
||||||
@@ -86,6 +93,10 @@ sysinfo = { version = "0.27.0", optional = true }
|
|||||||
# tray
|
# tray
|
||||||
stray = { version = "0.1.3", optional = true }
|
stray = { version = "0.1.3", optional = true }
|
||||||
|
|
||||||
|
# volume
|
||||||
|
libpulse-binding = { version = "2.27.1", optional = true }
|
||||||
|
libpulse-glib-binding = { version = "2.27.1", optional = true }
|
||||||
|
|
||||||
# workspaces
|
# workspaces
|
||||||
swayipc-async = { version = "2.0.1", optional = true }
|
swayipc-async = { version = "2.0.1", optional = true }
|
||||||
hyprland = { version = "0.3.0", optional = true }
|
hyprland = { version = "0.3.0", optional = true }
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -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.
|
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).
|
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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
|
## Installation
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
|
|
||||||
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install ironbar
|
cargo install ironbar
|
||||||
```
|
```
|
||||||
@@ -59,6 +70,8 @@ Here is an example nix flake that uses Ironbar.
|
|||||||
enable = true;
|
enable = true;
|
||||||
config = {};
|
config = {};
|
||||||
style = "";
|
style = "";
|
||||||
|
package = inputs.ironbar;
|
||||||
|
features = ["feature" "another_feature"];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -74,6 +87,8 @@ in case you don't want to compile Ironbar.
|
|||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/jakestanger/ironbar.git
|
git clone https://github.com/jakestanger/ironbar.git
|
||||||
cd ironbar
|
cd ironbar
|
||||||
@@ -83,7 +98,7 @@ install target/release/ironbar ~/.local/bin/ironbar
|
|||||||
```
|
```
|
||||||
|
|
||||||
By default, all features are enabled.
|
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)
|
[repo](https://github.com/jakestanger/ironbar)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ cargo build --release
|
|||||||
install target/release/ironbar ~/.local/bin/ironbar
|
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
|
## Features
|
||||||
|
|
||||||
By default, all features are enabled for convenience. This can result in a significant compile time.
|
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+toml | Enables configuration support for TOML. |
|
||||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
|
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
|
||||||
| **Modules** | |
|
| **Modules** | |
|
||||||
|
| clipboard | Enables the `clipboard` module. |
|
||||||
| clock | Enables the `clock` module. |
|
| clock | Enables the `clock` module. |
|
||||||
| music+all | Enables the `music` module with support for all player types. |
|
| music+all | Enables the `music` module with support for all player types. |
|
||||||
| music+mpris | Enables the `music` module with MPRIS support. |
|
| music+mpris | Enables the `music` module with MPRIS support. |
|
||||||
|
|||||||
@@ -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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Guides
|
# Guides
|
||||||
|
|
||||||
|
- [Compiling from source](compiling)
|
||||||
- [Configuration guide](configuration-guide)
|
- [Configuration guide](configuration-guide)
|
||||||
- [Scripts](scripts)
|
- [Scripts](scripts)
|
||||||
- [Images](images)
|
- [Images](images)
|
||||||
- [Styling guide](styling-guide)
|
- [Styling guide](styling-guide)
|
||||||
- [Examples](https://github.com/JakeStanger/ironbar/tree/master/examples)
|
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
|
|
||||||
|
- [Clipboard](clipboard)
|
||||||
- [Clock](clock)
|
- [Clock](clock)
|
||||||
- [Custom](custom)
|
- [Custom](custom)
|
||||||
- [Focused](focused)
|
- [Focused](focused)
|
||||||
|
|||||||
93
docs/modules/Clipboard.md
Normal file
93
docs/modules/Clipboard.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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. |
|
||||||
@@ -70,7 +70,7 @@ end:
|
|||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-------------------------------|------------------------------------------------------------------------------------|
|
|--------------------------------|------------------------------------------------------------------------------------|
|
||||||
| `#clock` | Clock widget button |
|
| `#clock` | Clock widget button |
|
||||||
| `#popup-clock` | Clock popup box |
|
| `#popup-clock` | Clock popup box |
|
||||||
| `#popup-clock #calendar-clock` | Clock inside the popup |
|
| `#popup-clock #calendar-clock` | Clock inside the popup |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ For example, the following label would output your system uptime, updated every
|
|||||||
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
|
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
|
### Commands
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ Displays the title and/or icon of the currently focused window.
|
|||||||
> Type: `focused`
|
> Type: `focused`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
||||||
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
||||||
| `icon_size` | `integer` | `32` | Size of icon in pixels |
|
| `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` | `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.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. |
|
| `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>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
|||||||
> Type: `music`
|
> Type: `music`
|
||||||
|
|
||||||
| | Type | Default | Description |
|
| | Type | Default | Description |
|
||||||
|-------------------|---------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
| `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. |
|
| `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` | `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.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. |
|
| `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.play` | `string/image` | `` | Icon to show when playing. |
|
||||||
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
||||||
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
||||||
|
|||||||
@@ -20,8 +20,18 @@ let {
|
|||||||
show_icons = true
|
show_icons = true
|
||||||
}
|
}
|
||||||
|
|
||||||
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
|
$mpris = {
|
||||||
$mpd_server = { type = "mpd" host = "chloe:6600" }
|
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 = {
|
$sys_info = {
|
||||||
type = "sys_info"
|
type = "sys_info"
|
||||||
@@ -55,6 +65,8 @@ let {
|
|||||||
show_if.interval = 500
|
show_if.interval = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||||
|
|
||||||
// -- begin custom --
|
// -- begin custom --
|
||||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||||
|
|
||||||
@@ -86,10 +98,13 @@ let {
|
|||||||
// -- end custom --
|
// -- end custom --
|
||||||
|
|
||||||
$left = [ $workspaces $launcher ]
|
$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 {
|
in {
|
||||||
anchor_to_edges = true
|
anchor_to_edges = true
|
||||||
position = "top"
|
position = "bottom"
|
||||||
start = $left end = $right
|
icon_theme = "Paper"
|
||||||
|
|
||||||
|
start = $left
|
||||||
|
end = $right
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"music_dir": "/home/jake/Music",
|
"music_dir": "/home/jake/Music",
|
||||||
"player_type": "mpd",
|
"player_type": "mpd",
|
||||||
"truncate": {
|
"truncate": {
|
||||||
"length": 100,
|
"max_length": 100,
|
||||||
"mode": "end"
|
"mode": "end"
|
||||||
},
|
},
|
||||||
"type": "music"
|
"type": "music"
|
||||||
@@ -43,6 +43,14 @@
|
|||||||
},
|
},
|
||||||
"type": "sys_info"
|
"type": "sys_info"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"max_items": 3,
|
||||||
|
"truncate": {
|
||||||
|
"length": 50,
|
||||||
|
"mode": "end"
|
||||||
|
},
|
||||||
|
"type": "clipboard"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"bar": [
|
"bar": [
|
||||||
{
|
{
|
||||||
@@ -98,16 +106,6 @@
|
|||||||
"icon_theme": "Paper",
|
"icon_theme": "Paper",
|
||||||
"position": "bottom",
|
"position": "bottom",
|
||||||
"start": [
|
"start": [
|
||||||
{
|
|
||||||
"bar": [
|
|
||||||
{
|
|
||||||
"size": 32,
|
|
||||||
"src": "file:///path/to/image.jpg",
|
|
||||||
"type": "image"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"all_monitors": false,
|
"all_monitors": false,
|
||||||
"name_map": {
|
"name_map": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ player_type = 'mpd'
|
|||||||
type = 'music'
|
type = 'music'
|
||||||
|
|
||||||
[end.truncate]
|
[end.truncate]
|
||||||
length = 100
|
max_length = 100
|
||||||
mode = 'end'
|
mode = 'end'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
@@ -44,6 +44,14 @@ memory = 30
|
|||||||
networks = 3
|
networks = 3
|
||||||
temps = 5
|
temps = 5
|
||||||
|
|
||||||
|
[[end]]
|
||||||
|
max_items = 3
|
||||||
|
type = 'clipboard'
|
||||||
|
|
||||||
|
[end.truncate]
|
||||||
|
length = 50
|
||||||
|
mode = 'end'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
class = 'power-menu'
|
class = 'power-menu'
|
||||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||||
@@ -87,14 +95,6 @@ type = 'label'
|
|||||||
[[end]]
|
[[end]]
|
||||||
type = 'clock'
|
type = 'clock'
|
||||||
|
|
||||||
[[start]]
|
|
||||||
type = 'custom'
|
|
||||||
|
|
||||||
[[start.bar]]
|
|
||||||
size = 32
|
|
||||||
src = 'file:///path/to/image.jpg'
|
|
||||||
type = 'image'
|
|
||||||
|
|
||||||
[[start]]
|
[[start]]
|
||||||
all_monitors = false
|
all_monitors = false
|
||||||
type = 'workspaces'
|
type = 'workspaces'
|
||||||
|
|||||||
@@ -1,50 +1,20 @@
|
|||||||
anchor_to_edges: true
|
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:
|
end:
|
||||||
- music_dir: /home/jake/Music
|
- music_dir: /home/jake/Music
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
truncate:
|
truncate:
|
||||||
length: 100
|
max_length: 100
|
||||||
mode: end
|
mode: end
|
||||||
type: music
|
type: music
|
||||||
|
|
||||||
- host: chloe:6600
|
- host: chloe:6600
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
truncate: end
|
truncate: end
|
||||||
type: music
|
type: music
|
||||||
|
|
||||||
- cmd: /home/jake/bin/phone-battery
|
- cmd: /home/jake/bin/phone-battery
|
||||||
show_if:
|
show_if:
|
||||||
cmd: /home/jake/bin/phone-connected
|
cmd: /home/jake/bin/phone-connected
|
||||||
interval: 500
|
interval: 500
|
||||||
type: script
|
type: script
|
||||||
|
|
||||||
- format:
|
- format:
|
||||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||||
@@ -60,7 +30,11 @@ end:
|
|||||||
networks: 3
|
networks: 3
|
||||||
temps: 5
|
temps: 5
|
||||||
type: sys_info
|
type: sys_info
|
||||||
|
- max_items: 3
|
||||||
|
truncate:
|
||||||
|
length: 50
|
||||||
|
mode: end
|
||||||
|
type: clipboard
|
||||||
- bar:
|
- bar:
|
||||||
- label:
|
- label:
|
||||||
name: power-btn
|
name: power-btn
|
||||||
@@ -89,9 +63,23 @@ end:
|
|||||||
type: label
|
type: label
|
||||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
type: custom
|
type: custom
|
||||||
|
|
||||||
- type: clock
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -210,3 +210,31 @@
|
|||||||
.popup-power-menu .power-btn:hover {
|
.popup-power-menu .power-btn:hover {
|
||||||
background-color: #1c1c1c;
|
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
12
flake.lock
generated
@@ -17,11 +17,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1675115703,
|
"lastModified": 1680213900,
|
||||||
"narHash": "sha256-4zetAPSyY0D77x+Ww9QBe8RHn1akvIvHJ/kgg8kGDbk=",
|
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2caf4ef5005ecc68141ecb4aac271079f7371c44",
|
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1675132198,
|
"lastModified": 1680229280,
|
||||||
"narHash": "sha256-izOVjdIfdv0OzcfO9rXX0lfGkQn4tdJ0eNm3P3LYo/o=",
|
"narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "48b1403150c3f5a9aeee8bc4c77c8926f29c6501",
|
"rev": "aa480d799023141e1b9e5d6108700de63d9ad002",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
49
flake.nix
49
flake.nix
@@ -6,9 +6,6 @@
|
|||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
#nci.url = "github:yusdacra/nix-cargo-integration";
|
|
||||||
#nci.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
#nci.inputs.rust-overlay.follows = "rust-overlay";
|
|
||||||
};
|
};
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
@@ -39,18 +36,16 @@
|
|||||||
cargo = rust;
|
cargo = rust;
|
||||||
rustc = 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 {
|
in {
|
||||||
ironbar = rustPlatform.buildRustPackage {
|
ironbar = prev.callPackage ./nix/default.nix {
|
||||||
pname = "ironbar";
|
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
||||||
version = self.rev or "dirty";
|
inherit rustPlatform;
|
||||||
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];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
packages = genSystems (
|
packages = genSystems (
|
||||||
@@ -95,26 +90,34 @@
|
|||||||
package = lib.mkOption {
|
package = lib.mkOption {
|
||||||
type = with lib.types; package;
|
type = with lib.types; package;
|
||||||
default = defaultIronbarPackage;
|
default = defaultIronbarPackage;
|
||||||
description = "The package for ironbar to use";
|
description = "The package for ironbar to use.";
|
||||||
};
|
};
|
||||||
systemd = lib.mkOption {
|
systemd = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = pkgs.stdenv.isLinux;
|
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 {
|
style = lib.mkOption {
|
||||||
type = lib.types.lines;
|
type = lib.types.lines;
|
||||||
default = "";
|
default = "";
|
||||||
description = "The stylesheet to apply to ironbar";
|
description = "The stylesheet to apply to ironbar.";
|
||||||
};
|
};
|
||||||
config = lib.mkOption {
|
config = lib.mkOption {
|
||||||
type = jsonFormat.type;
|
type = jsonFormat.type;
|
||||||
default = {};
|
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 {
|
config = let
|
||||||
home.packages = [cfg.package];
|
pkg = cfg.package.override {features = cfg.features;};
|
||||||
|
in
|
||||||
|
lib.mkIf cfg.enable {
|
||||||
|
home.packages = [pkg];
|
||||||
xdg.configFile = {
|
xdg.configFile = {
|
||||||
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
||||||
source = jsonFormat.generate "ironbar-config" cfg.config;
|
source = jsonFormat.generate "ironbar-config" cfg.config;
|
||||||
@@ -130,7 +133,7 @@
|
|||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = "${cfg.package}/bin/ironbar";
|
ExecStart = "${pkg}/bin/ironbar";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [
|
Install.WantedBy = [
|
||||||
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||||
@@ -140,4 +143,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
nixConfig = {
|
||||||
|
extra-substituters = ["https://jakestanger.cachix.org"];
|
||||||
|
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
36
nix/default.nix
Normal file
36
nix/default.nix
Normal 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";
|
||||||
|
};
|
||||||
|
}
|
||||||
42
src/bar.rs
42
src/bar.rs
@@ -1,5 +1,5 @@
|
|||||||
use crate::bridge_channel::BridgeChannel;
|
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::dynamic_string::DynamicString;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext};
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
@@ -24,7 +24,13 @@ pub fn create_bar(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let win = ApplicationWindow::builder().application(app).build();
|
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();
|
let orientation = config.position.get_orientation();
|
||||||
|
|
||||||
@@ -79,16 +85,18 @@ fn setup_layer_shell(
|
|||||||
monitor: &Monitor,
|
monitor: &Monitor,
|
||||||
position: BarPosition,
|
position: BarPosition,
|
||||||
anchor_to_edges: bool,
|
anchor_to_edges: bool,
|
||||||
|
margin: MarginConfig,
|
||||||
) {
|
) {
|
||||||
gtk_layer_shell::init_for_window(win);
|
gtk_layer_shell::init_for_window(win);
|
||||||
gtk_layer_shell::set_monitor(win, monitor);
|
gtk_layer_shell::set_monitor(win, monitor);
|
||||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||||
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
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::Top, margin.top);
|
||||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, 0);
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
|
||||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
|
||||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
|
||||||
|
|
||||||
let bar_orientation = position.get_orientation();
|
let bar_orientation = position.get_orientation();
|
||||||
|
|
||||||
@@ -185,7 +193,7 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
|||||||
macro_rules! add_module {
|
macro_rules! add_module {
|
||||||
($module:expr, $id:expr) => {{
|
($module:expr, $id:expr) => {{
|
||||||
let common = $module.common.take().expect("Common config did not exist");
|
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);
|
let container = wrap_widget(&widget);
|
||||||
content.add(&container);
|
content.add(&container);
|
||||||
@@ -195,6 +203,8 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
|||||||
|
|
||||||
for (id, config) in modules.into_iter().enumerate() {
|
for (id, config) in modules.into_iter().enumerate() {
|
||||||
match config {
|
match config {
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||||
#[cfg(feature = "clock")]
|
#[cfg(feature = "clock")]
|
||||||
ModuleConfig::Clock(mut module) => add_module!(module, id),
|
ModuleConfig::Clock(mut module) => add_module!(module, id),
|
||||||
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
||||||
@@ -281,6 +291,10 @@ fn setup_receiver<TSend>(
|
|||||||
) where
|
) where
|
||||||
TSend: Clone + Send + 'static,
|
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| {
|
channel.recv(move |ev| {
|
||||||
match ev {
|
match ev {
|
||||||
ModuleUpdateEvent::Update(update) => {
|
ModuleUpdateEvent::Update(update) => {
|
||||||
@@ -298,6 +312,12 @@ fn setup_receiver<TSend>(
|
|||||||
} else {
|
} else {
|
||||||
popup.show_content(id);
|
popup.show_content(id);
|
||||||
popup.show(geometry);
|
popup.show(geometry);
|
||||||
|
|
||||||
|
if !has_popup_opened {
|
||||||
|
popup.show_content(id);
|
||||||
|
popup.show(geometry);
|
||||||
|
has_popup_opened = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::OpenPopup(geometry) => {
|
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||||
@@ -307,6 +327,12 @@ fn setup_receiver<TSend>(
|
|||||||
popup.hide();
|
popup.hide();
|
||||||
popup.show_content(id);
|
popup.show_content(id);
|
||||||
popup.show(geometry);
|
popup.show(geometry);
|
||||||
|
|
||||||
|
if !has_popup_opened {
|
||||||
|
popup.show_content(id);
|
||||||
|
popup.show(geometry);
|
||||||
|
has_popup_opened = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::ClosePopup => {
|
ModuleUpdateEvent::ClosePopup => {
|
||||||
debug!("Closing popup for {} [#{}]", name, id);
|
debug!("Closing popup for {} [#{}]", name, id);
|
||||||
@@ -386,8 +412,6 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
|
|||||||
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
|
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
|
||||||
|
|
||||||
container.connect_scroll_event(move |_, event| {
|
container.connect_scroll_event(move |_, event| {
|
||||||
println!("{:?}", event.direction());
|
|
||||||
|
|
||||||
let script = match event.direction() {
|
let script = match event.direction() {
|
||||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||||
|
|||||||
244
src/clients/clipboard.rs
Normal file
244
src/clients/clipboard.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
pub mod clipboard;
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
#[cfg(feature = "music")]
|
#[cfg(feature = "music")]
|
||||||
@@ -5,3 +7,5 @@ pub mod music;
|
|||||||
#[cfg(feature = "tray")]
|
#[cfg(feature = "tray")]
|
||||||
pub mod system_tray;
|
pub mod system_tray;
|
||||||
pub mod wayland;
|
pub mod wayland;
|
||||||
|
#[cfg(feature = "volume")]
|
||||||
|
pub mod volume;
|
||||||
|
|||||||
9
src/clients/volume/mod.rs
Normal file
9
src/clients/volume/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[cfg(feature = "volume+pulse")]
|
||||||
|
pub mod pulse_bak;
|
||||||
|
// #[cfg(feature = "volume+pulse")]
|
||||||
|
// pub mod pulse;
|
||||||
|
|
||||||
|
trait VolumeClient {
|
||||||
|
// TODO: Write
|
||||||
|
}
|
||||||
|
|
||||||
345
src/clients/volume/pulse/callbacks.rs
Normal file
345
src/clients/volume/pulse/callbacks.rs
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
use libpulse_binding::{
|
||||||
|
callbacks::ListResult,
|
||||||
|
context::{
|
||||||
|
introspect::{CardInfo, SinkInfo, SinkInputInfo, SourceInfo, SourceOutputInfo},
|
||||||
|
subscribe::{InterestMaskSet, Operation},
|
||||||
|
},
|
||||||
|
// def::{SinkState, SourceState},
|
||||||
|
};
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use super::{common::*, /*pa_interface::ACTIONS_SX*/};
|
||||||
|
// use crate::{
|
||||||
|
// entry::{CardProfile, Entry},
|
||||||
|
// models::EntryUpdate,
|
||||||
|
// ui::Rect,
|
||||||
|
// };
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crate::clients::volume::pulse::CardProfile;
|
||||||
|
|
||||||
|
pub fn subscribe(
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("[PAInterface] Registering pulseaudio callbacks");
|
||||||
|
|
||||||
|
context.borrow_mut().subscribe(
|
||||||
|
InterestMaskSet::SINK
|
||||||
|
| InterestMaskSet::SINK_INPUT
|
||||||
|
| InterestMaskSet::SOURCE
|
||||||
|
| InterestMaskSet::CARD
|
||||||
|
| InterestMaskSet::SOURCE_OUTPUT
|
||||||
|
| InterestMaskSet::CLIENT
|
||||||
|
| InterestMaskSet::SERVER,
|
||||||
|
|success: bool| {
|
||||||
|
assert!(success, "subscription failed");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
context.borrow_mut().set_subscribe_callback(Some(Box::new(
|
||||||
|
move |facility, operation, index| {
|
||||||
|
if let Some(facility) = facility {
|
||||||
|
match facility {
|
||||||
|
Facility::Server | Facility::Client => {
|
||||||
|
error!("{:?} {:?}", facility, operation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry_type: EntryType = facility.into();
|
||||||
|
match operation {
|
||||||
|
Some(Operation::New) => {
|
||||||
|
info!("[PAInterface] New {:?}", entry_type);
|
||||||
|
|
||||||
|
info_sx
|
||||||
|
.send(EntryIdentifier::new(entry_type, index))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Some(Operation::Changed) => {
|
||||||
|
info!("[PAInterface] {:?} changed", entry_type);
|
||||||
|
info_sx
|
||||||
|
.send(EntryIdentifier::new(entry_type, index))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Some(Operation::Removed) => {
|
||||||
|
info!("[PAInterface] {:?} removed", entry_type);
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryRemoved(EntryIdentifier::new(
|
||||||
|
// entry_type, index,
|
||||||
|
// )))
|
||||||
|
// .unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_current_state(
|
||||||
|
context: Rc<RefCell<PAContext>>,
|
||||||
|
info_sxx: mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("[PAInterface] Requesting starting state");
|
||||||
|
|
||||||
|
let introspector = context.borrow_mut().introspect();
|
||||||
|
|
||||||
|
let info_sx = info_sxx.clone();
|
||||||
|
introspector.get_sink_info_list(move |x: ListResult<&SinkInfo>| {
|
||||||
|
if let ListResult::Item(e) = x {
|
||||||
|
let _ = info_sx
|
||||||
|
.clone()
|
||||||
|
.send(EntryIdentifier::new(EntryType::Sink, e.index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let info_sx = info_sxx.clone();
|
||||||
|
introspector.get_sink_input_info_list(move |x: ListResult<&SinkInputInfo>| {
|
||||||
|
if let ListResult::Item(e) = x {
|
||||||
|
let _ = info_sx.send(EntryIdentifier::new(EntryType::SinkInput, e.index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let info_sx = info_sxx.clone();
|
||||||
|
introspector.get_source_info_list(move |x: ListResult<&SourceInfo>| {
|
||||||
|
if let ListResult::Item(e) = x {
|
||||||
|
let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, e.index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let info_sx = info_sxx.clone();
|
||||||
|
introspector.get_source_output_info_list(move |x: ListResult<&SourceOutputInfo>| {
|
||||||
|
if let ListResult::Item(e) = x {
|
||||||
|
let _ = info_sx.send(EntryIdentifier::new(EntryType::SourceOutput, e.index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
introspector.get_card_info_list(move |x: ListResult<&CardInfo>| {
|
||||||
|
if let ListResult::Item(e) = x {
|
||||||
|
let _ = info_sxx.send(EntryIdentifier::new(EntryType::Card, e.index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_info(
|
||||||
|
ident: EntryIdentifier,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) {
|
||||||
|
let introspector = context.borrow_mut().introspect();
|
||||||
|
debug!(
|
||||||
|
"[PAInterface] Requesting info for {:?} {}",
|
||||||
|
ident.entry_type, ident.index
|
||||||
|
);
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::SinkInput => {
|
||||||
|
introspector.get_sink_input_info(ident.index, on_sink_input_info(&info_sx));
|
||||||
|
}
|
||||||
|
EntryType::Sink => {
|
||||||
|
introspector.get_sink_info_by_index(ident.index, on_sink_info(&info_sx));
|
||||||
|
}
|
||||||
|
EntryType::SourceOutput => {
|
||||||
|
introspector.get_source_output_info(ident.index, on_source_output_info(&info_sx));
|
||||||
|
}
|
||||||
|
EntryType::Source => {
|
||||||
|
introspector.get_source_info_by_index(ident.index, on_source_info(&info_sx));
|
||||||
|
}
|
||||||
|
EntryType::Card => {
|
||||||
|
introspector.get_card_info_by_index(ident.index, on_card_info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn on_card_info(res: ListResult<&CardInfo>) {
|
||||||
|
if let ListResult::Item(i) = res {
|
||||||
|
let n = match i
|
||||||
|
.proplist
|
||||||
|
.get_str(libpulse_binding::proplist::properties::DEVICE_DESCRIPTION)
|
||||||
|
{
|
||||||
|
Some(s) => s,
|
||||||
|
None => String::from(""),
|
||||||
|
};
|
||||||
|
let profiles: Vec<CardProfile> = i
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
p.name.clone().map(|n| CardProfile {
|
||||||
|
// area: Rect::default(),
|
||||||
|
is_selected: false,
|
||||||
|
name: n.to_string(),
|
||||||
|
description: match &p.description {
|
||||||
|
Some(s) => s.to_string(),
|
||||||
|
None => n.to_string(),
|
||||||
|
},
|
||||||
|
#[cfg(any(feature = "pa_v13"))]
|
||||||
|
available: p.available,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let selected_profile = match &i.active_profile {
|
||||||
|
Some(x) => {
|
||||||
|
if let Some(n) = &x.name {
|
||||||
|
profiles.iter().position(|p| p.name == *n)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// let ident = EntryIdentifier::new(EntryType::Card, i.index);
|
||||||
|
// let entry = Entry::new_card_entry(i.index, n, profiles, selected_profile);
|
||||||
|
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
|
||||||
|
// .unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_sink_info(
|
||||||
|
_sx: &mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> impl Fn(ListResult<&SinkInfo>) {
|
||||||
|
|res: ListResult<&SinkInfo>| {
|
||||||
|
if let ListResult::Item(i) = res {
|
||||||
|
debug!("[PADataInterface] Update {} sink info", i.index);
|
||||||
|
let name = match &i.description {
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
// let ident = EntryIdentifier::new(EntryType::Sink, i.index);
|
||||||
|
// let entry = Entry::new_play_entry(
|
||||||
|
// EntryType::Sink,
|
||||||
|
// i.index,
|
||||||
|
// name,
|
||||||
|
// None,
|
||||||
|
// i.mute,
|
||||||
|
// i.volume,
|
||||||
|
// Some(i.monitor_source),
|
||||||
|
// None,
|
||||||
|
// i.state == SinkState::Suspended,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
|
||||||
|
// .unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_sink_input_info(
|
||||||
|
sx: &mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> impl Fn(ListResult<&SinkInputInfo>) {
|
||||||
|
let info_sx = sx.clone();
|
||||||
|
move |res: ListResult<&SinkInputInfo>| {
|
||||||
|
if let ListResult::Item(i) = res {
|
||||||
|
debug!("[PADataInterface] Update {} sink input info", i.index);
|
||||||
|
let n = match i
|
||||||
|
.proplist
|
||||||
|
.get_str(libpulse_binding::proplist::properties::APPLICATION_NAME)
|
||||||
|
{
|
||||||
|
Some(s) => s,
|
||||||
|
None => match &i.name {
|
||||||
|
Some(s) => s.to_string(),
|
||||||
|
None => String::from(""),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// let ident = EntryIdentifier::new(EntryType::SinkInput, i.index);
|
||||||
|
//
|
||||||
|
// let entry = Entry::new_play_entry(
|
||||||
|
// EntryType::SinkInput,
|
||||||
|
// i.index,
|
||||||
|
// n,
|
||||||
|
// Some(i.sink),
|
||||||
|
// i.mute,
|
||||||
|
// i.volume,
|
||||||
|
// None,
|
||||||
|
// Some(i.sink),
|
||||||
|
// false,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
|
||||||
|
// .unwrap();
|
||||||
|
let _ = info_sx.send(EntryIdentifier::new(EntryType::Sink, i.sink));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_source_info(
|
||||||
|
_sx: &mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> impl Fn(ListResult<&SourceInfo>) {
|
||||||
|
move |res: ListResult<&SourceInfo>| {
|
||||||
|
if let ListResult::Item(i) = res {
|
||||||
|
debug!("[PADataInterface] Update {} source info", i.index);
|
||||||
|
let name = match &i.description {
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
// let ident = EntryIdentifier::new(EntryType::Source, i.index);
|
||||||
|
// let entry = Entry::new_play_entry(
|
||||||
|
// EntryType::Source,
|
||||||
|
// i.index,
|
||||||
|
// name,
|
||||||
|
// None,
|
||||||
|
// i.mute,
|
||||||
|
// i.volume,
|
||||||
|
// Some(i.index),
|
||||||
|
// None,
|
||||||
|
// i.state == SourceState::Suspended,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
|
||||||
|
// .unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_source_output_info(
|
||||||
|
sx: &mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> impl Fn(ListResult<&SourceOutputInfo>) {
|
||||||
|
let info_sx = sx.clone();
|
||||||
|
move |res: ListResult<&SourceOutputInfo>| {
|
||||||
|
if let ListResult::Item(i) = res {
|
||||||
|
debug!("[PADataInterface] Update {} source output info", i.index);
|
||||||
|
let n = match i
|
||||||
|
.proplist
|
||||||
|
.get_str(libpulse_binding::proplist::properties::APPLICATION_NAME)
|
||||||
|
{
|
||||||
|
Some(s) => s,
|
||||||
|
None => String::from(""),
|
||||||
|
};
|
||||||
|
if n == "RsMixerContext" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// let ident = EntryIdentifier::new(EntryType::SourceOutput, i.index);
|
||||||
|
// let entry = Entry::new_play_entry(
|
||||||
|
// EntryType::SourceOutput,
|
||||||
|
// i.index,
|
||||||
|
// n,
|
||||||
|
// Some(i.source),
|
||||||
|
// i.mute,
|
||||||
|
// i.volume,
|
||||||
|
// Some(i.source),
|
||||||
|
// None,
|
||||||
|
// false,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
|
||||||
|
// .unwrap();
|
||||||
|
let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, i.index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/clients/volume/pulse/common.rs
Normal file
30
src/clients/volume/pulse/common.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
pub use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
pub use libpulse_binding::{
|
||||||
|
context::{subscribe::Facility, Context as PAContext},
|
||||||
|
mainloop::{api::Mainloop as MainloopTrait, threaded::Mainloop},
|
||||||
|
stream::Stream,
|
||||||
|
};
|
||||||
|
pub use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
pub use super::{monitor::Monitors, PAInternal, SPEC};
|
||||||
|
// pub use crate::{
|
||||||
|
// entry::{EntryIdentifier, EntryType},
|
||||||
|
// models::{EntryUpdate, PulseAudioAction},
|
||||||
|
// prelude::*,
|
||||||
|
// };
|
||||||
|
|
||||||
|
pub static LOGGING_MODULE: &str = "PAInterface";
|
||||||
|
|
||||||
|
impl From<Facility> for EntryType {
|
||||||
|
fn from(fac: Facility) -> Self {
|
||||||
|
match fac {
|
||||||
|
Facility::Sink => EntryType::Sink,
|
||||||
|
Facility::Source => EntryType::Source,
|
||||||
|
Facility::SinkInput => EntryType::SinkInput,
|
||||||
|
Facility::SourceOutput => EntryType::SourceOutput,
|
||||||
|
Facility::Card => EntryType::Card,
|
||||||
|
_ => EntryType::Sink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/clients/volume/pulse/errors.rs
Normal file
17
src/clients/volume/pulse/errors.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// use thiserror::Error;
|
||||||
|
//
|
||||||
|
// use super::PAInternal;
|
||||||
|
//
|
||||||
|
// #[derive(Debug, Error)]
|
||||||
|
// pub enum PAError {
|
||||||
|
// #[error("cannot create pulseaudio mainloop")]
|
||||||
|
// MainloopCreateError,
|
||||||
|
// #[error("cannot connect pulseaudio mainloop")]
|
||||||
|
// MainloopConnectError,
|
||||||
|
// #[error("cannot create pulseaudio stream")]
|
||||||
|
// StreamCreateError,
|
||||||
|
// #[error("internal channel send error")]
|
||||||
|
// ChannelError(#[from] cb_channel::SendError<PAInternal>),
|
||||||
|
// #[error("pulseaudio disconnected")]
|
||||||
|
// PulseAudioDisconnected,
|
||||||
|
// }
|
||||||
36
src/clients/volume/pulse/mod.rs
Normal file
36
src/clients/volume/pulse/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
mod callbacks;
|
||||||
|
pub mod common;
|
||||||
|
mod errors;
|
||||||
|
mod monitor;
|
||||||
|
mod pa_actions;
|
||||||
|
mod pa_interface;
|
||||||
|
|
||||||
|
use common::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
pub use pa_interface::start;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PAInternal {
|
||||||
|
Tick,
|
||||||
|
Command(Box<PulseAudioAction>),
|
||||||
|
AskInfo(EntryIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref SPEC: libpulse_binding::sample::Spec = libpulse_binding::sample::Spec {
|
||||||
|
format: libpulse_binding::sample::Format::FLOAT32NE,
|
||||||
|
channels: 1,
|
||||||
|
rate: 1024,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
|
pub struct CardProfile {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
#[cfg(any(feature = "pa_v13"))]
|
||||||
|
pub available: bool,
|
||||||
|
// pub area: Rect,
|
||||||
|
pub is_selected: bool,
|
||||||
|
}
|
||||||
|
impl Eq for CardProfile {}
|
||||||
278
src/clients/volume/pulse/monitor.rs
Normal file
278
src/clients/volume/pulse/monitor.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use libpulse_binding::stream::PeekResult;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use super::{common::*, /*pa_interface::ACTIONS_SX*/};
|
||||||
|
// use crate::VARIABLES;
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
|
||||||
|
pub struct Monitor {
|
||||||
|
stream: Rc<RefCell<Stream>>,
|
||||||
|
exit_sender: mpsc::UnboundedSender<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Monitors {
|
||||||
|
monitors: HashMap<EntryIdentifier, Monitor>,
|
||||||
|
errors: HashMap<EntryIdentifier, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Monitors {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
monitors: HashMap::new(),
|
||||||
|
errors: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Monitors {
|
||||||
|
pub fn filter(
|
||||||
|
&mut self,
|
||||||
|
mainloop: &Rc<RefCell<Mainloop>>,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
targets: &HashMap<EntryIdentifier, Option<u32>>,
|
||||||
|
) {
|
||||||
|
// remove failed streams
|
||||||
|
// then send exit signal if stream is unwanted
|
||||||
|
self.monitors.retain(|ident, monitor| {
|
||||||
|
match monitor.stream.borrow_mut().get_state() {
|
||||||
|
libpulse_binding::stream::State::Terminated
|
||||||
|
| libpulse_binding::stream::State::Failed => {
|
||||||
|
info!(
|
||||||
|
"[PAInterface] Disconnecting {} sink input monitor (failed state)",
|
||||||
|
ident.index
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if targets.get(ident) == None {
|
||||||
|
let _ = monitor.exit_sender.send(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
targets.iter().for_each(|(ident, monitor_src)| {
|
||||||
|
if self.monitors.get(ident).is_none() {
|
||||||
|
self.create_monitor(mainloop, context, *ident, *monitor_src);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_monitor(
|
||||||
|
&mut self,
|
||||||
|
mainloop: &Rc<RefCell<Mainloop>>,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
ident: EntryIdentifier,
|
||||||
|
monitor_src: Option<u32>,
|
||||||
|
) {
|
||||||
|
if let Some(count) = self.errors.get(&ident) {
|
||||||
|
if *count >= 5 {
|
||||||
|
self.errors.remove(&ident);
|
||||||
|
// (*ACTIONS_SX)
|
||||||
|
// .get()
|
||||||
|
// .send(EntryUpdate::EntryRemoved(ident))
|
||||||
|
// .unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.monitors.contains_key(&ident) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (sx, rx) = mpsc::unbounded_channel();
|
||||||
|
if let Ok(stream) = create(
|
||||||
|
&mainloop,
|
||||||
|
&context,
|
||||||
|
&libpulse_binding::sample::Spec {
|
||||||
|
format: libpulse_binding::sample::Format::FLOAT32NE,
|
||||||
|
channels: 1,
|
||||||
|
rate: /*(*VARIABLES).get().pa_rate*/ 20,
|
||||||
|
},
|
||||||
|
ident,
|
||||||
|
monitor_src,
|
||||||
|
rx,
|
||||||
|
) {
|
||||||
|
self.monitors.insert(
|
||||||
|
ident,
|
||||||
|
Monitor {
|
||||||
|
stream,
|
||||||
|
exit_sender: sx,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.errors.remove(&ident);
|
||||||
|
} else {
|
||||||
|
self.error(&ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(&mut self, ident: &EntryIdentifier) {
|
||||||
|
let count = match self.errors.get(&ident) {
|
||||||
|
Some(x) => *x,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.errors.insert(*ident, count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice_to_4_bytes(slice: &[u8]) -> [u8; 4] {
|
||||||
|
slice.try_into().expect("slice with incorrect length")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
p_mainloop: &Rc<RefCell<Mainloop>>,
|
||||||
|
p_context: &Rc<RefCell<PAContext>>,
|
||||||
|
p_spec: &libpulse_binding::sample::Spec,
|
||||||
|
ident: EntryIdentifier,
|
||||||
|
source_index: Option<u32>,
|
||||||
|
mut close_rx: mpsc::UnboundedReceiver<u32>,
|
||||||
|
) -> Result<Rc<RefCell<Stream>>> {
|
||||||
|
info!("[PADataInterface] Attempting to create new monitor stream");
|
||||||
|
|
||||||
|
let stream_index = if ident.entry_type == EntryType::SinkInput {
|
||||||
|
Some(ident.index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = Rc::new(RefCell::new(
|
||||||
|
match Stream::new(&mut p_context.borrow_mut(), "RsMixer monitor", p_spec, None) {
|
||||||
|
Some(stream) => stream,
|
||||||
|
None => return Err(Report::msg("Error creating stream for monitoring volume")),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Stream state change callback
|
||||||
|
{
|
||||||
|
debug!("[PADataInterface] Registering stream state change callback");
|
||||||
|
let ml_ref = Rc::clone(&p_mainloop);
|
||||||
|
let stream_ref = Rc::downgrade(&stream);
|
||||||
|
stream
|
||||||
|
.borrow_mut()
|
||||||
|
.set_state_callback(Some(Box::new(move || {
|
||||||
|
let state = unsafe { (*(*stream_ref.as_ptr()).as_ptr()).get_state() };
|
||||||
|
match state {
|
||||||
|
libpulse_binding::stream::State::Ready
|
||||||
|
| libpulse_binding::stream::State::Failed
|
||||||
|
| libpulse_binding::stream::State::Terminated => {
|
||||||
|
unsafe { (*ml_ref.as_ptr()).signal(false) };
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// for sink inputs we want to set monitor stream to sink
|
||||||
|
if let Some(index) = stream_index {
|
||||||
|
stream.borrow_mut().set_monitor_stream(index).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let x;
|
||||||
|
let mut s = None;
|
||||||
|
if let Some(i) = source_index {
|
||||||
|
x = i.to_string();
|
||||||
|
s = Some(x.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("[PADataInterface] Connecting stream");
|
||||||
|
match stream.borrow_mut().connect_record(
|
||||||
|
s,
|
||||||
|
Some(&libpulse_binding::def::BufferAttr {
|
||||||
|
maxlength: std::u32::MAX,
|
||||||
|
tlength: std::u32::MAX,
|
||||||
|
prebuf: std::u32::MAX,
|
||||||
|
minreq: 0,
|
||||||
|
fragsize: /*(*VARIABLES).get().pa_frag_size*/ 48,
|
||||||
|
}),
|
||||||
|
libpulse_binding::stream::FlagSet::PEAK_DETECT
|
||||||
|
| libpulse_binding::stream::FlagSet::ADJUST_LATENCY,
|
||||||
|
) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Report::new(err).wrap_err("while connecting stream for monitoring volume"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("[PADataInterface] Waiting for stream to be ready");
|
||||||
|
loop {
|
||||||
|
match stream.borrow_mut().get_state() {
|
||||||
|
libpulse_binding::stream::State::Ready => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
libpulse_binding::stream::State::Failed
|
||||||
|
| libpulse_binding::stream::State::Terminated => {
|
||||||
|
error!("[PADataInterface] Stream state failed/terminated");
|
||||||
|
return Err(Report::msg("Stream terminated"))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
p_mainloop.borrow_mut().wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.borrow_mut().set_state_callback(None);
|
||||||
|
|
||||||
|
{
|
||||||
|
info!("[PADataInterface] Registering stream read callback");
|
||||||
|
let ml_ref = Rc::clone(&p_mainloop);
|
||||||
|
let stream_ref = Rc::downgrade(&stream);
|
||||||
|
stream.borrow_mut().set_read_callback(Some(Box::new(move |_size: usize| {
|
||||||
|
let remove_failed = || {
|
||||||
|
error!("[PADataInterface] Monitor failed or terminated");
|
||||||
|
};
|
||||||
|
let disconnect_stream = || {
|
||||||
|
warn!("[PADataInterface] {:?} Monitor existed while the sink (input)/source (output) was already gone", ident);
|
||||||
|
unsafe {
|
||||||
|
(*(*stream_ref.as_ptr()).as_ptr()).disconnect().unwrap();
|
||||||
|
(*ml_ref.as_ptr()).signal(false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if close_rx.try_recv().is_ok() {
|
||||||
|
disconnect_stream();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match unsafe {(*(*stream_ref.as_ptr()).as_ptr()).get_state() }{
|
||||||
|
libpulse_binding::stream::State::Failed => {
|
||||||
|
remove_failed();
|
||||||
|
},
|
||||||
|
libpulse_binding::stream::State::Terminated => {
|
||||||
|
remove_failed();
|
||||||
|
},
|
||||||
|
libpulse_binding::stream::State::Ready => {
|
||||||
|
match unsafe{ (*(*stream_ref.as_ptr()).as_ptr()).peek() } {
|
||||||
|
Ok(res) => match res {
|
||||||
|
PeekResult::Data(data) => {
|
||||||
|
let count = data.len() / 4;
|
||||||
|
let mut peak = 0.0;
|
||||||
|
for c in 0..count {
|
||||||
|
let data_slice = slice_to_4_bytes(&data[c * 4 .. (c + 1) * 4]);
|
||||||
|
peak += f32::from_ne_bytes(data_slice).abs();
|
||||||
|
}
|
||||||
|
peak = peak / count as f32;
|
||||||
|
|
||||||
|
// if (*ACTIONS_SX).get().send(EntryUpdate::PeakVolumeUpdate(ident, peak)).is_err() {
|
||||||
|
// disconnect_stream();
|
||||||
|
// }
|
||||||
|
|
||||||
|
unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); };
|
||||||
|
},
|
||||||
|
PeekResult::Hole(_) => {
|
||||||
|
unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); };
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
remove_failed();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
148
src/clients/volume/pulse/pa_actions.rs
Normal file
148
src/clients/volume/pulse/pa_actions.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
use super::{callbacks, common::*};
|
||||||
|
|
||||||
|
pub fn handle_command(
|
||||||
|
cmd: PulseAudioAction,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
info_sx: &mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) -> Option<()> {
|
||||||
|
match cmd {
|
||||||
|
PulseAudioAction::RequestPulseAudioState => {
|
||||||
|
callbacks::request_current_state(Rc::clone(&context), info_sx.clone()).unwrap();
|
||||||
|
}
|
||||||
|
PulseAudioAction::MuteEntry(ident, mute) => {
|
||||||
|
set_mute(ident, mute, &context);
|
||||||
|
}
|
||||||
|
PulseAudioAction::MoveEntryToParent(ident, parent) => {
|
||||||
|
move_entry_to_parent(ident, parent, &context, info_sx.clone());
|
||||||
|
}
|
||||||
|
PulseAudioAction::ChangeCardProfile(ident, profile) => {
|
||||||
|
change_card_profile(ident, profile, &context);
|
||||||
|
}
|
||||||
|
PulseAudioAction::SetVolume(ident, vol) => {
|
||||||
|
set_volume(ident, vol, &context);
|
||||||
|
}
|
||||||
|
PulseAudioAction::SetSuspend(ident, suspend) => {
|
||||||
|
set_suspend(ident, suspend, &context);
|
||||||
|
}
|
||||||
|
PulseAudioAction::KillEntry(ident) => {
|
||||||
|
kill_entry(ident, &context);
|
||||||
|
}
|
||||||
|
PulseAudioAction::Shutdown => {
|
||||||
|
//@TODO disconnect monitors
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_volume(
|
||||||
|
ident: EntryIdentifier,
|
||||||
|
vol: libpulse_binding::volume::ChannelVolumes,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
) {
|
||||||
|
let mut introspector = context.borrow_mut().introspect();
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::Sink => {
|
||||||
|
introspector.set_sink_volume_by_index(ident.index, &vol, None);
|
||||||
|
}
|
||||||
|
EntryType::SinkInput => {
|
||||||
|
introspector.set_sink_input_volume(ident.index, &vol, None);
|
||||||
|
}
|
||||||
|
EntryType::Source => {
|
||||||
|
introspector.set_source_volume_by_index(ident.index, &vol, None);
|
||||||
|
}
|
||||||
|
EntryType::SourceOutput => {
|
||||||
|
introspector.set_source_output_volume(ident.index, &vol, None);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_card_profile(ident: EntryIdentifier, profile: String, context: &Rc<RefCell<PAContext>>) {
|
||||||
|
if ident.entry_type != EntryType::Card {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context
|
||||||
|
.borrow_mut()
|
||||||
|
.introspect()
|
||||||
|
.set_card_profile_by_index(ident.index, &profile[..], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_entry_to_parent(
|
||||||
|
ident: EntryIdentifier,
|
||||||
|
parent: EntryIdentifier,
|
||||||
|
context: &Rc<RefCell<PAContext>>,
|
||||||
|
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
) {
|
||||||
|
let mut introspector = context.borrow_mut().introspect();
|
||||||
|
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::SinkInput => {
|
||||||
|
introspector.move_sink_input_by_index(
|
||||||
|
ident.index,
|
||||||
|
parent.index,
|
||||||
|
Some(Box::new(move |_| {
|
||||||
|
info_sx.send(parent).unwrap();
|
||||||
|
info_sx.send(ident).unwrap();
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EntryType::SourceOutput => {
|
||||||
|
introspector.move_source_output_by_index(
|
||||||
|
ident.index,
|
||||||
|
parent.index,
|
||||||
|
Some(Box::new(move |_| {
|
||||||
|
info_sx.send(parent).unwrap();
|
||||||
|
info_sx.send(ident).unwrap();
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_suspend(ident: EntryIdentifier, suspend: bool, context: &Rc<RefCell<PAContext>>) {
|
||||||
|
let mut introspector = context.borrow_mut().introspect();
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::Sink => {
|
||||||
|
introspector.suspend_sink_by_index(ident.index, suspend, None);
|
||||||
|
}
|
||||||
|
EntryType::Source => {
|
||||||
|
introspector.suspend_source_by_index(ident.index, suspend, None);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill_entry(ident: EntryIdentifier, context: &Rc<RefCell<PAContext>>) {
|
||||||
|
let mut introspector = context.borrow_mut().introspect();
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::SinkInput => {
|
||||||
|
introspector.kill_sink_input(ident.index, |_| {});
|
||||||
|
}
|
||||||
|
EntryType::SourceOutput => {
|
||||||
|
introspector.kill_source_output(ident.index, |_| {});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_mute(ident: EntryIdentifier, mute: bool, context: &Rc<RefCell<PAContext>>) {
|
||||||
|
let mut introspector = context.borrow_mut().introspect();
|
||||||
|
match ident.entry_type {
|
||||||
|
EntryType::Sink => {
|
||||||
|
introspector.set_sink_mute_by_index(ident.index, mute, None);
|
||||||
|
}
|
||||||
|
EntryType::SinkInput => {
|
||||||
|
introspector.set_sink_input_mute(ident.index, mute, None);
|
||||||
|
}
|
||||||
|
EntryType::Source => {
|
||||||
|
introspector.set_source_mute_by_index(ident.index, mute, None);
|
||||||
|
}
|
||||||
|
EntryType::SourceOutput => {
|
||||||
|
introspector.set_source_output_mute(ident.index, mute, None);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
162
src/clients/volume/pulse/pa_interface.rs
Normal file
162
src/clients/volume/pulse/pa_interface.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
// use lazy_static::lazy_static;
|
||||||
|
use libpulse_binding::proplist::Proplist;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
// use state::Storage;
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
|
||||||
|
use super::{callbacks, common::*, pa_actions};
|
||||||
|
|
||||||
|
// lazy_static! {
|
||||||
|
// pub static ref ACTIONS_SX: Storage<mpsc::UnboundedSender<EntryUpdate>> = Storage::new();
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
mut internal_rx: mpsc::Receiver<PAInternal>,
|
||||||
|
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
|
||||||
|
actions_sx: mpsc::UnboundedSender<EntryUpdate>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// (*ACTIONS_SX).set(actions_sx);
|
||||||
|
|
||||||
|
// Create new mainloop and context
|
||||||
|
let mut proplist = Proplist::new().unwrap();
|
||||||
|
proplist
|
||||||
|
.set_str(libpulse_binding::proplist::properties::APPLICATION_NAME, "RsMixer")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
debug!("[PAInterface] Creating new mainloop");
|
||||||
|
let mainloop = Rc::new(RefCell::new(match Mainloop::new() {
|
||||||
|
Some(ml) => ml,
|
||||||
|
None => {
|
||||||
|
error!("[PAInterface] Error while creating new mainloop");
|
||||||
|
return Err(Report::msg("Error while creating new mainloop"));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
debug!("[PAInterface] Creating new context");
|
||||||
|
let context = Rc::new(RefCell::new(
|
||||||
|
match PAContext::new_with_proplist(
|
||||||
|
mainloop.borrow_mut().deref().deref(),
|
||||||
|
"RsMixerContext",
|
||||||
|
&proplist,
|
||||||
|
) {
|
||||||
|
Some(ctx) => ctx,
|
||||||
|
None => {
|
||||||
|
error!("[PAInterface] Error while creating new context");
|
||||||
|
return Err(Report::msg("Error while creating new context"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// PAContext state change callback
|
||||||
|
{
|
||||||
|
debug!("[PAInterface] Registering state change callback");
|
||||||
|
let ml_ref = Rc::clone(&mainloop);
|
||||||
|
let context_ref = Rc::clone(&context);
|
||||||
|
context
|
||||||
|
.borrow_mut()
|
||||||
|
.set_state_callback(Some(Box::new(move || {
|
||||||
|
let state = unsafe { (*context_ref.as_ptr()).get_state() };
|
||||||
|
if matches!(
|
||||||
|
state,
|
||||||
|
libpulse_binding::context::State::Ready
|
||||||
|
| libpulse_binding::context::State::Failed
|
||||||
|
| libpulse_binding::context::State::Terminated
|
||||||
|
) {
|
||||||
|
unsafe { (*ml_ref.as_ptr()).signal(false) };
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to connect to pulseaudio
|
||||||
|
debug!("[PAInterface] Connecting context");
|
||||||
|
|
||||||
|
if context
|
||||||
|
.borrow_mut()
|
||||||
|
.connect(None, libpulse_binding::context::FlagSet::NOFLAGS, None)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
error!("[PAInterface] Error while connecting context");
|
||||||
|
return Err(Report::msg("Error while connecting context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("[PAInterface] Starting mainloop");
|
||||||
|
|
||||||
|
// start mainloop
|
||||||
|
mainloop.borrow_mut().lock();
|
||||||
|
|
||||||
|
if let Err(err) = mainloop.borrow_mut().start() {
|
||||||
|
return Err(Report::new(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("[PAInterface] Waiting for context to be ready...");
|
||||||
|
// wait for context to be ready
|
||||||
|
loop {
|
||||||
|
match context.borrow_mut().get_state() {
|
||||||
|
libpulse_binding::context::State::Ready => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
libpulse_binding::context::State::Failed | libpulse_binding::context::State::Terminated => {
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
mainloop.borrow_mut().stop();
|
||||||
|
error!("[PAInterface] Connection failed or context terminated");
|
||||||
|
return Err(Report::msg("Connection failed or context terminated"));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
mainloop.borrow_mut().wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("[PAInterface] PAContext ready");
|
||||||
|
|
||||||
|
context.borrow_mut().set_state_callback(None);
|
||||||
|
|
||||||
|
callbacks::subscribe(&context, info_sx.clone())?;
|
||||||
|
callbacks::request_current_state(context.clone(), info_sx.clone())?;
|
||||||
|
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
|
||||||
|
debug!("[PAInterface] Actually starting our mainloop");
|
||||||
|
|
||||||
|
let mut monitors = Monitors::default();
|
||||||
|
let mut last_targets = HashMap::new();
|
||||||
|
|
||||||
|
while let Some(msg) = internal_rx.recv().await {
|
||||||
|
mainloop.borrow_mut().lock();
|
||||||
|
|
||||||
|
match context.borrow_mut().get_state() {
|
||||||
|
libpulse_binding::context::State::Ready => {}
|
||||||
|
_ => {
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
return Err(Report::msg("Disconnected while working"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
PAInternal::AskInfo(ident) => {
|
||||||
|
callbacks::request_info(ident, &context, info_sx.clone());
|
||||||
|
}
|
||||||
|
PAInternal::Tick => {
|
||||||
|
// remove failed monitors
|
||||||
|
monitors.filter(&mainloop, &context, &last_targets);
|
||||||
|
}
|
||||||
|
PAInternal::Command(cmd) => {
|
||||||
|
let cmd = cmd.deref();
|
||||||
|
if pa_actions::handle_command(cmd.clone(), &context, &info_sx).is_none() {
|
||||||
|
monitors.filter(&mainloop, &context, &HashMap::new());
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let PulseAudioAction::CreateMonitors(mons) = cmd.clone() {
|
||||||
|
last_targets = mons;
|
||||||
|
monitors.filter(&mainloop, &context, &last_targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
90
src/clients/volume/pulse_bak.rs
Normal file
90
src/clients/volume/pulse_bak.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use crate::clients::volume::VolumeClient;
|
||||||
|
use libpulse_binding::context::State;
|
||||||
|
use libpulse_binding::{
|
||||||
|
context::{Context, FlagSet},
|
||||||
|
mainloop::threaded::Mainloop,
|
||||||
|
proplist::{properties, Proplist},
|
||||||
|
};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
pub fn test() {
|
||||||
|
let mut prop_list = Proplist::new().unwrap();
|
||||||
|
prop_list
|
||||||
|
.set_str(properties::APPLICATION_NAME, "ironbar")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mainloop = Rc::new(RefCell::new(Mainloop::new().unwrap()));
|
||||||
|
|
||||||
|
let context = Rc::new(RefCell::new(
|
||||||
|
Context::new_with_proplist(mainloop.borrow().deref(), "ironbar_context", &prop_list)
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// PAContext state change callback
|
||||||
|
{
|
||||||
|
debug!("[PAInterface] Registering state change callback");
|
||||||
|
let ml_ref = Rc::clone(&mainloop);
|
||||||
|
let context_ref = Rc::clone(&context);
|
||||||
|
context
|
||||||
|
.borrow_mut()
|
||||||
|
.set_state_callback(Some(Box::new(move || {
|
||||||
|
let state = unsafe { (*context_ref.as_ptr()).get_state() };
|
||||||
|
if matches!(state, State::Ready | State::Failed | State::Terminated) {
|
||||||
|
unsafe { (*ml_ref.as_ptr()).signal(false) };
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = context.borrow_mut().connect(None, FlagSet::NOFLAGS, None) {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:?}", context.borrow().get_server());
|
||||||
|
|
||||||
|
mainloop.borrow_mut().lock();
|
||||||
|
if let Err(err) = mainloop.borrow_mut().start() {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("[PAInterface] Waiting for context to be ready...");
|
||||||
|
println!("[PAInterface] Waiting for context to be ready...");
|
||||||
|
// wait for context to be ready
|
||||||
|
loop {
|
||||||
|
match context.borrow().get_state() {
|
||||||
|
State::Ready => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
State::Failed | State::Terminated => {
|
||||||
|
mainloop.borrow_mut().unlock();
|
||||||
|
mainloop.borrow_mut().stop();
|
||||||
|
error!("[PAInterface] Connection failed or context terminated");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
mainloop.borrow_mut().wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("[PAInterface] PAContext ready");
|
||||||
|
println!("[PAInterface] PAContext ready");
|
||||||
|
|
||||||
|
context.borrow_mut().set_state_callback(None);
|
||||||
|
|
||||||
|
println!("jfgjfgg");
|
||||||
|
|
||||||
|
let introspector = context.borrow().introspect();
|
||||||
|
|
||||||
|
println!("jfgjfgg2");
|
||||||
|
|
||||||
|
introspector.get_sink_info_list(|result| {
|
||||||
|
println!("boo: {result:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("fjgjfgf??");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PulseVolumeClient {}
|
||||||
|
|
||||||
|
impl VolumeClient for PulseVolumeClient {}
|
||||||
@@ -1,31 +1,61 @@
|
|||||||
use super::toplevel::{ToplevelEvent, ToplevelInfo};
|
use super::wlr_foreign_toplevel::{
|
||||||
use super::toplevel_manager::listen_for_toplevels;
|
handle::{ToplevelEvent, ToplevelInfo},
|
||||||
use super::ToplevelChange;
|
manager::listen_for_toplevels,
|
||||||
use super::{Env, ToplevelHandler};
|
};
|
||||||
use crate::{error as err, send, write_lock};
|
use super::{DData, Env, ToplevelHandler};
|
||||||
|
use crate::{error as err, send};
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use smithay_client_toolkit::environment::Environment;
|
use smithay_client_toolkit::environment::Environment;
|
||||||
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||||
use smithay_client_toolkit::reexports::calloop;
|
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||||
use smithay_client_toolkit::{new_default_environment, WaylandSource};
|
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||||
|
use smithay_client_toolkit::WaylandSource;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::sync::{broadcast, oneshot};
|
use tokio::sync::{broadcast, oneshot};
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::{error, trace};
|
use tracing::{debug, error};
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
|
use wayland_client::{ConnectError, Display, EventQueue};
|
||||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
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 struct WaylandClient {
|
||||||
pub outputs: Vec<OutputInfo>,
|
pub outputs: Vec<OutputInfo>,
|
||||||
pub seats: Vec<WlSeat>,
|
pub seats: Vec<WlSeat>,
|
||||||
|
|
||||||
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||||
_toplevel_rx: broadcast::Receiver<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 {
|
impl WaylandClient {
|
||||||
@@ -35,21 +65,44 @@ impl WaylandClient {
|
|||||||
|
|
||||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||||
|
|
||||||
let toplevel_tx2 = toplevel_tx.clone();
|
|
||||||
|
|
||||||
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
||||||
let toplevels2 = toplevels.clone();
|
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 || {
|
spawn_blocking(move || {
|
||||||
|
let toplevels = toplevels2;
|
||||||
|
let toplevel_tx = toplevel_tx2;
|
||||||
|
|
||||||
let (env, _display, queue) =
|
let (env, _display, queue) =
|
||||||
new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()])
|
Self::new_environment().expect("Failed to connect to Wayland compositor");
|
||||||
.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);
|
let outputs = Self::get_outputs(&env);
|
||||||
send!(output_tx, outputs);
|
send!(output_tx, outputs);
|
||||||
|
|
||||||
let seats = env.get_all_seats();
|
let seats = env.get_all_seats();
|
||||||
|
|
||||||
|
// TODO: Actually handle seats properly
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
let default_seat = seats[0].detach();
|
||||||
|
|
||||||
send!(
|
send!(
|
||||||
seat_tx,
|
seat_tx,
|
||||||
seats
|
seats
|
||||||
@@ -58,30 +111,56 @@ impl WaylandClient {
|
|||||||
.collect::<Vec<WlSeat>>()
|
.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 _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
|
||||||
|
|
||||||
let _listener = listen_for_toplevels(env, move |handle, event, _ddata| {
|
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
|
||||||
trace!("Received toplevel event: {:?}", event);
|
super::wlr_foreign_toplevel::update_toplevels(
|
||||||
|
&toplevels,
|
||||||
if event.change == ToplevelChange::Close {
|
handle,
|
||||||
write_lock!(toplevels2).remove(&event.toplevel.id);
|
event,
|
||||||
} else {
|
&toplevel_tx,
|
||||||
write_lock!(toplevels2)
|
);
|
||||||
.insert(event.toplevel.id, (event.toplevel.clone(), handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
send!(toplevel_tx2, event);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut event_loop =
|
cfg_if! {
|
||||||
calloop::EventLoop::<()>::try_new().expect("Failed to create new event loop");
|
if #[cfg(feature = "clipboard")] {
|
||||||
WaylandSource::new(queue)
|
let clipboard_tx = clipboard_tx2;
|
||||||
.quick_insert(event_loop.handle())
|
let handle = event_loop.handle();
|
||||||
.expect("Failed to insert event loop into wayland event queue");
|
|
||||||
|
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 {
|
loop {
|
||||||
// TODO: Avoid need for duration here - can we force some event when sending requests?
|
if let Err(err) = event_loop.dispatch(None, &mut data) {
|
||||||
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
|
|
||||||
error!(
|
error!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
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 outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||||
|
|
||||||
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
|
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||||
@@ -97,9 +188,14 @@ impl WaylandClient {
|
|||||||
Self {
|
Self {
|
||||||
outputs,
|
outputs,
|
||||||
seats,
|
seats,
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
clipboard,
|
||||||
toplevels,
|
toplevels,
|
||||||
toplevel_tx,
|
toplevel_tx,
|
||||||
_toplevel_rx: toplevel_rx,
|
_toplevel_rx: toplevel_rx,
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
clipboard_tx,
|
||||||
|
request_tx: ev_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +203,26 @@ impl WaylandClient {
|
|||||||
self.toplevel_tx.subscribe()
|
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> {
|
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
|
||||||
let outputs = env.get_all_outputs();
|
let outputs = env.get_all_outputs();
|
||||||
|
|
||||||
@@ -115,4 +231,57 @@ impl WaylandClient {
|
|||||||
.filter_map(|output| with_output_info(output, Clone::clone))
|
.filter_map(|output| with_output_info(output, Clone::clone))
|
||||||
.collect()
|
.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}");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
mod client;
|
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 async_once::AsyncOnce;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
pub use toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
|
use std::fmt::Debug;
|
||||||
use toplevel_manager::{ToplevelHandler, ToplevelHandling, ToplevelStatusListener};
|
use cfg_if::cfg_if;
|
||||||
use wayland_client::{Attached, DispatchData, Interface};
|
use smithay_client_toolkit::default_environment;
|
||||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
use smithay_client_toolkit::environment::Environment;
|
||||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
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;
|
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.
|
/// A utility for lazy-loading globals.
|
||||||
/// Taken from `smithay_client_toolkit` where it's not exposed
|
/// Taken from `smithay_client_toolkit` where it's not exposed
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -25,21 +36,32 @@ enum LazyGlobal<I: Interface> {
|
|||||||
Bound(Attached<I>),
|
Bound(Attached<I>),
|
||||||
}
|
}
|
||||||
|
|
||||||
sctk::default_environment!(Env,
|
pub struct DData {
|
||||||
|
env: Environment<Env>,
|
||||||
|
offer_tokens: HashMap<u128, RegistrationToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "clipboard")] {
|
||||||
|
default_environment!(Env,
|
||||||
fields = [
|
fields = [
|
||||||
toplevel: ToplevelHandler
|
toplevel: ToplevelHandler,
|
||||||
|
data_control_device: DataControlDeviceHandler
|
||||||
],
|
],
|
||||||
singles = [
|
singles = [
|
||||||
ZwlrForeignToplevelManagerV1 => toplevel
|
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||||
|
ZwlrDataControlManagerV1 => data_control_device
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
impl ToplevelHandling for Env {
|
default_environment!(Env,
|
||||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
fields = [
|
||||||
where
|
toplevel: ToplevelHandler,
|
||||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
],
|
||||||
{
|
singles = [
|
||||||
self.toplevel.listen(f)
|
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
88
src/clients/wayland/wlr_data_control/device.rs
Normal file
88
src/clients/wayland/wlr_data_control/device.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
253
src/clients/wayland/wlr_data_control/manager.rs
Normal file
253
src/clients/wayland/wlr_data_control/manager.rs
Normal 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))
|
||||||
|
}
|
||||||
258
src/clients/wayland/wlr_data_control/mod.rs
Normal file
258
src/clients/wayland/wlr_data_control/mod.rs
Normal 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(×tamp) {
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
74
src/clients/wayland/wlr_data_control/offer.rs
Normal file
74
src/clients/wayland/wlr_data_control/offer.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/clients/wayland/wlr_data_control/source.rs
Normal file
54
src/clients/wayland/wlr_data_control/source.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use super::toplevel::{Toplevel, ToplevelEvent};
|
use super::handle::{Toplevel, ToplevelEvent};
|
||||||
use super::LazyGlobal;
|
use crate::wayland::LazyGlobal;
|
||||||
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc;
|
use std::rc::{self, Rc};
|
||||||
use std::rc::Rc;
|
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||||
use wayland_client::{Attached, DispatchData};
|
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
|
where
|
||||||
E: ToplevelHandling,
|
E: ToplevelHandling,
|
||||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||||
39
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
39
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
use super::{BarPosition, Config, MonitorConfig};
|
use super::{BarPosition, Config, MonitorConfig};
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use color_eyre::eyre::{ContextCompat, WrapErr};
|
|
||||||
use color_eyre::{Help, Report};
|
use color_eyre::{Help, Report};
|
||||||
use dirs::config_dir;
|
|
||||||
use gtk::Orientation;
|
use gtk::Orientation;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::{env, fs};
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
// Manually implement for better untagged enum error handling:
|
// Manually implement for better untagged enum error handling:
|
||||||
// currently open pr: https://github.com/serde-rs/serde/pull/1544
|
// currently open pr: https://github.com/serde-rs/serde/pull/1544
|
||||||
impl<'de> Deserialize<'de> for MonitorConfig {
|
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
|
where
|
||||||
D: Deserializer<'de>,
|
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")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
mod r#impl;
|
mod r#impl;
|
||||||
mod truncate;
|
mod truncate;
|
||||||
|
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
use crate::modules::clipboard::ClipboardModule;
|
||||||
#[cfg(feature = "clock")]
|
#[cfg(feature = "clock")]
|
||||||
use crate::modules::clock::ClockModule;
|
use crate::modules::clock::ClockModule;
|
||||||
use crate::modules::custom::CustomModule;
|
use crate::modules::custom::CustomModule;
|
||||||
@@ -38,19 +40,21 @@ pub struct CommonConfig {
|
|||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum ModuleConfig {
|
pub enum ModuleConfig {
|
||||||
#[cfg(feature = "clock")]
|
#[cfg(feature = "clock")]
|
||||||
Clock(ClockModule),
|
Clipboard(Box<ClipboardModule>),
|
||||||
Custom(CustomModule),
|
#[cfg(feature = "clock")]
|
||||||
Focused(FocusedModule),
|
Clock(Box<ClockModule>),
|
||||||
Launcher(LauncherModule),
|
Custom(Box<CustomModule>),
|
||||||
|
Focused(Box<FocusedModule>),
|
||||||
|
Launcher(Box<LauncherModule>),
|
||||||
#[cfg(feature = "music")]
|
#[cfg(feature = "music")]
|
||||||
Music(MusicModule),
|
Music(Box<MusicModule>),
|
||||||
Script(ScriptModule),
|
Script(Box<ScriptModule>),
|
||||||
#[cfg(feature = "sys_info")]
|
#[cfg(feature = "sys_info")]
|
||||||
SysInfo(SysInfoModule),
|
SysInfo(Box<SysInfoModule>),
|
||||||
#[cfg(feature = "tray")]
|
#[cfg(feature = "tray")]
|
||||||
Tray(TrayModule),
|
Tray(Box<TrayModule>),
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
Workspaces(WorkspacesModule),
|
Workspaces(Box<WorkspacesModule>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -74,14 +78,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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_bar_position")]
|
#[serde(default)]
|
||||||
pub position: BarPosition,
|
pub position: BarPosition,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub anchor_to_edges: bool,
|
pub anchor_to_edges: bool,
|
||||||
#[serde(default = "default_bar_height")]
|
#[serde(default = "default_bar_height")]
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub margin: MarginConfig,
|
||||||
|
|
||||||
/// GTK icon theme to use.
|
/// GTK icon theme to use.
|
||||||
pub icon_theme: Option<String>,
|
pub icon_theme: Option<String>,
|
||||||
@@ -93,10 +111,6 @@ pub struct Config {
|
|||||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_bar_position() -> BarPosition {
|
|
||||||
BarPosition::Bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_bar_height() -> i32 {
|
const fn default_bar_height() -> i32 {
|
||||||
42
|
42
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,31 +24,43 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
|
|||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum TruncateMode {
|
pub enum TruncateMode {
|
||||||
Auto(EllipsizeMode),
|
Auto(EllipsizeMode),
|
||||||
MaxLength {
|
Length {
|
||||||
mode: EllipsizeMode,
|
mode: EllipsizeMode,
|
||||||
length: Option<i32>,
|
length: Option<i32>,
|
||||||
|
max_length: Option<i32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TruncateMode {
|
impl TruncateMode {
|
||||||
const fn mode(&self) -> EllipsizeMode {
|
const fn mode(&self) -> EllipsizeMode {
|
||||||
match self {
|
match self {
|
||||||
Self::MaxLength { mode, .. } | Self::Auto(mode) => *mode,
|
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn length(&self) -> Option<i32> {
|
const fn length(&self) -> Option<i32> {
|
||||||
match self {
|
match self {
|
||||||
Self::Auto(_) => None,
|
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: >k::Label) {
|
pub fn truncate_label(&self, label: >k::Label) {
|
||||||
label.set_ellipsize(self.mode().into());
|
label.set_ellipsize(self.mode().into());
|
||||||
|
|
||||||
if let Some(max_length) = self.length() {
|
if let Some(length) = self.length() {
|
||||||
label.set_max_width_chars(max_length);
|
label.set_width_chars(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(length) = self.max_length() {
|
||||||
|
label.set_max_width_chars(length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::script::{OutputStream, Script};
|
use crate::script::{OutputStream, Script};
|
||||||
use crate::{lock, send};
|
use crate::{lock, send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use indexmap::IndexMap;
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
|
||||||
@@ -60,31 +59,30 @@ impl DynamicString {
|
|||||||
chars.drain(..skip);
|
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);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
for (i, segment) in segments.into_iter().enumerate() {
|
for (i, segment) in segments.into_iter().enumerate() {
|
||||||
match segment {
|
match segment {
|
||||||
DynamicStringSegment::Static(str) => {
|
DynamicStringSegment::Static(str) => {
|
||||||
lock!(label_parts).insert(i, str);
|
lock!(label_parts).push(str);
|
||||||
}
|
}
|
||||||
DynamicStringSegment::Dynamic(script) => {
|
DynamicStringSegment::Dynamic(script) => {
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let label_parts = label_parts.clone();
|
let label_parts = label_parts.clone();
|
||||||
|
|
||||||
|
// insert blank value to preserve segment order
|
||||||
|
lock!(label_parts).push(String::new());
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script
|
script
|
||||||
.run(|(out, _)| {
|
.run(|(out, _)| {
|
||||||
if let OutputStream::Stdout(out) = out {
|
if let OutputStream::Stdout(out) = out {
|
||||||
let mut label_parts = lock!(label_parts);
|
let mut label_parts = lock!(label_parts);
|
||||||
|
|
||||||
label_parts.insert(i, out);
|
let _ = std::mem::replace(&mut label_parts[i], out);
|
||||||
|
|
||||||
let string = label_parts
|
|
||||||
.iter()
|
|
||||||
.map(|(_, part)| part.as_str())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
|
let string = label_parts.join("");
|
||||||
send!(tx, string);
|
send!(tx, string);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -96,11 +94,7 @@ impl DynamicString {
|
|||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
{
|
{
|
||||||
let label_parts = lock!(label_parts)
|
let label_parts = lock!(label_parts).join("");
|
||||||
.iter()
|
|
||||||
.map(|(_, part)| part.as_str())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
send!(tx, label_parts);
|
send!(tx, label_parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ use gtk::prelude::*;
|
|||||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||||
use tracing::error;
|
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 {
|
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||||
let button = Button::new();
|
let button = Button::new();
|
||||||
|
|
||||||
if ImageProvider::is_definitely_image_input(input) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
|
image.set_widget_name("image");
|
||||||
|
|
||||||
match ImageProvider::parse(input, icon_theme, size)
|
match ImageProvider::parse(input, icon_theme, size)
|
||||||
.and_then(|provider| provider.load_into_image(image.clone()))
|
.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) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
|
image.set_widget_name("image");
|
||||||
|
|
||||||
container.add(&image);
|
container.add(&image);
|
||||||
|
|
||||||
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
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 {
|
} else {
|
||||||
let label = Label::new(Some(input));
|
let label = Label::new(Some(input));
|
||||||
|
label.set_widget_name("label");
|
||||||
|
|
||||||
container.add(&label);
|
container.add(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||||
mod gtk;
|
mod gtk;
|
||||||
mod provider;
|
mod provider;
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl<'a> ImageProvider<'a> {
|
|||||||
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
||||||
}
|
}
|
||||||
None => get_desktop_icon_name(input_name).map_or_else(
|
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),
|
|input| Self::get_location(&input, theme, size),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -132,16 +132,16 @@ impl<'a> ImageProvider<'a> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.load_into_image_sync(image)?;
|
self.load_into_image_sync(&image)?;
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "http"))]
|
#[cfg(not(feature = "http"))]
|
||||||
self.load_into_image_sync(image)?;
|
self.load_into_image_sync(&image)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_into_image_sync(&self, image: gtk::Image) -> Result<()> {
|
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||||
let pixbuf = match &self.location {
|
let pixbuf = match &self.location {
|
||||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
||||||
ImageLocation::Local(path) => self.get_from_file(path),
|
ImageLocation::Local(path) => self.get_from_file(path),
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -32,6 +32,7 @@ use tokio::task::block_in_place;
|
|||||||
use crate::error::ExitCode;
|
use crate::error::ExitCode;
|
||||||
use clients::wayland::{self, WaylandClient};
|
use clients::wayland::{self, WaylandClient};
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
use universal_config::ConfigLoader;
|
||||||
|
|
||||||
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
@@ -43,6 +44,8 @@ async fn main() -> Result<()> {
|
|||||||
info!("Ironbar version {}", VERSION);
|
info!("Ironbar version {}", VERSION);
|
||||||
info!("Starting application");
|
info!("Starting application");
|
||||||
|
|
||||||
|
clients::volume::pulse_bak::test();
|
||||||
|
|
||||||
let wayland_client = wayland::get_client().await;
|
let wayland_client = wayland::get_client().await;
|
||||||
|
|
||||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||||
@@ -57,13 +60,19 @@ async fn main() -> Result<()> {
|
|||||||
|display| display,
|
|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,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(ExitCode::Config as i32)
|
exit(ExitCode::Config as i32)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Loaded config file");
|
debug!("Loaded config file");
|
||||||
|
|
||||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||||
|
|||||||
315
src/modules/clipboard.rs
Normal file
315
src/modules/clipboard.rs
Normal 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())
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ impl Widget {
|
|||||||
let mut builder = Label::builder().use_markup(true);
|
let mut builder = Label::builder().use_markup(true);
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
if let Some(name) = self.name {
|
||||||
builder = builder.name(&name);
|
builder = builder.name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let label = builder.build();
|
let label = builder.build();
|
||||||
@@ -143,7 +143,7 @@ impl Widget {
|
|||||||
let mut builder = Button::builder();
|
let mut builder = Button::builder();
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
if let Some(name) = self.name {
|
||||||
builder = builder.name(&name);
|
builder = builder.name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let button = builder.build();
|
let button = builder.build();
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
let wl = wayland::get_client().await;
|
let wl = wayland::get_client().await;
|
||||||
let open_windows = read_lock!(wl.toplevels);
|
let open_windows = read_lock!(wl.toplevels);
|
||||||
|
|
||||||
|
let open_windows = open_windows.clone();
|
||||||
|
for (_, (window, _)) in open_windows {
|
||||||
let mut items = lock!(items);
|
let mut items = lock!(items);
|
||||||
|
|
||||||
for (_, (window, _)) in open_windows.clone() {
|
|
||||||
let item = items.get_mut(&window.app_id);
|
let item = items.get_mut(&window.app_id);
|
||||||
match item {
|
match item {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
@@ -124,6 +124,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let items = lock!(items);
|
||||||
let items = items.iter();
|
let items = items.iter();
|
||||||
for (_, item) in items {
|
for (_, item) in items {
|
||||||
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||||
@@ -281,7 +282,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
ItemEvent::FocusItem(app_id) => items
|
ItemEvent::FocusItem(app_id) => items
|
||||||
.get(&app_id)
|
.get(&app_id)
|
||||||
.and_then(|item| item.windows.first().map(|(_, win)| win.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!(),
|
ItemEvent::OpenItem(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -292,6 +293,9 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
handle.activate(seat);
|
handle.activate(seat);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roundtrip to immediately send activate event
|
||||||
|
wl.roundtrip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -434,7 +438,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, win)| {
|
.map(|(_, win)| {
|
||||||
let button = Button::builder()
|
let button = Button::builder()
|
||||||
.label(&clamp(&win.name))
|
.label(clamp(&win.name))
|
||||||
.height_request(40)
|
.height_request(40)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -464,7 +468,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
let button = Button::builder()
|
let button = Button::builder()
|
||||||
.height_request(40)
|
.height_request(40)
|
||||||
.label(&clamp(&win.name))
|
.label(clamp(&win.name))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
pub mod clipboard;
|
||||||
/// Displays the current date and time.
|
/// Displays the current date and time.
|
||||||
///
|
///
|
||||||
/// A custom date/time format string can be set in the config.
|
/// A custom date/time format string can be set in the config.
|
||||||
|
|||||||
@@ -361,16 +361,19 @@ fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System
|
|||||||
// no refresh required for these tokens
|
// no refresh required for these tokens
|
||||||
|
|
||||||
let load_average = sys.load_average();
|
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(
|
format_info.insert(
|
||||||
String::from("load_average:5"),
|
String::from("load_average:5"),
|
||||||
load_average.five.to_string(),
|
format!("{:.2}", load_average.five),
|
||||||
);
|
);
|
||||||
|
|
||||||
format_info.insert(
|
format_info.insert(
|
||||||
String::from("load_average:15"),
|
String::from("load_average:15"),
|
||||||
load_average.fifteen.to_string(),
|
format!("{:.2}", load_average.fifteen),
|
||||||
);
|
);
|
||||||
|
|
||||||
let uptime = Duration::from_secs(sys.uptime()).as_secs();
|
let uptime = Duration::from_secs(sys.uptime()).as_secs();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ impl Popup {
|
|||||||
|
|
||||||
gtk_layer_shell::init_for_window(&win);
|
gtk_layer_shell::init_for_window(&win);
|
||||||
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
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(
|
gtk_layer_shell::set_margin(
|
||||||
&win,
|
&win,
|
||||||
|
|||||||
Reference in New Issue
Block a user