elkowar / eww

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

[FEATURE] Functions #430

Open oldwomanjosiah opened 2 years ago

oldwomanjosiah commented 2 years ago

Description of the requested feature

As your configuration grows, expressions for referencing state data become rather unweildy. This is especially the case when multiple widgets require similar (but still different) configuration. Some of this can be handled using custom wrapper widgets, but some cannot. I would thus propose a new type of widget: function definitions.

Definition

A function would be defined similarly to the current deflisten and defpoll. Within the function an implicit variable children would be defined, which would contain the unnamed arguments to a function in the order they were passed.

Purity

Functions would be assumed to be pure for optimization purposes. This means that functions are only guaranteed to run in the case that at least one of their arguments or captures has changed since the last invocation (at that position in the tree), with the specific semantics outside of that being implementation-defined. Shell calls (using `...`) are not able to make a function re-run.

My assumption is that this would use the same mechanism as the current widget variable references, but I haven't looked at how that is implemented specifically.

Captures

A function may "capture" variables from the enclosing scope (usually the file) by referencing them within the body of the function. It is guaranteed to re-run in the case that any of its captures change.

Proposed configuration syntax

; Functions can be recursive
(deffunc joinAll [separator]
  ; body is a simplexpr, the name children is automatically bound to the list of
  ; unnamed arguments following the last named argument.
  children.len == 0
    ? ""
    : (children.len == 1
      ? children[0]
      : ((children[0]) + separator + (joinAll :separator separator (children[1..])))))

(deffunc workspaceData [for] active_workspaces["${for}"])

; Here we define the function, using the same syntax as a widget definition
(deffunc workspaceClasses [workspace monitor]
  ; The let in syntax from #408
  let
    data = (workspaceData :for workspace)
  in
    (joinAll
      :separator " "
      ; Unnamed argument list
      "indicator"
      data.data == monitor ? "focused" : ""
      data.dis >= 0 ? "visible" : ""
      data.ex ? "exists" : ""))

; The function implemented in simplexpr
:class "indicator ${active_workspaces["${num}"]["dis"] == monitor ? "focused" : "" } ${active_workspaces["${num}"].dis < 0 ? "" : "visible" } ${active_workspaces["${num}"].ex ? "exists" : ""}"

; Replacing that with the defined function
:class (workspaceClasses
         :workspace num
         :monitor monitor)

Additional context

No response

elkowar commented 2 years ago

Hey! I'll need to think about this a good bit to know if I wanna consider it, but it is a very well written out feature proposal, so thanks for that! Very cool!

However, would it not make more sense to make functions just work as functions in simplexpr, rather than as some sort of pseudo-widget?

oldwomanjosiah commented 2 years ago

I am definitely open to further conversations, I'm sure that we wouldn't want to complicate yuck too much as it is supposed to be simple for ease of configuration.

Thinking on it more, it could also be done using the existing var syntax and fn values.

(defvar joinAll
  ; I'm pretty sure that vars' defs are treated as simpleexpr automatically,
  ; but if not you would have to wrap this in {}
  let
    inner = fn sep list sofar =>
      if list.length == 0 then sofar
      else inner(sep, list[1..], sofar + sep + list[0])
  in
    fn sep list => inner(sep, list, ""))

(defvar workspaceData
  fn for => active_workspaces["${for}"])

(defvar workspaceClass
  fn workspace monitor =>
    let
      data = workspaceData(workspace)
    in
      joinAll(
        " ",
        data.dis == monitor ? "focused" : "",
        data.dis >= 0 ? "visible" : "",
        data.ex ? "exists" : ""))

:class {workspaceClass(num, monitor)}

The major downside that I see with this is that it makes optional parameters more difficult (unless you also wanted function parameter pattern matching :wink:), but this is not necessarily a bad thing. It is also possible that this would complicate the implementation of reference capturing and memoization (I have ideas on this but id rather get your feedback on this first). On the other hand, being able to have local functions could come in handy in some situations.