elkowar / eww

ElKowars wacky widgets
https://elkowar.github.io/eww
MIT License
9.4k stars 381 forks source link

[FEATURE] `range_select` function #622

Closed ModProg closed 1 year ago

ModProg commented 1 year ago

Description of the requested feature

A function that can take a json array and select the first matching entry.

I could imagine two things, a generic one that would be more useful but would also definitly increase complexity by quite a lot, maybe too much (would require something closure like):

select("[1,2,3]","value > 1") ; would return 2

More narrow solution and still quite powerful, take an array of tuples with the first being the range and the second being the returned value: An example of a Wi-Fi strength visualization. It will select the first entry where the range contains wifi_strength.

ranged_select('[["-45..", "▂▄▆█"], ["-60..", "▂▄▆_"], ["-70..", "▂▄__"], ["-80..", "▂___"], ["..", "____"]]', wifi_strength)

Proposed configuration syntax

Add a new function to expressions range_select that takes an array and a value.

The value is a number. The array contains tuples, with the first entry being a Range expression and the second being an arbitrary value that will be returned (array, object, string, ...).

My proposed syntax for Range is Rusts: start_inclusive..end_exclusive and start_inclusive..=end_exclusive, with both ends being optional to create an open range, matching any value higher or lower than the specified limit, or any value if no limits are specified.

There are also other possible range syntaxes [start,end], [start,end_exclusive), [start, end_exclusive[ or start - end or we could even use <2, 4<= for open ranges, I'm not fixated on one, as long as open ranges and exclusive ranges are supported.

Additional context

It is currently possible to implement something like this, but it requires using a bunch of ? expressions, which make it very complex and hard to read.

An example of the current syntax: https://dharmx.is-a.dev/eww-powermenu/#battery

(defwidget _battery [battery status one two three
                    four five six seven charge]
  (box :class "bat-box" :space-evenly false :spacing 8
    (label :text {status == 'Charging' ? charge :
      battery < 15 ? seven :
        battery < 30 ? six :
          battery < 45 ? five :
            battery < 60 ? four :
              battery < 75 ? three :
                battery < 95 ? two : one})))
ModProg commented 1 year ago

I somewhat implemented this as a widget, but due to there not being local variables it is not very readable:

(defwidget select_ranged [map value]
  (box
    (for entry in map
        (literal :visible {
                           (matches(entry[0], "\\d+\\.\\.\\d+") && search(entry[0], "\\d+")[0] <= value && search(entry[0], "\\d+")[1] > value) ||
                           (matches(entry[0], "\\d+\\.\\.=\\d+") && search(entry[0], "\\d+")[0] <= value && search(entry[0], "\\d+")[1] >= value) ||
                           (matches(entry[0], "\\d+\\.\\.$") && search(entry[0], "\\d+")[0] <= value) ||
                           (matches(entry[0], "^\\.\\.\\d+") && search(entry[0], "\\d+")[1] > value) ||
                           (matches(entry[0], "^\\.\\.=\\d+") && search(entry[0], "\\d+")[1] >= value)}
                 :content {entry[1]}))))
ModProg commented 1 year ago

I somewhat implemented this as a widget, but due to there not being local variables it is not very readable:

(defwidget select_ranged [map value]
  (box
    (for entry in map
        (literal :visible {
                           (matches(entry[0], "\\d+\\.\\.\\d+") && search(entry[0], "\\d+")[0] <= value && search(entry[0], "\\d+")[1] > value) ||
                           (matches(entry[0], "\\d+\\.\\.=\\d+") && search(entry[0], "\\d+")[0] <= value && search(entry[0], "\\d+")[1] >= value) ||
                           (matches(entry[0], "\\d+\\.\\.$") && search(entry[0], "\\d+")[0] <= value) ||
                           (matches(entry[0], "^\\.\\.\\d+") && search(entry[0], "\\d+")[1] > value) ||
                           (matches(entry[0], "^\\.\\.=\\d+") && search(entry[0], "\\d+")[1] >= value)}
                 :content {entry[1]}))))

This implementation doesn't actually work, due to #624

ModProg commented 1 year ago

Actual working solution I'm using for now:

(defwidget select_ranged [map value ?template]
  (box
    (for entry in map
      (literal :visible {
                         (matches(entry[0], "-?\\d+\\.\\.-?\\d+") && (search(entry[0], "-?\\d+")[0] == "null" ? 0 : search(entry[0], "-?\\d+")[0]) <= value && (search(entry[0], "-?\\d+")[1] == "null" ? 0 : search(entry[0], "-?\\d+")[1]) > value) ||
                         (matches(entry[0], "-?\\d+\\.\\.=-?\\d+") && (search(entry[0], "-?\\d+")[0] == "null" ? 0 : search(entry[0], "-?\\d+")[0]) <= value && (search(entry[0], "-?\\d+")[1] == "null" ? 0 : search(entry[0], "-?\\d+")[1]) >= value) ||
                         (matches(entry[0], "-?\\d+\\.\\.$") && (search(entry[0], "-?\\d+")[0] == "null" ? 0 : search(entry[0], "-?\\d+")[0]) <= value) ||
                         (matches(entry[0], "^\\.\\.-?\\d+") && (search(entry[0], "-?\\d+")[0] == "null" ? 0 : search(entry[0], "-?\\d+")[0]) > value) ||
                         (matches(entry[0], "^\\.\\.=-?\\d+") && (search(entry[0], "-?\\d+")[0] == "null" ? 0 : search(entry[0], "-?\\d+")[0]) >= value)}
             :content {template == ""? entry[1] : replace(template, "\\{}", entry[1])}))))

The template is there as it is not a function but a widget and I therefor cannot wrap the returned value in another widget otherwise.

ModProg commented 1 year ago

I implemented a proof of concept here: https://github.com/elkowar/eww/pull/628

ModProg commented 1 year ago

This could be extended to support non-numeric ranges/values as well. I imagine a set of conditions that could work like so:

Alternative syntaxes

oldwomanjosiah commented 1 year ago

I like this idea! In the long run, i wonder if it would make sense to have a case {x} of {pattern} => {value} {pattern} {value} ... syntax.

ModProg commented 1 year ago

I like this idea! In the long run, i wonder if it would make sense to have a case {x} of {pattern} => {value} {pattern} {value} ... syntax.

Would be interesting to consider syntax options. I tried to do this without adding a new language feature, but having actual syntax for this would maybe also help for readability.

Rust-like match would also be an option

match value {
  == "value" => smth,
  /regex/ => smth,
  0..=10 => smth
}
elkowar commented 1 year ago

This kind of plays into a bigger problem I have with the current simplexpr syntax and language... I kinda of really want to have lambdas, but simultaneously feel like it's already grown wayyy further than I should have let it, and adding higher order functions would make it even worse. I get that this sort of feature would be useful, but,... I'll be very careful adding any sort of magic syntax like this, for now, in hopes of finding a good general solution in the future

ModProg commented 1 year ago

Yeah that's why I proposed the purely string based function that would not be adding a new syntax to the simpleexpr language.

elkowar commented 1 year ago

I now added a function jq that allows jq-style json processing, and should also support the string processing functionality there, through the slice operation .[2:10]