nikitabobko / AeroSpace

AeroSpace is an i3-like tiling window manager for macOS
https://nikitabobko.github.io/AeroSpace/guide
MIT License
6.35k stars 103 forks source link

Feature Request: Embedded script language #560

Open Henkru opened 6 days ago

Henkru commented 6 days ago

I'd like to discuss embedding a scripting language into AeroSpace to enable more flexible customization and configuration. Currently, AeroSpace supports limited "dynamic" behavior, such as the on-window-detected callback with a matcher. However, there's a growing need for more advanced conditional behavior (see #278). Instead of turning TOML into a pseudo-programming language, would it make sense to support scripting directly?

Does this feature align with the project's values?

If the answer is yes, then I'd like to discuss the following questions:

Which scripting language would be the best fit?

Two options come to mind:

There is also a Lua 5.1 JIT implementation, though there are known performance issues on aarch64: https://github.com/LuaJIT/LuaJIT/issues/285

What features should the API support?

Some of my ideas:

My current probe/research

I have the same issue as #60, and I wanted more generic solution, so I have been probing/researching how well Lua fits into AeroSpace's current architecture, and I had promising results. Some of my testing can be found in my probe-lua branch. Lua integrates smoothly with the current config and callback system. For example, I implemented dynamic gaps for my ultrawide monitor:

local gaps = {
    -- [window count] = gap value
    [1] = 1280, -- 5120 / 4
    [2] = 640, -- 5120 / 8
    enabled = true,
}
local default_gap = 5

local dynamic_gap = function(_id, monitor)
    -- if monitor.name == "Dell U4919DW" then
    if monitor.width > 5000 and gaps.enabled then
        local window_count = aero.api.workspace_windows_count(monitor.activeWorkspace)
        return gaps[window_count] or default_gap
    end
    return default_gap
end

-- Set static gap for top and bottom
aero.api.gap_set(GAP_OUTER_TOP, 5)
aero.api.gap_set(GAP_OUTER_BOTTOM, 2)
-- Set dynamic gap for left and right
aero.api.gap_set(GAP_OUTER_LEFT, dynamic_gap)
aero.api.gap_set(GAP_OUTER_RIGHT, dynamic_gap)

-- Toggle the dynamic gap on/off, and then go back to the main mode
aero.keymap.set("service", "g", function()
    gaps.enabled = not gaps.enabled
end, "mode main")

The branch implements a lightweight Lua 5.1 wrapper for Swift (chosen for easy switch to LuaJIT) and abstractions for adding new API functions with type-checked parameters. However, it has evolved organically (based on what I currently needed to implement feature X), so the wrapper needs some refactoring/remodeling; for example, the error handling is pretty much via the error function that kills the app.

Supported features:

The currently implemented API functions can be found:

Areas for improvement:

For a more comprehensive example, I fully implemented my current configuration in Lua to showcase what can be done.

nikitabobko commented 2 days ago

Thanks both to you and @jakenvac (https://github.com/nikitabobko/AeroSpace/issues/278#issuecomment-2288794813) for taking in-depth look at the problem. It's good to see people doing such investments. Very much appreciated! I didn't even know about the existance of JavaScriptCore in macOS SDK.

However, right now, I don't plan to introduce any embedded scripting programming language.

AeroSpace is not a framework like Hammerspoon. I want AeroSpace to be designed as "ready to go" solution. AeroSpace is optimized for specific workflows and adds just enough of extensibility. If users want something beyond, well - sorry. As a supportive argument I want to mention Helix vs Neovim approaches. I'm very much for Helix approach of all batteries included and sensible defaults.

If users want to do scripting, they should do it outside of the AeroSpace configuration in whatever users' favorite programming language. All AeroSpace query commands should support --json flag to make it easier to use commands from programming languages.

About my suggestion in #278. I don't want it to be a programming language. Initially, I thought to only include &&, |, || and that's it. on-window-detected would look like:

on-window-detected = '''
    test %{app-bundle-id} == org.jetbrains.intellij && move-node-to-workspace I
        || test %{app-bundle-id} == com.google.chrome && move-node-to-workspace W
'''

but then I realized the common gotcha https://www.shellcheck.net/wiki/SC2015, and it, unfortunatelly, made me think about the if syntax:

on-window-detected = '''
    if test %{app-bundle-id} == org.jetbrains.intellij do 
        move-node-to-workspace I
    elif test %{app-bundle-id} == com.google.chrome do 
        move-node-to-workspace W
    end
'''

Regarding #60, I think the current go to solution is max-width suggested in the comments. I now discard my "conditional gaps" suggestion in favor of max-width.

I'd very happy if we managed to simplify on-window-detected rather than complicating it with programming languages (no matter if it's an in-house language or general purpose language like Lua). I think that assigning windows to workspaces is inherently conditional, that's why it's hard to avoid viewing it like if-statements.

Henkru commented 1 day ago

Thank you for sharing your perspective on this, and I completely understand where you're coming from. The vision of AeroSpace as a "ready-to-go" solution, optimized for specific workflows with sensible defaults, definitely has its strengths. My only concern is the potential latency introduced when triggering external programs (which may then query properties, such as windows count, etc.), but without the actual measurements, I wouldn't worry too much.

on-window-detected definitely has its conditional nature. I like the current approach, where you have bunch of rules in list, and the (first) matched rule(s) will be applied, it's not just one big if-else statement. In addition looping over those matchers is pretty fast.

jakenvac commented 23 hours ago

Latency is my main issue when shelling out for custom behaviors (I integrate my window manager with my terminal mux), but after reading @nikitabobko's justifications for wanting a batteries included solution I think it makes sense to not implement a scripting language.

I think there are other ways we can alleviate the latency, such as the async/threaded AX requests discussed in #497. Combine this with improved JSON and Callback support, I think shell scripting will augment Aerospace nicely.

On top of this, if it were possible to set config options via the cli, as discussed in #355 then you could essentially define your config as a shell script if you so desired.

neevparikh commented 18 hours ago

One of the things that I was thinking about and would like to add to this discussion is that targeting developers is a core project value:

AeroSpace is targeted at advanced users and developers I think, as a developer, the most important thing I would want is the ability to customize my tools to make them work the way I want to fit my workflow.

In some sense, it is a philosophical difference because I would always chose neovim over helix for this reason, so perhaps we just disagree on what to do. In that case, I would, of course, defer to your vision of what aerospace would be.

Handling scripting outside of the configuration would be a neat approach and honestly, the improvements discussed about reducing the latency would be sufficient for this.

Right now, to properly handle visible workspaces being highlighted in sketchybar for example, I need to call an aerospace list-workspaces command on the workspace change callback. This noticeably introduces latency in the sketchybar focus icon switching (all else being equal, it's not exactly instant even without this call). If this didn't have substantial latency, this would be no issue at all!