greshake / i3status-rust

Very resourcefriendly and feature-rich replacement for i3status, written in pure Rust
GNU General Public License v3.0
2.88k stars 475 forks source link

Add option to configure in rust by turning blocks & bar into a library crate #1328

Closed LordMZTE closed 1 year ago

LordMZTE commented 3 years ago

I'm currently looking to switch from a homemade bar to i3status-rust, but I think that I'd prefer configuring i3status-rs in rust, as opposed to toml.

This would (assuming we add a good API) also make it easy to implement custom blocks. The way I'm seeing this work, is to create a lib.rs file in i3status-rs, and have all the modules that wouldn't be used only in the current, toml-based version be declared in lib.rs, in order to have them be available in the library crate. This crate could then also be pushed to crates.io.

The user would then create a binary crate, and add the i3status-rust library to Cargo.toml. The main function could look something like this:

use i3status_rust::{blocks::{ClockBlock, BatteryBlock}, Bar};
fn main() {
    let mut bar = Bar::new();

    bar.add_block(ClockBlock::new());
    bar.add_block(BatteryBlock::new());

    bar.run();
}

Of course this is just a rough idea of how the API could look.

There seems to be a project that already works like this called i3monkit, but it seems to be unmaintained, and doesn't have nearly as many blocks as i3status-rust, and also doesn't seem to support styling as fancy as i3status-rust does or callbacks.

I would love to contribute to i3status-rust, but I've currently not used it or looked at the code, so it might take me some time to get used to it. 😅

GladOSkar commented 3 years ago

Related: #756 (But very different, this actually sounds pretty feasible and i'm not opposed)

ammgws commented 3 years ago

I don't really understand what you are gaining by doing configuration in source or for creating a custom binary as in your example. If you're going that far you might as well just keep a personal fork and do whatever you want there?

LordMZTE commented 3 years ago

I don't really understand what you are gaining by doing configuration in source or for creating a custom binary as in your example. If you're going that far you might as well just keep a personal fork and do whatever you want there?

I think this approach would make it more convenient. In a personal fork, you'd have to rip out the whole config part of i3status-rust first. With a library crate, this would be made much easier and we could easily integrate it, as the binary crate could just call the same API as a cutom binary would, but based on the config file.

Here's some reasons I think someone might prefer configuring in rust:

MaxVerevkin commented 3 years ago

I imagine it might be useful for some custom blocks that are hard and/or not efficient to implement as a script and which cannot be merged upstream for some reason. Maintaining a fork is not that easy.

LordMZTE commented 3 years ago

Good point! Maintaining a fork just for a personal config would be a lot of work for keeping up to date with upstream. You might have to deal with merge conflicts, whereas in the case of a library crate it would just be a matter of adjusting the version in Cargo.toml, and maybe accounting for API changes.

MaxVerevkin commented 3 years ago

As an experiment, I'll try splitting swaystatus (#430) into two crates - one with all the types, bloks and logic, and the second which will parse the config and run the bar. Will see how it'll go.

LordMZTE commented 3 years ago

I can start working on this if that's alright!

MaxVerevkin commented 3 years ago

I can start working on this if that's alright!

Sure, I'll let you know when I get to the point when at least one block can run, so you can port other blocks if you will.

LordMZTE commented 3 years ago

I meant on i3status-rust, not swaystatus :D

MaxVerevkin commented 3 years ago

Anyways, you are always free to try this on your fork xD

LordMZTE commented 3 years ago

It looks like the best way to go about this would be to split up the codebase into 2 crates. We could for example call them libi3status-rust and i3status-rust. This would be necessary to only have the dependencies on toml and clap on the binary crate, as (to my knowledge) it is not possible to have a dependency only for the binary part of a bin+lib crate.

Also, it would allow a seperate examples directory for both, and make it more obvious what actually belongs to the binary and what does to the library.

If we're gonna do this, I'd have to know how to go about restructuring the project. It would require moving some files around.

LordMZTE commented 3 years ago

Phew, looks like implementing this requires some crazy amounts of refactoring to split into 2 crates :P

jfpedroza commented 3 years ago

I think another benefit is being able to enable or disable blocks based on certain conditions, or change the formatting of a block. An example is having a desktop and a laptop and showing the battery block on the laptop only without having two config files. Another is showing swap info only if the setup has swap.

GladOSkar commented 3 years ago

@johnf9896 this is already possible for the battery block using the allow_missing and hide_missing options. Not so for swap.

But how do you intend to use this feature for that usecase? Isn't maintaining two separate source configs just as difficult/annoying as maintaining two config files?

