Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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 Request: Embedded script language #560

Closed
Henkru opened this issue Oct 4, 2024 · 4 comments
Closed

Feature Request: Embedded script language #560

Henkru opened this issue Oct 4, 2024 · 4 comments

Comments

@Henkru
Copy link
Contributor

Henkru commented Oct 4, 2024

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:

  • Lua: A battle-tested language with a lightweight interpreter and a robust ecosystem. It's used in many applications, like Neovim.
  • JavaScriptCore: Built directly into macOS's SDK, with no extra dependencies. It supports JIT compilation (though this requires specific permissions) and is fully sandboxed. However, it lacks built-in functionality to import other JavaScript files. (e.g. you need to implement all your config in one JS file, or some custom require functionality must be added)

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

What features should the API support?

Some of my ideas:

  • Setting and modifying config values.
  • Dynamic config values (functions that return values on access).
  • Callbacks to execute functions.
  • Querying windows, monitors, workspaces, etc.
  • Custom layouts.
  • Custom formatters for the tray mode text.

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:

  • Setting common config options.
  • Setting/removing key bindings.
  • Executing Aero commands (as strings).
  • Adding Aero callbacks (Lua functions or Aero command strings).
  • Dynamic config values for gaps.
  • Some functions for querying the current state (e.g., window count).

The currently implemented API functions can be found:

Areas for improvement:

  • Returning structured data (as Lua tables) from Aero commands. Currently, all commands return strings.

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

@nikitabobko
Copy link
Owner

Thanks both to you and @jakenvac (#278 (comment)) 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
Copy link
Contributor Author

Henkru commented Oct 8, 2024

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
Copy link
Contributor

jakenvac commented Oct 9, 2024

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
Copy link

neevparikh commented Oct 9, 2024

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!

Repository owner locked and limited conversation to collaborators Oct 27, 2024
@nikitabobko nikitabobko converted this issue into discussion #622 Oct 27, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants