Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: shell-like combinators for conditional computation and sending several commands to server in a single batch #278

Open
nikitabobko opened this issue Jun 15, 2024 · 5 comments

Comments

@nikitabobko
Copy link
Owner

nikitabobko commented Jun 15, 2024

Currently it's impossible to consume stdout and exit codes of commands in toml config. One has to use exec-and-forget

alt-w = 'exec-and-forget aerospace workspace W || aerospace workspace-back-and-forth'
atl-tab = 'exec-and-forget aerospace list-workspaces --all | aerospace workspace next'

It's slow. Communicating with the server back and forth from CLI client can take additional 100ms which becomes noticeable. Even if we fix the slowness somehow, annoying flickering will still remain an issue

It'd be cool if AeroSpace supported basic shell combinators (||, &&, ;, ( ), !)

alt-w = 'workspace W || workspace-back-and-forth'
atl-tab = 'list-workspaces --all | workspace next'

That's a big feature that lays the foundation for a lot of things (basically the combinators allow programming custom logic):

  • It becomes possible to send a batch of commands from CLI to server at once, resulting in reduced flickering (because server already implements "double buffering" to reduce flickering)
    https://nikitabobko.github.io/AeroSpace/goodness#use-trackpad-gestures-to-switch-workspaces
    aerospace eval 'list-workspaces --monitor mouse --visible | xargs workspace; aerospace workspace next'
  • It fixes the ugliness of on-window-detected TOML callback. The new syntax is much more compact and powerful. It remains readable for people familiar with shell:
    on-window-detected = '''
        test %{app-bundle-id} == com.jetbrains.intellij && move-workspace-to-monitor I \
            || test %{app-bundle-id} == com.google.Chrome && move-node-to-workspace W \
            || test %{app-bundle-id} == com.apple.dt.Xcode && (move-node-to-workspace X; exec-async 'echo hi!')
    '''
  • It opens up a possibility for even more powerful conditional gaps [Feature Request] Large Monitor, Single App Window Size Concern #60:
    [gaps]
    outer.left = 'test %{workspace-tiling-windows-count} --lessThan 2 && echo 100'
    outer.right = 'test %{workspace-tiling-windows-count} --lessThan 2 && echo 100'
  • It makes everything more universal and reusable. E.g. there won't be need to fix Add command to programatically run all on-window-detected callbacks  #107 if config command allows to query on-window-detected command:
    aerospace eval 'config --get on-window-detected | eval -'

Other subcommands that AeroSpace has to implement to make the feature complete:

Overall a lot issues can be fixed: #264 #60 #54 #174 (partially) #104 (partially) #107 (kinda) #150

Open question: doesn't it open a pandora box of own script programming language? It kinda does. I hope that people will never ask for loops

nikitabobko added a commit that referenced this issue Jun 16, 2024
…instead of using JSON array

Reason: it better fits into a future with built-in shell-like combinators #278

Before:

    $ aerospace config --get mode.service.binding --json
    {
      "alt-shift-k" : [
        "join-with up",
        "mode main"
      ],
      "backspace" : [
        "close-all-windows-but-current",
        "mode main"
      ],
      ...
    }

After:

    $ aerospace config --get mode.service.binding --json
    {
      "alt-shift-h" : "join-with left; mode main",
      "esc" : "mode main; reload-config",
      "alt-shift-k" : "join-with up; mode main",
      "r" : "flatten-workspace-tree; mode main",
      "backspace" : "close-all-windows-but-current; mode main",
      "alt-shift-j" : "join-with down; mode main",
      "f" : "layout floating tiling; mode main",
      "alt-shift-l" : "join-with right; mode main"
    }

Raycast extension doesn't depend on this API yet, so it should be fine
to break it #215
nikitabobko added a commit that referenced this issue Jun 16, 2024
It's a more detailed name. %{app-id} wasn't yet released, so it's fine
to rename it. `--app-id` remains supported in `list-windows`

The biggest usage of "app-id" term is going to be renamed in
`on-window-detected` callback with the introduction of shell-like
combinators #278
@nikitabobko
Copy link
Owner Author

nikitabobko commented Aug 11, 2024

Draft:

on-window-detected = '''
if test %{app-bundle-id} == com.jetbrains.intellij.ce || test %{app-bundle-id} == com.jetbrains.intellij do
    move-workspace-to-monitor I # [I]de
elif test %{app-bundle-id} == com.google.Chrome do
    move-node-to-workspace W # [W]eb browser
elif test %{app-bundle-id} == com.apple.dt.Xcode do
    move-node-to-workspace X; exec-async 'echo hi!' # [X]code
end
'''

@nikitabobko
Copy link
Owner Author

nikitabobko commented Aug 11, 2024

Since it's a custom language, it'd be good to be able to print AST (Abstract Syntax Tree) for better discoverability and debugging

$ aerospace eval --print-ast-and-exit '
if test %{app-bundle-id} == com.jetbrains.intellij.ce do
    move-workspace-to-monitor I
end

echo 'hi!'
'

IfBlock
    IfCondition
        Command
            ['test', '%{app-bundle-id}', '==', 'com.jetbrains.intellij.ce']
    IfThenBlock
        Command
            ['move-workspace-to-monitor', 'I']
Command
    ['echo', 'hi!']

@jakenvac
Copy link
Contributor

jakenvac commented Aug 14, 2024

I think this sounds like it would be an incredible addition to the project.