jfpedroza commented 3 years ago

this is already possible for the battery block using the allow_missing and hide_missing options.

Yeah, I found out about them after writing the comment. They weren't available back when I added i3status-rust.

But how do you intend to use this feature for that usecase? Isn't maintaining two separate source configs just as difficult/annoying as maintaining two config files?

I wouldn't keep two source configs. I would have one with an if to check if there is swap and set clickable = true/false

I am already doing that for the battery, using scriptfs + Ruby ERB

My i3status.toml.erb contains

<% if system("ls /sys/class/power_supply/*/capacity >/dev/null 2>&1") %>
[[block]]
block = "battery"
driver = "upower"
device = "DisplayDevice"
format = "{percentage}%"
<% end %>

The point is controlling the config using arbitrary conditions, not just for showing or hiding blocks as #1326 would allow. I am already solving that by using scriptfs + Ruby to generate the config file but having it in Rust would allow the other benefits stated above.

GladOSkar commented 2 years ago

That's a neat solution, I admit, but IMO usecases like these should be covered within the block's logic (like battery does now) rather than in source config. People shouldn't have to recompile to enable such behavior, esp. if they may be installing from a binary package manager

jfpedroza commented 2 years ago

Yes, I agree. The more that can be done through a regular config file the better. I would leave the source config for cases that are very particular because you can't integrate every single one into the program

MaxVerevkin commented 2 years ago

How about using lua for configuration (nvim way)?

LordMZTE commented 2 years ago

That's a cool idea, but I'd still prefer source configs, as I suggested. That would offer even more flexibility than lua would, without any performance penalty.

MaxVerevkin commented 2 years ago

In case it's used only for configuration, the startup might take a little longer, however, looking at neovim, this shouldn't be noticeable. Performance shouldn't be an issue even if lua will be used for custom blocks, at least it should be much better compared to external scripts.

And to be honest, I don't think rust fits well into the "source as a config" category. It takes more than two minutes to recompile this project on my machine after a small change of the defaults (Intel i5-5200U, rustc 1.58.0-nightly, release mode). How many times should I start a config-less version to get those two minutes back :laughing: ?

And in addition, wouldn't it be nice to give almost the power of source config to binary package users?

MaxVerevkin commented 2 years ago

Still, both approaches can coexist.

MaxVerevkin commented 1 year ago

This is now possible:

```rust use std::sync::Arc; use i3status_rs::config::{BlockConfigEntry, CommonBlockConfig, Config}; use i3status_rs::errors::*; use i3status_rs::escape::CollectEscaped; use i3status_rs::icons::Icons; use i3status_rs::themes::color::Color; use i3status_rs::themes::separator::Separator; use i3status_rs::themes::{ColorOrLink, Theme, ThemeOverrides, ThemeUserConfig}; use i3status_rs::widget::{State, Widget}; use i3status_rs::*; #[tokio::main(flavor = "current_thread")] async fn main() { if let Err(error) = try_main().await { let error_widget = Widget::new() .with_text(error.to_string().chars().collect_pango_escaped()) .with_state(State::Critical); println!( "{},", serde_json::to_string(&error_widget.get_data(&Default::default(), 0).unwrap()).unwrap() ); eprintln!("\n\n{error}\n\n"); dbg!(error); std::future::pending::<()>().await; } } async fn try_main() -> Result<()> { env_logger::init(); protocol::init(false); let icons = Icons::from_file("material-nf")?; let theme = Theme::try_from(ThemeUserConfig { theme: Some("slick".into()), overrides: Some(ThemeOverrides { separator: Some(Separator::Native), separator_bg: Some(ColorOrLink::Color(Color::None)), separator_fg: Some(ColorOrLink::Color(Color::None)), ..Default::default() }), })?; let mut bar = BarState::new(Config { shared: config::SharedConfig { theme: Arc::new(theme), icons: Arc::new(icons), ..Default::default() }, ..Default::default() }); bar.spawn_block(BlockConfigEntry { config: blocks::BlockConfig::time(blocks::time::Config { format: Default::default(), interval: 10.into(), timezone: None, }), common: CommonBlockConfig { ..Default::default() }, }) .await?; bar.spawn_block(BlockConfigEntry { config: blocks::BlockConfig::github(blocks::github::Config { interval: 600.into(), hide_if_total_is_zero: true, ..Default::default() }), common: CommonBlockConfig { ..Default::default() }, }) .await?; bar.run_event_loop(|| unimplemented!()).await } ```

If there is interest in making the API more convenient, it can be discussed in new issues.