28 Commits

Author SHA1 Message Date
Jake Stanger
789d54e2ea ci: add deploy job 2022-08-15 21:23:54 +01:00
Jake Stanger
be726ddde8 ci(build): always use latest toolchain 2022-08-15 21:23:47 +01:00
Jake Stanger
96ff974b82 ci(build): fix clippy, add fmt check 2022-08-15 21:16:18 +01:00
Jake Stanger
226bca632e chore(release): v0.3.0 2022-08-15 21:12:15 +01:00
Jake Stanger
446720c578 chore(intellij): config update 2022-08-15 21:12:04 +01:00
Jake Stanger
7625635050 refactor: fix a couple of clippy warnings 2022-08-15 21:11:17 +01:00
Jake Stanger
8576ac5c44 fix: popup placement issues 2022-08-15 21:11:00 +01:00
Jake Stanger
8518262053 chore(intellij): add run config 2022-08-15 21:05:13 +01:00
Jake Stanger
a29df37e77 docs: include example files
These are also present in the wiki
2022-08-15 17:06:55 +01:00
Jake Stanger
2d755d37e5 feat: support for multiple bars per monitor, loading config from env var
Restructures `monitor` config key into object of monitor names -> configs.
2022-08-15 17:06:42 +01:00
Jake Stanger
001dd5473a chore(intellij): load config from environment variable 2022-08-15 17:05:53 +01:00
Jake Stanger
120580994c docs: write better configuration/styling guides 2022-08-15 17:05:13 +01:00
Jake Stanger
18e088d593 ci: add libgtk-layer-shell-dev 2022-08-15 00:15:36 +01:00
Jake Stanger
2c6338f82f ci: add missing sudo call to apt 2022-08-15 00:11:35 +01:00
Jake Stanger
882fa30b66 ci: install libgtk3 for build 2022-08-15 00:09:47 +01:00
Jake Stanger
3c564a7774 ci: add build job 2022-08-15 00:04:06 +01:00
Jake Stanger
73f0e7e48e docs(readme): tidy 2022-08-15 00:03:04 +01:00
Jake Stanger
5e72a7ba32 docs(readme): add install links, aur package 2022-08-15 00:01:38 +01:00
Jake Stanger
de80b99e64 refactor: use macro for module config matching 2022-08-14 23:32:20 +01:00
Jake Stanger
ebf7a97a85 chore(release): v0.2.0 2022-08-14 20:42:42 +01:00
Jake Stanger
7a515359ac chore(release): v0.2.0 2022-08-14 20:42:25 +01:00
Jake Stanger
2e0f033bed feat: config option for bar height 2022-08-14 20:41:38 +01:00
Jake Stanger
dc14cb003f feat: new focused window module 2022-08-14 20:40:11 +01:00
Jake Stanger
e416e03b0a chore(intellij): update run configs 2022-08-14 20:38:57 +01:00
Jake Stanger
65c5d391d9 fix(launcher): some icons displaying too large 2022-08-14 19:45:10 +01:00
Jake Stanger
53adaa846c feat(workspaces): support for toggling showing workspaces for all monitors 2022-08-14 16:23:41 +01:00
Jake Stanger
a358037d3e feat: add support for showing bar at top of screen 2022-08-14 15:56:21 +01:00
Jake Stanger
19d009fe5b feat(script): enable markup support 2022-08-14 15:55:20 +01:00
29 changed files with 813 additions and 282 deletions

37
.github/workflows/build.yml vendored Normal file
View 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
View 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
View File

@@ -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>

View File

@@ -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" />

View 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>

View File

@@ -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" />

View File

@@ -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="" />

View 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
View File

@@ -1033,7 +1033,7 @@ dependencies = [
[[package]]
name = "ironbar"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"chrono",
"cornfig",

View File

@@ -1,6 +1,6 @@
[package]
name = "ironbar"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
license = "MIT"
description = "Customisable wlroots/sway bar"

View File

@@ -8,74 +8,39 @@ For information and examples on styling please see the [wiki](https://github.com
![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png)
## 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
View 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
View 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
View 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;
}

View File

@@ -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(&center));
content.pack_end(&right, false, false, 0);
load_modules(&left, &center, &right, app, config);
load_modules(&left, &center, &right, app, config, monitor, monitor_name);
win.add(&content);
win.connect_destroy_event(|_, _| {
@@ -48,78 +48,70 @@ fn load_modules(
right: &gtk::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: &gtk::Box, modules: Vec<ModuleConfig>, info: ModuleInfo) {
fn add_modules(content: &gtk::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);
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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
View 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
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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)),

View File

@@ -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 {

View File

@@ -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(&current.name).unwrap();
menubar.remove(item);
if let Some(item) = button_map.get(&current.name) {
menubar.remove(item);
}
}
_ => {}
}

View File

@@ -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
View 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,
}

View File

@@ -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 {