Have you thought about using existing embeddable scripting languages such as lua? I think this would be great for several reasons: (I'll use lua as an example here, but there other options)

  • Users can leverage existing tooling, such as:
    • text editor syntax highlighting
    • language server clients (With type def comments, in the case of lua lsp)
    • package managers for plugins/extensibility
  • No need to maintain a custom language/interpreter/vm.
    • A good example of this is neovim with lua vs vim with vimscript. The vim project spends a significant amount of time implementing and maintaining vimscript, whereas neovim is able to iterate quickly and use a tried and tested scripting language with its own maintainers and development cycle.
  • No need to embed scripts as strings in a toml file
  • Things like callbacks can use an event handler pattern:
    aerospace.on('window-detected', function(window) print(window.app_bundle_id) end)

This of course would come with some cons, too:

  • You miss out on the fun of building your own language 😄
  • There doesn't seem to be any actively maintained swift bindings for lua specifically
  • Overhead. Using a general scripting language vs something lean and bespoke could increase build sizes and potentially impact performance. (Although luajit is fast)
  • It could be off putting to new users. Keeping a toml file that's easy to configure, with the option of extending by adding scripts as multiline strings is a good compromise to cater to newcomers and power users alike.

Edit: I've just found some incredibly well documented and up to date bindings for lua. Shocked it has so few stars. https://github.com/tomsci/LuaSwift

Edit 2: I don't know how I didn't think of this at first, but macos provides a great embeddable language in the form of JavaScriptCore. Part of apples webkit. It requires no third party dependencies and has a very easy to use swift/obj-c api.

Here's a very simple working code example, demonstrating just how easy it is to use.

import Foundation
import JavaScriptCore

let context = JSContext()

let addTwoNumbers: @convention(block) (Int, Int) -> Int = { num1, num2 in
    print("Adding two numbers: \(num1) + \(num2)")
    return num1 + num2
}

context?.setObject(addTwoNumbers, forKeyedSubscript: "addTwoNumbers" as NSString)

let jsScript = "addTwoNumbers(2, 3)"

if let result = context?.evaluateScript(jsScript) {
    print(result.toString() ?? "No result")
}

@tobiasgiese
Copy link
Sponsor

Have you thought about using existing embeddable scripting languages such as lua?

There is also a sketchybar lua plugin 🙂
https://github.com/FelixKratz/SbarLua

jakenvac pushed a commit to jakenvac/AeroSpace that referenced this issue Aug 16, 2024
…instead of using JSON array

Reason: it better fits into a future with built-in shell-like combinators nikitabobko#278

Before:

    $ aerospace config --get mode.service.binding --json
    {
      "alt-shift-k" : [
        "join-with up",
        "mode main"
      ],
      "backspace" : [
        "close-all-windows-but-current",
        "mode main"
      ],
      ...
    }

After:

    $ aerospace config --get mode.service.binding --json
    {
      "alt-shift-h" : "join-with left; mode main",
      "esc" : "mode main; reload-config",
      "alt-shift-k" : "join-with up; mode main",
      "r" : "flatten-workspace-tree; mode main",
      "backspace" : "close-all-windows-but-current; mode main",
      "alt-shift-j" : "join-with down; mode main",
      "f" : "layout floating tiling; mode main",
      "alt-shift-l" : "join-with right; mode main"
    }

Raycast extension doesn't depend on this API yet, so it should be fine
to break it nikitabobko#215
jakenvac pushed a commit to jakenvac/AeroSpace that referenced this issue Aug 16, 2024
It's a more detailed name. %{app-id} wasn't yet released, so it's fine
to rename it. `--app-id` remains supported in `list-windows`

The biggest usage of "app-id" term is going to be renamed in
`on-window-detected` callback with the introduction of shell-like
combinators nikitabobko#278
jakenvac pushed a commit to jakenvac/AeroSpace that referenced this issue Aug 16, 2024
@osolmaz
Copy link

osolmaz commented Sep 9, 2024

Just wanted up this

I currently use the following shortcuts, and the delay due to exec-and-forget feels significant

alt-p = 'exec-and-forget aerospace list-workspaces --monitor focused --empty no | aerospace workspace prev'
alt-n = 'exec-and-forget aerospace list-workspaces --monitor focused --empty no | aerospace workspace next'

As far as I've read here and in #264, this is supposed to help with that, no?

nikitabobko added a commit that referenced this issue Sep 21, 2024
… CmdResult

This commit lays out the foundation for the following issues
- #186
- #278
- #20

Also the commit changes how focused window is tracked throught the chain
of executed commands.

- CommandMutableState was a mutable state that tracked the focused
  window, and the track had to be updated throught the chain of executed
  commands (for example, take a look at the `focus` command)
- CmdEnv is simplier. It just forces a particular window to be percieved
  as "focused".

CmdEnv approach makes things easier to understand and describe in the
docs (which I'm going to do later, CmdEnv will be exposed as
AEROSPACE_FOCUSED_WORKSPACE and AEROSPACE_FOCUSED_WINDOW_ID environment
variables)

Unlike CommandMutableState, CmdEnv approach disallows to change focused
window in on-window-detected, on-focus-changed, and other callbacks.
Which I think is not an issue at all. It maybe even considered a safety
mechanism.

If a user uses `close` in one of the mentioned callbacks, previously, a
random window could become focused and it would receive all the
following commands. Now, all the commands that go after `close` will
fail with "Invalid <window-id> \(windowId) specified in
AEROSPACE_FOCUSED_WINDOW_ID env variable"

- This commit is not a breaking change for on-window-detected thanks to
  limitations in #20
- But this commit is technically a breaking change for other mentioned
  callbacks, since no limitations were impoosed on those callbacks. But
  I don't believe that anyone in sane mind relied on it. And the docs
  are explicit that changing focus in those callbacks is a bad idea:

  > Changing the focus within these callbacks is a bad idea anyway, and the way it’s handled will probably change in future versions.

Currently, the "force focused state" in CmdEnv is immutable, and I hope
it will stay so. But hypothetically, it can be mutable in future
versions if we decide that the embedded language #278 should allow
chaning environment variables.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants