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

config: window-inherit-working-directory options #1694

Closed

Conversation

jparise
Copy link
Collaborator

@jparise jparise commented Apr 17, 2024

window-inherit-working-directory was previously a boolean value. This change makes it a more granular option, allowing you to enable this behavior for one or more surface "kinds": split, tab, window.

The full behavior is only implemented for the embedded runtime and macOS app. The other runtimes still need their own plumbing to track surface kind creation (stubbed as .window by default in this change).

Resolves #1392

Copy link
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far so good. Am I missing where the kind is actually set from the apprts?

@jparise
Copy link
Collaborator Author

jparise commented Apr 18, 2024

So far so good. Am I missing where the kind is actually set from the apprts?

For the embedded runtime, I'm setting it from newSurfaceOptions. For the others, I added lines like this pending a real implementation:

const kind: CoreSurface.Kind = .window; // TODO
var config = try apprt.surface.newConfig(app.app, &app.config, kind);

@jparise jparise force-pushed the window-inherit-working-directory branch from 00bd212 to 4e088ac Compare April 18, 2024 04:54
@jparise jparise requested a review from mitchellh April 26, 2024 14:44
@jparise jparise force-pushed the window-inherit-working-directory branch from 4e088ac to 22403b5 Compare May 5, 2024 21:48
@jparise
Copy link
Collaborator Author

jparise commented May 6, 2024

A few notes on the configuration parsing:

Parsing Bools

To maintain some backwards compatibility with the previous boolean config value, I added logic to the parseCLI implementation. But this could also be generalized for all packed struct parsing with something like this:

diff --git i/src/cli/args.zig w/src/cli/args.zig
index 49c5152a..acf9c839 100644
--- i/src/cli/args.zig
+++ w/src/cli/args.zig
@@ -281,6 +281,8 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T {
     const info = @typeInfo(T).Struct;
     assert(info.layout == .@"packed");
 
+    const parseBoolFn: ?std.builtin.Type.Fn = if (@hasDecl(T, "parseBool")) @typeInfo(@TypeOf(T.parseBool)).Fn else null;
+
     var result: T = .{};
 
     // We split each value by ","
@@ -306,6 +308,20 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T {
             }
         }
 
+        if (parseBoolFn) |pb| {
+            const b = parseBool(part) catch |err| switch (err) {
+                error.InvalidValue => continue :loop,
+                else => return err,
+            };
+
+            switch (pb.params.len) {
+                2 => try result.parseBool(b),
+                else => @compileError("parseBool invalid argument count"),
+            }
+
+            continue :loop;
+        }
+
         // No field matched
         return error.InvalidValue;
     }

This felt like the most flexible approach, but there are two other related options we could take:

  1. Always support boolean values in packed struct config options, where "true" means "flip all boolean fields to on" and "false" means the opposite.
  2. Opt in to (1) based on the presence of some declaration in the target struct rather than it being automatic for all packed structs.

Default Field Values

The current parsePackedStruct parses and sets fields on top of the struct's default field values. In this case, we want to default all of the window-inherit-working-directory fields to true, but that means that seeing a configuration value like split will only visit that one field (leaving the other fields set to true), which doesn't seem like the expected behavior given an explicit configuration value. Instead, the user would need to spell out split,no-tab,no-window (or just no-tab,no-window) if they only want the split behavior.

This is why I have this line in my custom parseCLI function:

self.* = .{ .split = false, .tab = false, .window = false };
var iter = std.mem.splitSequence(u8, value, ",");
// ...

I can't decide if this is a shortcoming of the currently parsePackedStruct implementation or intentional behavior because it sort of depends on how we've viewed the default field values and the no-... value support (i.e. the user knows the full defaults and sets a configuration value string to specifically alter the ones they want to change).

If the former, it wouldn't be hard to fix -- reset all fields to "off" before parsing the new, explicit values -- but I thought I'd bring it up in context here first.


With some blend of the above, configuration types like this new WindowInheritWorkingDirectory would be a lot easier to implement.

@jparise jparise force-pushed the window-inherit-working-directory branch from 22403b5 to fa421d1 Compare May 8, 2024 16:55
@jparise jparise force-pushed the window-inherit-working-directory branch from fa421d1 to 18722d0 Compare May 23, 2024 01:15
@jparise jparise force-pushed the window-inherit-working-directory branch 2 times, most recently from fbabc96 to 9e85018 Compare May 30, 2024 14:33
@jparise
Copy link
Collaborator Author

jparise commented May 30, 2024

In the interest of moving this forward, I simplified this to use the standard packed struct config behavior. This means there is no backward compatibility for true / false boolean values, and because all of the field values default to true, people will primarily use the configuration to turn things off (e.g. no-tab,no-window).

