Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789d54e2ea | ||
|
|
be726ddde8 | ||
|
|
96ff974b82 | ||
|
|
226bca632e | ||
|
|
446720c578 | ||
|
|
7625635050 | ||
|
|
8576ac5c44 | ||
|
|
8518262053 | ||
|
|
a29df37e77 | ||
|
|
2d755d37e5 | ||
|
|
001dd5473a | ||
|
|
120580994c | ||
|
|
18e088d593 | ||
|
|
2c6338f82f | ||
|
|
882fa30b66 | ||
|
|
3c564a7774 | ||
|
|
73f0e7e48e | ||
|
|
5e72a7ba32 | ||
|
|
de80b99e64 | ||
|
|
ebf7a97a85 | ||
|
|
7a515359ac | ||
|
|
2e0f033bed | ||
|
|
dc14cb003f | ||
|
|
e416e03b0a | ||
|
|
65c5d391d9 | ||
|
|
53adaa846c | ||
|
|
a358037d3e | ||
|
|
19d009fe5b |
37
.github/workflows/build.yml
vendored
Normal file
37
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
45
.github/workflows/deploy.yml
vendored
Normal file
45
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Update CHANGELOG
|
||||
id: changelog
|
||||
uses: Requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
name: ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog.outputs.changes }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
- name: Commit CHANGELOG.md
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
branch: main
|
||||
commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]'
|
||||
file_pattern: CHANGELOG.md
|
||||
|
||||
- uses: katyo/publish-crates@v1
|
||||
with:
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
1
.idea/ironbar.iml
generated
1
.idea/ironbar.iml
generated
@@ -2,6 +2,7 @@
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
|
||||
4
.idea/runConfigurations/Clippy.xml
generated
4
.idea/runConfigurations/Clippy.xml
generated
@@ -1,6 +1,6 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
|
||||
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used" />
|
||||
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="clippy" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="false" />
|
||||
|
||||
19
.idea/runConfigurations/Clippy__Strict_.xml
generated
Normal file
19
.idea/runConfigurations/Clippy__Strict_.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="false" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="emulateTerminal" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
1
.idea/runConfigurations/Run.xml
generated
1
.idea/runConfigurations/Run.xml
generated
@@ -10,6 +10,7 @@
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs>
|
||||
<env name="IRONBAR_CONFIG" value="examples/config.json" />
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
|
||||
1
.idea/runConfigurations/Run__Debug_.xml
generated
1
.idea/runConfigurations/Run__Debug_.xml
generated
@@ -11,6 +11,7 @@
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs>
|
||||
<env name="GTK_DEBUG" value="interactive" />
|
||||
<env name="IRONBAR_CONFIG" value="examples/config.json" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
|
||||
19
.idea/runConfigurations/Run__Live_Config_.xml
generated
Normal file
19
.idea/runConfigurations/Run__Live_Config_.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run (Live Config)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="run" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="emulateTerminal" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1033,7 +1033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironbar"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cornfig",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable wlroots/sway bar"
|
||||
|
||||
69
README.md
69
README.md
@@ -8,74 +8,39 @@ For information and examples on styling please see the [wiki](https://github.com
|
||||
|
||||

|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Install with cargo:
|
||||
Run using `ironbar`.
|
||||
|
||||
### Cargo
|
||||
|
||||
```sh
|
||||
cargo install ironbar
|
||||
```
|
||||
|
||||
Then just run with `ironbar`.
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
### Arch Linux
|
||||
|
||||
```sh
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, running will get you a blank bar. To start, you will need a configuration file in `.config/ironbar`.
|
||||
Ironbar supports a range of file formats so pick your favourite:
|
||||
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||
and options for scaling complexity: you can use a single config across all monitors,
|
||||
or configure different/multiple bars per monitor.
|
||||
|
||||
- JSON
|
||||
- TOML
|
||||
- YAML
|
||||
- [Corn](https://github.com/jakestanger/corn) (Experimental. JSON/Nix like config lang. Supports variables.)
|
||||
|
||||
For a full list of modules and their configuration options, please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||
|
||||
There are two different approaches to configuring the bar:
|
||||
|
||||
### Same configuration across all monitors
|
||||
|
||||
> If you have a single monitor, or want the same bar to appear across each of your monitors, choose this option.
|
||||
|
||||
The top-level object takes any combination of `left`, `center`, and `right`. These each take a list of modules and determine where they are positioned.
|
||||
|
||||
```json
|
||||
{
|
||||
"left": [],
|
||||
"center": [],
|
||||
"right": []
|
||||
}
|
||||
```
|
||||
|
||||
### Different configuration across monitors
|
||||
|
||||
> If you have multiple monitors and want them to differ in configuration, choose this option.
|
||||
|
||||
The top-level object takes a single key called `monitors`. This takes an array where each entry is an object with a configuration for each monitor.
|
||||
The monitor's config object takes any combination of `left`, `center`, and `right`. These each take a list of modules and determine where they are positioned.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"left": [],
|
||||
"center": [],
|
||||
"right": []
|
||||
},
|
||||
{
|
||||
"left": [],
|
||||
"center": [],
|
||||
"right": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
|
||||
|
||||
## Styling
|
||||
|
||||
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the file.
|
||||
|
||||
An example stylesheet and information about each module's styling information can be found on the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
|
||||
|
||||
## Project Status
|
||||
|
||||
|
||||
43
examples/config.corn
Normal file
43
examples/config.corn
Normal file
@@ -0,0 +1,43 @@
|
||||
let {
|
||||
$workspaces = {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
2 = ""
|
||||
3 = ""
|
||||
Games = ""
|
||||
Code = ""
|
||||
}
|
||||
}
|
||||
|
||||
$launcher = {
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord" "Steam"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
icon_theme = "Paper"
|
||||
}
|
||||
|
||||
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
|
||||
$mpd_server = { type = "mpd" host = "chloe:6600" }
|
||||
|
||||
$sys_info = {
|
||||
type = "sys-info"
|
||||
format = ["{cpu-percent}% " "{memory-percent}% "]
|
||||
}
|
||||
|
||||
$tray = { type = "tray" }
|
||||
$clock = { type = "clock" }
|
||||
|
||||
$phone_battery = {
|
||||
type = "script"
|
||||
path = "/home/jake/bin/phone-battery"
|
||||
}
|
||||
|
||||
$left = [ $workspaces $launcher ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
|
||||
}
|
||||
in {
|
||||
left = $left right = $right
|
||||
}
|
||||
18
examples/config.json
Normal file
18
examples/config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"monitors": {
|
||||
"DP-1": [
|
||||
{
|
||||
"left": [{"type": "clock"}]
|
||||
},
|
||||
{
|
||||
"position": "top",
|
||||
"left": []
|
||||
}
|
||||
],
|
||||
"DP-2": {
|
||||
"position": "bottom",
|
||||
"height": 30,
|
||||
"left": []
|
||||
}
|
||||
}
|
||||
}
|
||||
148
examples/style.css
Normal file
148
examples/style.css
Normal file
@@ -0,0 +1,148 @@
|
||||
* {
|
||||
/* `otf-font-awesome` is required to be installed for icons */
|
||||
font-family: Noto Sans Nerd Font, sans-serif;
|
||||
/* font-family: 'Jetbrains Mono', monospace;*/
|
||||
font-size: 16px;
|
||||
|
||||
/*color: white;*/
|
||||
/*background-color: #2d2d2d;*/
|
||||
/*background-color: red;*/
|
||||
border: none;
|
||||
|
||||
/*opacity: 0.4;*/
|
||||
}
|
||||
|
||||
#bar {
|
||||
border-top: 1px solid #424242;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
/* test 34543*/
|
||||
|
||||
#right > * + * {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#workspaces .item {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#workspaces *:not(.focused):hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
#launcher .item {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#launcher .item:not(.focused):hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#launcher .open {
|
||||
border-bottom: 2px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .focused {
|
||||
color: white;
|
||||
background-color: black;
|
||||
border-bottom: 4px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .urgent {
|
||||
color: white;
|
||||
background-color: #8f0a0a;
|
||||
}
|
||||
|
||||
#clock {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#script {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sysinfo {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tray .item {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
#mpd {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clock {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#calendar-clock {
|
||||
color: white;
|
||||
font-size: 2.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#calendar .header {
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #424242;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#calendar:selected {
|
||||
background-color: #6699cc;
|
||||
}
|
||||
|
||||
#popup-mpd {
|
||||
color: white;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #album-art {
|
||||
/*border: 1px solid #424242;*/
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #title .icon, #popup-mpd #title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
#popup-mpd #controls * {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#popup-mpd #controls *:disabled {
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
#focused {
|
||||
color: white;
|
||||
}
|
||||
94
src/bar.rs
94
src/bar.rs
@@ -1,20 +1,20 @@
|
||||
use crate::config::ModuleConfig;
|
||||
use crate::config::{BarPosition, ModuleConfig};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleLocation};
|
||||
use crate::Config;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, Orientation};
|
||||
|
||||
pub fn create_bar(app: &Application, monitor: &Monitor, config: Config) {
|
||||
pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, config: Config) {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
|
||||
setup_layer_shell(&win, monitor);
|
||||
setup_layer_shell(&win, monitor, &config.position);
|
||||
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(0)
|
||||
.hexpand(false)
|
||||
.height_request(42)
|
||||
.height_request(config.height)
|
||||
.name("bar")
|
||||
.build();
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn create_bar(app: &Application, monitor: &Monitor, config: Config) {
|
||||
content.set_center_widget(Some(¢er));
|
||||
content.pack_end(&right, false, false, 0);
|
||||
|
||||
load_modules(&left, ¢er, &right, app, config);
|
||||
load_modules(&left, ¢er, &right, app, config, monitor, monitor_name);
|
||||
win.add(&content);
|
||||
|
||||
win.connect_destroy_event(|_, _| {
|
||||
@@ -48,78 +48,70 @@ fn load_modules(
|
||||
right: >k::Box,
|
||||
app: &Application,
|
||||
config: Config,
|
||||
monitor: &Monitor,
|
||||
output_name: &str,
|
||||
) {
|
||||
if let Some(modules) = config.left {
|
||||
let info = ModuleInfo {
|
||||
app,
|
||||
location: ModuleLocation::Left,
|
||||
bar_position: &config.position,
|
||||
monitor,
|
||||
output_name,
|
||||
};
|
||||
|
||||
add_modules(left, modules, info);
|
||||
add_modules(left, modules, &info);
|
||||
}
|
||||
|
||||
if let Some(modules) = config.center {
|
||||
let info = ModuleInfo {
|
||||
app,
|
||||
location: ModuleLocation::Center,
|
||||
bar_position: &config.position,
|
||||
monitor,
|
||||
output_name,
|
||||
};
|
||||
|
||||
add_modules(center, modules, info);
|
||||
add_modules(center, modules, &info);
|
||||
}
|
||||
|
||||
if let Some(modules) = config.right {
|
||||
let info = ModuleInfo {
|
||||
app,
|
||||
location: ModuleLocation::Right,
|
||||
bar_position: &config.position,
|
||||
monitor,
|
||||
output_name,
|
||||
};
|
||||
|
||||
add_modules(right, modules, info);
|
||||
add_modules(right, modules, &info);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: ModuleInfo) {
|
||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) {
|
||||
macro_rules! add_module {
|
||||
($module:expr, $name:literal) => {{
|
||||
let widget = $module.into_widget(&info);
|
||||
widget.set_widget_name($name);
|
||||
content.add(&widget);
|
||||
}};
|
||||
}
|
||||
|
||||
for config in modules {
|
||||
match config {
|
||||
ModuleConfig::Clock(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("clock");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Mpd(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("mpd");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Tray(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("tray");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Workspaces(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("workspaces");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::SysInfo(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("sysinfo");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Launcher(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("launcher");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Script(module) => {
|
||||
let widget = module.into_widget(&info);
|
||||
widget.set_widget_name("script");
|
||||
content.add(&widget);
|
||||
}
|
||||
ModuleConfig::Clock(module) => add_module!(module, "clock"),
|
||||
ModuleConfig::Mpd(module) => add_module!(module, "mpd"),
|
||||
ModuleConfig::Tray(module) => add_module!(module, "tray"),
|
||||
ModuleConfig::Workspaces(module) => add_module!(module, "workspaces"),
|
||||
ModuleConfig::SysInfo(module) => add_module!(module, "sysinfo"),
|
||||
ModuleConfig::Launcher(module) => add_module!(module, "launcher"),
|
||||
ModuleConfig::Script(module) => add_module!(module, "script"),
|
||||
ModuleConfig::Focused(module) => add_module!(module, "focused"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor) {
|
||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||
@@ -130,8 +122,16 @@ fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor) {
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
|
||||
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Top, false);
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Bottom, true);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
position == &BarPosition::Top,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
position == &BarPosition::Bottom,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Left, true);
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Right, true);
|
||||
}
|
||||
|
||||
114
src/config.rs
114
src/config.rs
@@ -1,4 +1,5 @@
|
||||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
use crate::modules::mpd::MpdModule;
|
||||
use crate::modules::script::ScriptModule;
|
||||
@@ -7,7 +8,9 @@ use crate::modules::tray::TrayModule;
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use dirs::config_dir;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
@@ -19,47 +22,102 @@ pub enum ModuleConfig {
|
||||
SysInfo(SysInfoModule),
|
||||
Launcher(LauncherModule),
|
||||
Script(ScriptModule),
|
||||
Focused(FocusedModule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum MonitorConfig {
|
||||
Single(Config),
|
||||
Multiple(Vec<Config>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BarPosition {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Default for BarPosition {
|
||||
fn default() -> Self {
|
||||
Self::Bottom
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_bar_position")]
|
||||
pub position: BarPosition,
|
||||
#[serde(default = "default_bar_height")]
|
||||
pub height: i32,
|
||||
|
||||
pub left: Option<Vec<ModuleConfig>>,
|
||||
pub center: Option<Vec<ModuleConfig>>,
|
||||
pub right: Option<Vec<ModuleConfig>>,
|
||||
|
||||
pub monitors: Option<Vec<Config>>,
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
}
|
||||
|
||||
const fn default_bar_position() -> BarPosition {
|
||||
BarPosition::Bottom
|
||||
}
|
||||
|
||||
const fn default_bar_height() -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Option<Self> {
|
||||
let config_dir = config_dir().expect("Failed to locate user config dir");
|
||||
if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
||||
let path = PathBuf::from(config_path);
|
||||
Self::load_file(
|
||||
&path,
|
||||
path.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
let config_dir = config_dir().expect("Failed to locate user config dir");
|
||||
|
||||
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
|
||||
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
|
||||
|
||||
extensions.into_iter().find_map(|extension| {
|
||||
let full_path = config_dir
|
||||
.join("ironbar")
|
||||
.join(format!("config.{extension}"));
|
||||
extensions.into_iter().find_map(|extension| {
|
||||
let full_path = config_dir
|
||||
.join("ironbar")
|
||||
.join(format!("config.{extension}"));
|
||||
|
||||
if full_path.exists() {
|
||||
let file = fs::read(full_path).expect("Failed to read config file");
|
||||
Some(match extension {
|
||||
"json" => serde_json::from_slice(&file).expect("Invalid JSON config"),
|
||||
"toml" => toml::from_slice(&file).expect("Invalid TOML config"),
|
||||
"yaml" | "yml" => serde_yaml::from_slice(&file).expect("Invalid YAML config"),
|
||||
"corn" => {
|
||||
// corn doesn't support deserialization yet
|
||||
// so serialize the interpreted result then deserialize that
|
||||
let file =
|
||||
String::from_utf8(file).expect("Config file contains invalid UTF-8");
|
||||
let config = cornfig::parse(&file).expect("Invalid corn config").value;
|
||||
serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
Self::load_file(&full_path, extension)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file(path: &Path, extension: &str) -> Option<Self> {
|
||||
if path.exists() {
|
||||
let file = fs::read(path).expect("Failed to read config file");
|
||||
Some(match extension {
|
||||
"json" => serde_json::from_slice(&file).expect("Invalid JSON config"),
|
||||
"toml" => toml::from_slice(&file).expect("Invalid TOML config"),
|
||||
"yaml" | "yml" => serde_yaml::from_slice(&file).expect("Invalid YAML config"),
|
||||
"corn" => {
|
||||
// corn doesn't support deserialization yet
|
||||
// so serialize the interpreted result then deserialize that
|
||||
let file = String::from_utf8(file).expect("Config file contains invalid UTF-8");
|
||||
let config = cornfig::parse(&file).expect("Invalid corn config").value;
|
||||
serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
pub const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
|
||||
|
||||
match icon_location {
|
||||
Some(IconLocation::Theme(icon_name)) => {
|
||||
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::empty());
|
||||
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
|
||||
|
||||
match icon {
|
||||
Ok(icon) => icon,
|
||||
45
src/main.rs
45
src/main.rs
@@ -1,16 +1,21 @@
|
||||
mod bar;
|
||||
mod collection;
|
||||
mod config;
|
||||
mod icon;
|
||||
mod modules;
|
||||
mod popup;
|
||||
mod style;
|
||||
mod sway;
|
||||
|
||||
use crate::bar::create_bar;
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::style::load_css;
|
||||
use crate::sway::SwayOutput;
|
||||
use dirs::config_dir;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{gdk, Application};
|
||||
use ksway::client::Client;
|
||||
use ksway::IpcCommand;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -18,7 +23,14 @@ async fn main() {
|
||||
.application_id("dev.jstanger.waylandbar")
|
||||
.build();
|
||||
|
||||
app.connect_activate(|app| {
|
||||
let mut sway_client = Client::connect().expect("Failed to connect to Sway IPC");
|
||||
let outputs = sway_client
|
||||
.ipc(IpcCommand::GetOutputs)
|
||||
.expect("Failed to get Sway outputs");
|
||||
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)
|
||||
.expect("Failed to deserialize outputs message from Sway IPC");
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
let config = Config::load().unwrap_or_default();
|
||||
|
||||
// TODO: Better logging (https://crates.io/crates/tracing)
|
||||
@@ -28,14 +40,33 @@ async fn main() {
|
||||
|
||||
let display = gdk::Display::default().expect("Failed to get default GDK display");
|
||||
let num_monitors = display.n_monitors();
|
||||
|
||||
for i in 0..num_monitors {
|
||||
let monitor = display.monitor(i).unwrap();
|
||||
let monitor_name = &outputs
|
||||
.get(i as usize)
|
||||
.expect("GTK monitor output differs from Sway's")
|
||||
.name;
|
||||
|
||||
let config = config.monitors.as_ref().map_or(&config, |monitor_config| {
|
||||
monitor_config.get(i as usize).unwrap_or(&config)
|
||||
});
|
||||
|
||||
create_bar(app, &monitor, config.clone());
|
||||
config.monitors.as_ref().map_or_else(
|
||||
|| {
|
||||
create_bar(app, &monitor, monitor_name, config.clone());
|
||||
},
|
||||
|config| {
|
||||
let config = config.get(monitor_name);
|
||||
match &config {
|
||||
Some(MonitorConfig::Single(config)) => {
|
||||
create_bar(app, &monitor, monitor_name, config.clone());
|
||||
}
|
||||
Some(MonitorConfig::Multiple(configs)) => {
|
||||
for config in configs {
|
||||
create_bar(app, &monitor, monitor_name, config.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
let style_path = config_dir()
|
||||
|
||||
@@ -2,7 +2,6 @@ mod popup;
|
||||
|
||||
use self::popup::Popup;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::popup::PopupAlignment;
|
||||
use chrono::Local;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
@@ -30,20 +29,19 @@ impl Module<Button> for ClockModule {
|
||||
fn into_widget(self, info: &ModuleInfo) -> Button {
|
||||
let button = Button::new();
|
||||
|
||||
let popup = Popup::new("popup-clock", info.app, Orientation::Vertical);
|
||||
let popup = Popup::new(
|
||||
"popup-clock",
|
||||
info.app,
|
||||
info.monitor,
|
||||
Orientation::Vertical,
|
||||
info.bar_position,
|
||||
);
|
||||
popup.add_clock_widgets();
|
||||
|
||||
button.show_all();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
let button_w = button.allocation().width();
|
||||
|
||||
let (button_x, _) = button
|
||||
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
popup.show();
|
||||
popup.set_pos(f64::from(button_x + button_w), PopupAlignment::Right);
|
||||
popup.show(button);
|
||||
});
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
94
src/modules/focused.rs
Normal file
94
src/modules/focused.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use crate::icon;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::node::get_open_windows;
|
||||
use crate::sway::WindowEvent;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Image, Label, Orientation};
|
||||
use ksway::{Client, IpcEvent};
|
||||
use serde::Deserialize;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct FocusedModule {
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_icon: bool,
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_title: bool,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
icon_theme: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for FocusedModule {
|
||||
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
|
||||
let icon_theme = IconTheme::new();
|
||||
|
||||
if let Some(theme) = self.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(&theme));
|
||||
}
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let icon = Image::builder().name("icon").build();
|
||||
let label = Label::builder().name("label").build();
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
|
||||
let mut sway = Client::connect().unwrap();
|
||||
|
||||
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
let focused = get_open_windows(&mut sway)
|
||||
.into_iter()
|
||||
.find(|node| node.focused);
|
||||
|
||||
if let Some(focused) = focused {
|
||||
tx.send(focused).unwrap();
|
||||
}
|
||||
|
||||
spawn_blocking(move || loop {
|
||||
while let Ok((_, payload)) = srx.try_recv() {
|
||||
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap();
|
||||
|
||||
let update = match payload.change.as_str() {
|
||||
"focus" => true,
|
||||
"title" => payload.container.focused,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if update {
|
||||
tx.send(payload.container).unwrap();
|
||||
}
|
||||
}
|
||||
sway.poll().unwrap();
|
||||
});
|
||||
|
||||
{
|
||||
rx.attach(None, move |node| {
|
||||
let value = node.name.as_deref().unwrap_or_else(|| node.get_id());
|
||||
|
||||
let pixbuf = icon::get_icon(&icon_theme, node.get_id(), self.icon_size);
|
||||
|
||||
if self.show_icon {
|
||||
icon.set_pixbuf(pixbuf.as_ref());
|
||||
}
|
||||
|
||||
if self.show_title {
|
||||
label.set_label(value);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::collection::Collection;
|
||||
use crate::modules::launcher::icon::{find_desktop_file, get_icon};
|
||||
use crate::modules::launcher::node::SwayNode;
|
||||
use crate::icon::{find_desktop_file, get_icon};
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::launcher::FocusEvent;
|
||||
use crate::popup::PopupAlignment;
|
||||
use crate::sway::SwayNode;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image};
|
||||
use std::process::{Command, Stdio};
|
||||
@@ -175,19 +174,8 @@ impl LauncherItem {
|
||||
button.connect_enter_notify_event(move |button, _| {
|
||||
let windows = windows.lock().unwrap();
|
||||
if windows.len() > 1 {
|
||||
let button_w = button.allocation().width();
|
||||
|
||||
let (button_x, _) = button
|
||||
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let button_center = f64::from(button_x) + f64::from(button_w) / 2.0;
|
||||
|
||||
popup.set_windows(windows.as_slice(), &tx_hover);
|
||||
popup.show();
|
||||
|
||||
// TODO: Pass through module location
|
||||
popup.set_pos(button_center, PopupAlignment::Center);
|
||||
popup.show(button);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
mod icon;
|
||||
mod item;
|
||||
mod node;
|
||||
mod popup;
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
|
||||
use crate::modules::launcher::node::{get_open_windows, SwayNode};
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::node::get_open_windows;
|
||||
use crate::sway::{SwayNode, WindowEvent};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Orientation};
|
||||
use ksway::{Client, IpcEvent};
|
||||
@@ -20,28 +19,14 @@ use tokio::task::spawn_blocking;
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LauncherModule {
|
||||
favorites: Option<Vec<String>>,
|
||||
#[serde(default = "default_false")]
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
show_names: bool,
|
||||
#[serde(default = "default_true")]
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_icons: bool,
|
||||
|
||||
icon_theme: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WindowEvent {
|
||||
change: String,
|
||||
container: SwayNode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FocusEvent {
|
||||
AppId(String),
|
||||
@@ -181,7 +166,6 @@ impl Launcher {
|
||||
} else {
|
||||
windows.get_mut(&window.id).unwrap().name = Some(name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +191,13 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
let mut sway = Client::connect().unwrap();
|
||||
|
||||
let popup = Popup::new("popup-launcher", info.app, Orientation::Vertical);
|
||||
let popup = Popup::new(
|
||||
"popup-launcher",
|
||||
info.app,
|
||||
info.monitor,
|
||||
Orientation::Vertical,
|
||||
info.bar_position,
|
||||
);
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
/// Clicking the widget opens a popup containing the current time
|
||||
/// with second-level precision and a calendar.
|
||||
pub mod clock;
|
||||
pub mod focused;
|
||||
pub mod launcher;
|
||||
pub mod mpd;
|
||||
pub mod script;
|
||||
@@ -12,9 +13,11 @@ pub mod sysinfo;
|
||||
pub mod tray;
|
||||
pub mod workspaces;
|
||||
|
||||
use crate::config::BarPosition;
|
||||
/// Shamelessly stolen from here:
|
||||
/// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs>
|
||||
use glib::IsA;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::{Application, Widget};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
@@ -29,6 +32,9 @@ pub enum ModuleLocation {
|
||||
pub struct ModuleInfo<'a> {
|
||||
pub app: &'a Application,
|
||||
pub location: ModuleLocation,
|
||||
pub bar_position: &'a BarPosition,
|
||||
pub monitor: &'a Monitor,
|
||||
pub output_name: &'a str,
|
||||
}
|
||||
|
||||
pub trait Module<W>
|
||||
|
||||
@@ -5,7 +5,6 @@ use self::popup::Popup;
|
||||
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
|
||||
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::popup::PopupAlignment;
|
||||
use dirs::home_dir;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
@@ -80,7 +79,7 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Open(f64),
|
||||
Open,
|
||||
Update(Box<Option<(Song, Status, String)>>),
|
||||
}
|
||||
|
||||
@@ -93,7 +92,13 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
||||
|
||||
let popup = Popup::new("popup-mpd", info.app, Orientation::Horizontal);
|
||||
let popup = Popup::new(
|
||||
"popup-mpd",
|
||||
info.app,
|
||||
info.monitor,
|
||||
Orientation::Horizontal,
|
||||
info.bar_position,
|
||||
);
|
||||
let mpd_popup = MpdPopup::new(popup, ui_tx);
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
@@ -101,16 +106,8 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
let music_dir = self.music_dir.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
let button_w = button.allocation().width();
|
||||
|
||||
let (button_x, _) = button
|
||||
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
click_tx
|
||||
.send(Event::Open(f64::from(button_x + button_w)))
|
||||
.unwrap();
|
||||
button.connect_clicked(move |_| {
|
||||
click_tx.send(Event::Open).unwrap();
|
||||
});
|
||||
|
||||
let host = self.host.clone();
|
||||
@@ -148,11 +145,12 @@ impl Module<Button> for MpdModule {
|
||||
match status.state {
|
||||
PlayState::Playing => client.command(commands::SetPause(true)).await,
|
||||
PlayState::Paused => client.command(commands::SetPause(false)).await,
|
||||
PlayState::Stopped => Ok(())
|
||||
PlayState::Stopped => Ok(()),
|
||||
}
|
||||
}
|
||||
PopupEvent::Next => client.command(commands::Next).await
|
||||
}.unwrap();
|
||||
PopupEvent::Next => client.command(commands::Next).await,
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,9 +159,8 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
Event::Open(pos) => {
|
||||
mpd_popup.popup.show();
|
||||
mpd_popup.popup.set_pos(pos, PopupAlignment::Right);
|
||||
Event::Open => {
|
||||
mpd_popup.popup.show(&button);
|
||||
}
|
||||
Event::Update(mut msg) => {
|
||||
if let Some((song, status, string)) = msg.take() {
|
||||
@@ -214,7 +211,7 @@ impl MpdModule {
|
||||
PlayState::Playing => self.icon_play.as_ref(),
|
||||
PlayState::Paused => self.icon_pause.as_ref(),
|
||||
};
|
||||
icon.map(|i| i.as_str())
|
||||
icon.map(String::as_str)
|
||||
}
|
||||
"title" => song.title(),
|
||||
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
|
||||
|
||||
@@ -20,7 +20,7 @@ const fn default_interval() -> u64 {
|
||||
|
||||
impl Module<Label> for ScriptModule {
|
||||
fn into_widget(self, _info: &ModuleInfo) -> Label {
|
||||
let label = Label::new(None);
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
spawn(async move {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::{Workspace, WorkspaceEvent};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Orientation};
|
||||
use ksway::client::Client;
|
||||
@@ -11,15 +12,10 @@ use tokio::task::spawn_blocking;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WorkspacesModule {
|
||||
pub(crate) name_map: Option<HashMap<String, String>>,
|
||||
}
|
||||
name_map: Option<HashMap<String, String>>,
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Workspace {
|
||||
name: String,
|
||||
focused: bool,
|
||||
// num: i32,
|
||||
// output: String,
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
@@ -45,22 +41,24 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct WorkspaceEvent {
|
||||
change: String,
|
||||
old: Option<Workspace>,
|
||||
current: Option<Workspace>,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for WorkspacesModule {
|
||||
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
|
||||
fn into_widget(self, info: &ModuleInfo) -> gtk::Box {
|
||||
let mut sway = Client::connect().unwrap();
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let workspaces = {
|
||||
let raw = sway.ipc(IpcCommand::GetWorkspaces).unwrap();
|
||||
serde_json::from_slice::<Vec<Workspace>>(&raw).unwrap()
|
||||
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw).unwrap();
|
||||
|
||||
if self.all_monitors {
|
||||
workspaces
|
||||
} else {
|
||||
workspaces
|
||||
.into_iter()
|
||||
.filter(|workspace| workspace.output == info.output_name)
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
let name_map = self.name_map.unwrap_or_default();
|
||||
@@ -88,29 +86,35 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
|
||||
{
|
||||
let menubar = container.clone();
|
||||
let output_name = info.output_name.to_string();
|
||||
rx.attach(None, move |event| {
|
||||
match event.change.as_str() {
|
||||
"focus" => {
|
||||
let old = event.old.unwrap();
|
||||
let old_button = button_map.get(&old.name).unwrap();
|
||||
old_button.style_context().remove_class("focused");
|
||||
if let Some(old_button) = button_map.get(&old.name) {
|
||||
old_button.style_context().remove_class("focused");
|
||||
}
|
||||
|
||||
let new = event.current.unwrap();
|
||||
let new_button = button_map.get(&new.name).unwrap();
|
||||
new_button.style_context().add_class("focused");
|
||||
if let Some(new_button) = button_map.get(&new.name) {
|
||||
new_button.style_context().add_class("focused");
|
||||
}
|
||||
}
|
||||
"init" => {
|
||||
let workspace = event.current.unwrap();
|
||||
let item = workspace.as_button(&name_map, &ui_tx);
|
||||
if self.all_monitors || workspace.output == output_name {
|
||||
let item = workspace.as_button(&name_map, &ui_tx);
|
||||
|
||||
item.show();
|
||||
menubar.add(&item);
|
||||
button_map.insert(workspace.name, item);
|
||||
item.show();
|
||||
menubar.add(&item);
|
||||
button_map.insert(workspace.name, item);
|
||||
}
|
||||
}
|
||||
"empty" => {
|
||||
let current = event.current.unwrap();
|
||||
let item = button_map.get(¤t.name).unwrap();
|
||||
menubar.remove(item);
|
||||
if let Some(item) = button_map.get(¤t.name) {
|
||||
menubar.remove(item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
95
src/popup.rs
95
src/popup.rs
@@ -1,32 +1,59 @@
|
||||
use crate::config::BarPosition;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, Orientation};
|
||||
use gtk::{Application, ApplicationWindow, Button, Orientation};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Popup {
|
||||
pub window: ApplicationWindow,
|
||||
pub container: gtk::Box,
|
||||
}
|
||||
|
||||
pub enum PopupAlignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
monitor: Monitor,
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
pub fn new(name: &str, app: &Application, orientation: Orientation) -> Self {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
app: &Application,
|
||||
monitor: &Monitor,
|
||||
orientation: Orientation,
|
||||
bar_position: &BarPosition,
|
||||
) -> Self {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
|
||||
gtk_layer_shell::init_for_window(&win);
|
||||
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
||||
|
||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Top, 0);
|
||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Bottom, 5);
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
if bar_position == &BarPosition::Top {
|
||||
5
|
||||
} else {
|
||||
0
|
||||
},
|
||||
);
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
if bar_position == &BarPosition::Bottom {
|
||||
5
|
||||
} else {
|
||||
0
|
||||
},
|
||||
);
|
||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Left, 0);
|
||||
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Right, 0);
|
||||
|
||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Top, false);
|
||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Bottom, true);
|
||||
gtk_layer_shell::set_anchor(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
bar_position == &BarPosition::Top,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
bar_position == &BarPosition::Bottom,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Left, true);
|
||||
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Right, false);
|
||||
|
||||
@@ -42,11 +69,11 @@ impl Popup {
|
||||
win.add(&content);
|
||||
|
||||
win.connect_leave_notify_event(|win, ev| {
|
||||
const THRESHOLD: f64 = 3.0;
|
||||
|
||||
let (w, _h) = win.size();
|
||||
let (x, y) = ev.position();
|
||||
|
||||
const THRESHOLD: f64 = 3.0;
|
||||
|
||||
// some child widgets trigger this event
|
||||
// so check we're actually outside the window
|
||||
if x < THRESHOLD || y < THRESHOLD || x > f64::from(w) - THRESHOLD {
|
||||
@@ -59,29 +86,41 @@ impl Popup {
|
||||
Self {
|
||||
window: win,
|
||||
container: content,
|
||||
monitor: monitor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the popover's X position relative to the left border of the screen
|
||||
pub fn set_pos(&self, pos: f64, alignment: PopupAlignment) {
|
||||
let width = self.window.allocated_width();
|
||||
|
||||
let offset = match alignment {
|
||||
PopupAlignment::Left => pos,
|
||||
PopupAlignment::Center => (pos - (f64::from(width) / 2.0)).round(),
|
||||
PopupAlignment::Right => pos - f64::from(width),
|
||||
};
|
||||
|
||||
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
|
||||
}
|
||||
|
||||
/// Shows the popover
|
||||
pub fn show(&self) {
|
||||
pub fn show(&self, button: &Button) {
|
||||
self.window.show_all();
|
||||
self.set_pos(button);
|
||||
}
|
||||
|
||||
/// Hides the popover
|
||||
pub fn hide(&self) {
|
||||
self.window.hide();
|
||||
}
|
||||
|
||||
/// Sets the popover's X position relative to the left border of the screen
|
||||
fn set_pos(&self, button: &Button) {
|
||||
let widget_width = button.allocation().width();
|
||||
let screen_width = self.monitor.workarea().width();
|
||||
let popup_width = self.window.allocated_width();
|
||||
|
||||
let (widget_x, _) = button
|
||||
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0;
|
||||
|
||||
let mut offset = (widget_center - (f64::from(popup_width) / 2.0)).round();
|
||||
|
||||
if offset < 5.0 {
|
||||
offset = 5.0;
|
||||
} else if offset > f64::from(screen_width - popup_width) - 5.0 {
|
||||
offset = f64::from(screen_width - popup_width) - 5.0;
|
||||
}
|
||||
|
||||
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
|
||||
}
|
||||
}
|
||||
|
||||
49
src/sway/mod.rs
Normal file
49
src/sway/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod node;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct WorkspaceEvent {
|
||||
pub change: String,
|
||||
pub old: Option<Workspace>,
|
||||
pub current: Option<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Workspace {
|
||||
pub name: String,
|
||||
pub focused: bool,
|
||||
// pub num: i32,
|
||||
pub output: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WindowEvent {
|
||||
pub change: String,
|
||||
pub container: SwayNode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SwayNode {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub app_id: Option<String>,
|
||||
pub focused: bool,
|
||||
pub urgent: bool,
|
||||
pub nodes: Vec<SwayNode>,
|
||||
pub floating_nodes: Vec<SwayNode>,
|
||||
pub shell: Option<String>,
|
||||
pub window_properties: Option<WindowProperties>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WindowProperties {
|
||||
pub class: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SwayOutput {
|
||||
pub name: String,
|
||||
}
|
||||
@@ -1,25 +1,5 @@
|
||||
use crate::sway::SwayNode;
|
||||
use ksway::{Client, IpcCommand};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SwayNode {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub app_id: Option<String>,
|
||||
pub focused: bool,
|
||||
pub urgent: bool,
|
||||
pub nodes: Vec<SwayNode>,
|
||||
pub floating_nodes: Vec<SwayNode>,
|
||||
pub shell: Option<String>,
|
||||
pub window_properties: Option<WindowProperties>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WindowProperties {
|
||||
pub class: String,
|
||||
}
|
||||
|
||||
impl SwayNode {
|
||||
pub fn get_id(&self) -> &str {
|
||||
Reference in New Issue
Block a user