@jparise jparise force-pushed the window-inherit-working-directory branch 3 times, most recently from d38ff6d to b6215ee Compare June 1, 2024 23:07
@mitchellh
Copy link
Contributor

Thanks @jparise. My main delay in merging this was that plus one more thing: I don't super love how information is flowing through this. It may be correct, though, but I want to spend some time thinking about it and just haven't yet... annoyingly, so sorry.

@jparise
Copy link
Collaborator Author

jparise commented Jun 6, 2024

Thanks @jparise. My main delay in merging this was that plus one more thing: I don't super love how information is flowing through this. It may be correct, though, but I want to spend some time thinking about it and just haven't yet... annoyingly, so sorry.

No worries! Happy to take your feedback (once you find time for it), or if you'd prefer to restructure things yourself, that sounds great, too.

@jparise jparise force-pushed the window-inherit-working-directory branch 2 times, most recently from 9b68c6b to 7d60959 Compare June 6, 2024 23:06
@jparise
Copy link
Collaborator Author

jparise commented Jun 7, 2024

Thanks @jparise. My main delay in merging this was that plus one more thing: I don't super love how information is flowing through this. It may be correct, though, but I want to spend some time thinking about it and just haven't yet... annoyingly, so sorry.

I tried out another approach locally that moves the window-inherit-working-directory logic out of newConfig and into (for the embedded apprt) newSurfaceOptions, placing it alongside the window-inherit-font-size logic.

const font_size: f32 = font_size: { // ...

const working_directory: [*:0]const u8 = working_directory: { // ...

return .{
    .font_size = font_size,
    .working_directory = working_directory,
};
  1. It removes CoreSurface.Kind. The apprt can just use whatever signally mechanism it wants between callbacks like newSplit and its surface creation routines.
  2. At least in the embedded apprt world, it makes use of the existing working_directory surface creation option.
  3. newConfig can go away because it's now empty. apprts could just call config.shallowClone themselves.

... but the downside is that it makes each apprt responsible for the window-inherit-working-directory config + pwd logic. (Maybe I could hoist some of it into a common function in a apprt/surface.zig they could all call.)

Does this feel like a nicer approach to pursue?

@jparise
Copy link
Collaborator Author

jparise commented Jul 16, 2024

I'm back to playing with this approach:

I tried out another approach locally that moves the window-inherit-working-directory logic out of newConfig and into (for the embedded apprt) newSurfaceOptions, placing it alongside the window-inherit-font-size logic.

const font_size: f32 = font_size: { // ...

const working_directory: [*:0]const u8 = working_directory: { // ...

return .{
    .font_size = font_size,
    .working_directory = working_directory,
};

I generally prefer it, but I'm not sure "who" should own the working_directory allocation, which comes from CoreSurface.pwd (copied from the IO thread). Surface.Options is an extern struct, so I'm looking for a nice pattern for the Surface.Options result (from newSurfaceOptions) to reference that [*:0]const u8 copy (without owning it), allowing the e.g. Swift runtime to safely overwrite that field with a new reference.

All of these objects have different lifetimes, and I don't think I can add an allocator to Surface.Options itself (because extern struct), so I'm not sure if that's a sign that this overall approach is flawed.

Any advice, @mitchellh?

@jparise jparise force-pushed the window-inherit-working-directory branch from a8d2d8b to 5a9dfe6 Compare July 16, 2024 16:06
@jparise
Copy link
Collaborator Author

jparise commented Jul 19, 2024

All of these objects have different lifetimes, and I don't think I can add an allocator to Surface.Options itself (because extern struct), so I'm not sure if that's a sign that this overall approach is flawed.

I suppose I could try stuffing the ArenaAllocator into Surface.Options as an *anyopaque pointer, but that seems... unsafe and gross.

@mitchellh
Copy link
Contributor

Sorry for neglecting this PR. I'm still on the fence about the approach. I go back and forth on it. With the recent tmux control mode issue, I've been considering making the core more aware of the window/tab/split hierarchy. That would make doing something like this a lot simpler I think (I mean, this PR is already only +/- like 80 lines, but I'm not sure about all the data ownership).

I've also considered making the window vs tab vs split config related to the "conditional config" we've talked about in the issue for system theme changes. For example, I could imagine hypothetically letting you do window-inherit-working-directory[split] = true or something (hypothetical syntax, none of it is real). And that would be the same syntax as say theme[system-theme=dark] = whatever. So if we implemented that system, it's possible you get this feature "for free" so to speak.

That's my current line of thinking... anyways... sorry for the delay.

@jparise
Copy link
Collaborator Author

jparise commented Jul 25, 2024

Sorry for neglecting this PR. I'm still on the fence about the approach. I go back and forth on it. With the recent tmux control mode issue, I've been considering making the core more aware of the window/tab/split hierarchy. That would make doing something like this a lot simpler I think (I mean, this PR is already only +/- like 80 lines, but I'm not sure about all the data ownership).

That sounds good, although the trickier bits here have been more about configuration data flow and ownership rather than tracking the surface types. Any architectural improvements here would be easy to build upon, though!

I've also considered making the window vs tab vs split config related to the "conditional config" we've talked about in the issue for system theme changes. For example, I could imagine hypothetically letting you do window-inherit-working-directory[split] = true or something (hypothetical syntax, none of it is real). And that would be the same syntax as say theme[system-theme=dark] = whatever. So if we implemented that system, it's possible you get this feature "for free" so to speak.

Is the applicable idea that we'd have a general surface (or kind) dimension that could be applied to all (or a distinct subset of) configuration fields (e.g. window-inherit-working-directory[surface=split] = true, window-inherit-font-size[surface=split] = true, etc.)?

`window-inherit-working-directory` was previously a boolean value. This
change makes it a more granular option, allowing you to enable this
behavior for one or more surface "kinds": split, tab, window.

The configuration value can be `true` (all), `false` (none), or a
comma-delimited string of kinds (e.g. `tab` or `split,window`).

The full behavior is only implemented for the embedded runtime and macOS
app. The other runtimes still need their own plumbing to track surface
kind creation (stubbed as `.window` by default in this change).
jparise added 3 commits July 25, 2024 09:49
This setting previously defaulted to `true`, so flip all of our packed
booleans on by default.

Also, note that the granular values are only supported on macOS.
This introduces an ArenaAllocator that models the lifetime of the
Surface.Options structure from initial surface creation command to the
actual surface creation. This implementation is a little rough and
relies on an `anyopaque` pointer (because `extern struct`), but it
demonstrates a more apprt-centric approach.

Still only implemented for embedded apprt and macOS. The other apprts
will need their own plumbing to support this approach.

We can also chose to clean up newConfig() and call Config.shallowClone()
directly after this change.
@jparise jparise force-pushed the window-inherit-working-directory branch from 5a9dfe6 to 2fb8434 Compare July 25, 2024 13:54
@jparise
Copy link
Collaborator Author

jparise commented Jul 25, 2024

All of these objects have different lifetimes, and I don't think I can add an allocator to Surface.Options itself (because extern struct), so I'm not sure if that's a sign that this overall approach is flawed.

I suppose I could try stuffing the ArenaAllocator into Surface.Options as an *anyopaque pointer, but that seems... unsafe and gross.

I pushed an initial (rough) implementation of this approach for reference. Understood that it's probably not the approach we ultimately want to take with this.

@@ -1499,6 +1541,7 @@ pub const CAPI = struct {
app: *App,
opts: *const apprt.Surface.Options,
) !*Surface {
defer @constCast(opts).deinit();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I acknowledge this is gross for multiple reasons.

@jparise
Copy link
Collaborator Author

jparise commented Nov 20, 2024

I've also considered making the window vs tab vs split config related to the "conditional config" we've talked about in the issue for system theme changes. For example, I could imagine hypothetically letting you do window-inherit-working-directory[split] = true or something (hypothetical syntax, none of it is real). And that would be the same syntax as say theme[system-theme=dark] = whatever. So if we implemented that system, it's possible you get this feature "for free" so to speak.

Nothing that the work started in #2735 might provide what we need to unblock this aspect of this change.

@Snikimonkd
Copy link

Snikimonkd commented Dec 17, 2024

maybe it's better to make this as a parameter (enum) for new_split and new_tab with values like cwd and home for example
so then we can create keybinds like this:

keybind = super+d=new_split:home

in this approach surface does not need to know anything about splits or tabs

@mitchellh
Copy link
Contributor

Chatted with @jparise on Discord awhile back about next steps for this PR and I think we're on the same page so I'm going to close this for now.

The config bike shed isn't too hard, the hard part is actually the plumbing of awareness of what a thing is. Ghostty core isn't aware of what is a tab split window etc. That's all in the apprts. So plumbing that through in a thoughtful way is the "hard" part (not technically hard, but hard to design well).

@mitchellh mitchellh closed this Dec 17, 2024
@jparise
Copy link
Collaborator Author

jparise commented Dec 17, 2024

Ghostty core isn't aware of what is a tab split window etc. That's all in the apprts. So plumbing that through in a thoughtful way is the "hard" part (not technically hard, but hard to design well).

That's exactly right. I've tried a few versions of it but haven't landed on something that I liked enough to submit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow different window-inherit-working-directory behavior for tabs and windows
3 participants