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

Switch to kdl #1759

Merged
merged 60 commits into from
Oct 5, 2022
Merged

Switch to kdl #1759

merged 60 commits into from
Oct 5, 2022

Conversation

imsnif
Copy link
Member

@imsnif imsnif commented Sep 30, 2022

Introduction

This PR introduces two main features:

  1. Moving all of our configuration / layouts / themes from YAML to KDL (including some new and improved features for each)
  2. A CLI interface that allows users fine control over the current or other Zellij session through the command line.

Following is the initial documentation for all of these features (once this is merged, it will replace the relevant documentation on our website).

kdl.vim

To help us vim people edit KDL, I created: kdl.vim. I'm mentioning this here because I think it will also help trying out this change.

Conversion from old YAML files

As a matter of convenience, when Zellij is run with an old configuration / layout / theme file (either explicitly with a cli flag or if it found the file in the default locations) it will prompt the user and convert that file to the new format.

This can also be done manually:

zellij convert-config /path/to/my/config.yaml > /path/to/my/config.kdl
zellij convert-layout /path/to/my/layout.yaml > /path/to/my/layout.kdl
zellij convert-theme /path/to/my/theme.yaml > /path/to/my/theme.kdl

Layouts

Layout documentation This configuration change revamps and greatly simplifies the way we write layouts for Zellij.

The layout structure is nested under a global layout node. Within it are pane, tab, pane_template and tab_template nodes that describe the layout.

A basic example layout can look like this:

layout {
    pane
    pane split_direction="vertical" {
        pane
        pane command="htop"
    }
}

Which would create the following layout:

htop-layout

Panes

pane nodes are the basic building blocks of a layout.

They could represent standalone panes:

layout {
    pane // panes can be bare
    pane command="diskonaut" // panes can have arguments on the same line
    pane {
        // panes can have arguments inside child-braces
        command "htop"
        cwd "/"
    }
    pane command="htop" { // or a mixture of same-line and child-braces arguments
        cwd "/"
    }
}

They could also represent logical containers:

layout {
    pane split_direction="vertical" {
        pane
        pane
    }
}

Note: if panes represent logical containers, all their arguments should be specified on their title line.

split_direction

split_direction is a pane argument that indicates whether its children will be laid out vertically or horizontally.
Possible values: "vertical" | "horizontal"
Default value if omitted: "horizontal"

eg.

layout {
    pane split_direction="vertical" {
        pane
        pane
    }
    pane {
        // value omitted, will be layed out horizontally
        pane
        pane
    }
}

Note: The layout node itself has a set value of "horizontal". It can be changed by adding a logical pane container:

layout {
    pane split_direction="vertical" {
        pane
        pane
    }
}

size

size is a pane argument that represents the fixed or percentage space taken up by this pane inside its logical container.

Possible values: bare integers representing fixed values (eg. 1) | quoted percentages (eg. "50%")

Note: specifying fixed values that are not unselectable plugins is currently unstable. Please see: #1758

eg.

layout {
    pane size=5
    pane split_direction="vertical" {
        pane size="80%"
        pane size="20%"
    }
    pane size=4
}

borderless

borderless is a pane argument indicating whether a pane should have a frame or not.

Possible values: true | false
Default value if omitted: false

eg.

layout {
    pane borderless=true
    pane {
        borderless true
    }
}

focus

focus is a pane argument indicating whether a pane should have focus on startup.

Possible values: true | false
Default value if omitted: false

Note: specifying multiple panes with focus will result in the first one of them being focused.

eg.

layout {
    pane focus=true
    pane {
        focus true
    }
}

name

name is a string pane argument to change the default pane title.

Possible values: "a quoted string"

eg.

layout {
    pane name="my awesome pane"
    pane {
        name "my amazing pane"
    }
}

cwd

A pane can have a cwd argument, pointing to its Current Working Directory.

Possible values: "/path/to/some/folder", "relative/path/to/some/folder"

eg.

layout {
    pane cwd="/"
    pane {
        command "/usr/bin/btm"
        cwd "/path/to/some/folder"
    }
}

command

command is a string (path) to an executable that should be run in this pane instead of the default shell.

Possible values: "/path/to/some/executable" | "executable" (the latter should be accessible through PATH)

eg.

layout {
    pane command="htop"
    pane {
        command "/usr/bin/btm"
    }
}
args

A pane with a command can also have an args argument. This argument can include one or more strings that will be passed to the command as its arguments.

Possible values: "a" "series" "of" "quoted" "strings"

Note: args must be inside the pane's child-braces and cannot be specified on the same line as the pane.

eg.

layout {
    pane command="tail" {
        args "-f" "/path/to/my/logfile"
    }

    // Hint: include "quoted" shell arguments as a single argument:
    pane command="bash" {
        args "-c" "tail -f /path/to/my/logfile"
    }

}

edit

edit is a string (path) to a file that will be opened using the editor specified in the EDITOR or VISUAL environment variables. This can alternatively also be specified using the scrollback_editor config variable.

Possible values: "/path/to/some/file" | "./relative/path/from/cwd"

eg.

layout {
    pane split_direction="vertical" {
        pane edit="./git_diff_side_a"
        pane edit="./git_diff_side_b"
    }
}

plugin

plugin is a pane argument the points to a Zellij plugin to load. Currently is is only possible to specify inside the child-braces of a pane followed by a URL location in quoted string.

Possible values: zellij:internal-plugin | file:/path/to/my/plugin.wasm

eg.

layout {
    pane {
        plugin location="zellij:status-bar"
    }
}

Tabs

tab nodes can optionally be used to start a layout with several tabs.

Note: all tab arguments should be specified on its title line. The child-braces are reserved for its child panes.

eg.

layout {
    tab // a tab with a single pane
    tab {
        // a tab with three horizontal panes
        pane
        pane
        pane
    }
    tab name="my third tab" split_direction="vertical" {
        // a tab with a name and two vertical panes
        pane
        pane
    }
}

split_direction

Tabs can have a split_direction just like panes. This argument indicates whether the tab's children will be laid out vertically or horizontally.

Possible values: "vertical" | "horizontal"
Default value if omitted: "horizontal"

eg.

layout {
    tab split_direction="vertical" {
        pane
        pane
    }
    tab {
        // if omitted, will be "horizontal" by default
        pane
        pane
    }
}

focus

Tabs can have a focus just like panes. This argument indicates whether a tab should have focus on startup.

Possible values: true | false
Default value if omitted: false

Note: only one tab can be focused.

eg.

layout {
    tab {
        pane
        pane
    }
    tab focus=true {
        pane
        pane
    }
}

name

Tabs can have a name just like panes. This argument is a string to change the default tab title.

Possible values: "a quoted string"

eg.

layout {
    tab name="my awesome tab"
    tab name="my amazing tab" {
        pane
    }
}

cwd

Tabs can have a cwd just like panes - pointing to their Current Working Directory.
All panes in this tab will have this cwd prefixed to their own cwd (if they have one) or start in this cwd if they don't.

Possible values: "/path/to/some/folder", "relative/path/to/some/folder"

eg.

layout {
    tab cwd="/tmp"
    tab name="my amazing tab" {
        pane // will have its cwd set to "/tmp"
        pane cwd="./foo" // will have its cwd set to "/tmp/foo"
        pane cwd="/home/foo" // will have its cwd set to "/home/foo", overriding the tab cwd with its absolute path
    }
}

Templates

Templates can be used avoid repetition when creating layouts.
Each template has a name that should be used directly as a node name instead of "pane" or "tab".

Pane Templates

Pane templates can be used to shorten pane attributes:

layout {
    pane_template name="htop" {
        command "htop"
    }
    pane_template name="htop-tree" {
        command "htop"
        args "--tree"
        borderless true
    }
    // the below will create a template with four panes
    // the top and bottom panes running htop and the two
    // middle panes running "htop --tree" without a pane frame
    htop
    htop-tree
    htop-tree
    htop
}

Pane templates with the command attribute can take the args and cwd of their consumers:

layout {
    pane_template name="follow-log" command="tail"
    follow-log {
        args "-f" "/tmp/my-first-log"
    }
    follow-log {
        args "-f" "my-second-log"
        cwd "/tmp"
    }
}

Note: the above only works for direct consumers and not other templates.

Pane templates can be used as logical containers. In this case a special children node must be specified to indicate where the child panes should be inserted.

Note: the children node can be nested inside panes but not inside other pane_templates.

layout {
    pane_template name="vertical-sandwich" split_direction="vertical" {
        pane
        children
        pane
    }
    vertical-sandwich {
        pane command="htop"
    }
}

Pane templates can include other pane templates.

layout {
    pane_template name="vertical-sandwich" split_direction="vertical" {
        pane
        children
        pane
    }
    pane_template name="vertical-htop-sandwich" {
        vertical-sandwich {
            pane command="htop"
        }
    }
    pane_template name="vertical-htop-sandwich-below" split_direction="horizontal" {
        children
        vertical-htop-sandwich
    }
    vertical-htop-sandwich
    vertical-htop-sandwich-below {
        pane command="diskonaut"
    }
}

The children node should be thought of as a placeholder for the pane using this template.

This:

layout {
    pane_template name="my_template" {
        pane
        children
        pane
    }
    my_template split_direction="vertical" {
        pane
        pane
    }
}

Will be translated into this:

layout {
    pane {
        pane
        pane split_direction="vertical" {
            pane
            pane
        }
        pane
    }
}

Tab Templates

Tab templates, similar to pane templates, help avoiding repetition when defining tabs. Like pane_templates they can include a children block to indicate where their child panes should be inserted.

Note: for the sake of clarity, arguments passed to tab_templates can only be specified on their title line.

layout {
    tab_template name="ranger-on-the-side" {
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        pane split_direction="vertical" {
            pane command="ranger" size="20%"
            children
        }
    }
    ranger-on-the-side name="my first tab" split_direction="horizontal" {
        pane
        pane
    }
    ranger-on-the-side name="my second tab" split_direction="vertical" {
        pane
        pane
    }
}
Default Tab Template

There is a special default_tab_template node that can be used just like a regular tab_template node, but that would apply to all tabs in the template as well as all new tabs opened in the session.

Note: the default_tab_template will not apply to tabs using other tab_templates.

Another note: if no tabs are specified, the whole layout is treated as a default_tab_template.

layout {
    default_tab_template {
        // the default zellij tab-bar and status bar plugins
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
    tab // the default_tab_template
    tab name="second tab" // the default_tab_template with a custom tab name
    tab split_direction="vertical" { // the default_tab_template with three vertical panes between the plugins
        pane
        pane
        pane
    }
}

Global cwd

The cwd property can also be specified globally in the layout node itself. Doing this would make all panes in this layout start in this cwd unless they have an absolute path. If they have a relative path, it will be appended to the global cwd. This should allow greater flexibility when creating custom layouts.

Eg.

layout {
    cwd "/home/aram/code/my-project"
    pane cwd="src" // will be opened in /home/aram/code/my-project/src
    pane cwd="/tmp" // absolute paths override the global cwd, this will be opened in /tmp
    pane command="cargo test" // will be started in /home/aram/code/my-project
}

Config

Config documentation

Keybindings

Keybindings can be configured in the keybinds block.

keybinds {
    // keybinds are divided into modes
    normal {
        // bind instructions can include one or more keys (both keys will be bound separately)
        // bind keys can include one or more actions (all actions will be performed with no sequential guarantees)
        bind "Ctrl g" { SwitchToMode "locked"; }
        bind "Ctrl p" { SwitchToMode "pane"; }
        bind "Alt n" { NewPane; }
        bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; }
    }
    pane {
        bind "h" "Left" { MoveFocus "Left"; }
        bind "l" "Right" { MoveFocus "Right"; }
        bind "j" "Down" { MoveFocus "Down"; }
        bind "k" "Up" { MoveFocus "Up"; }
        bind "p" { SwitchFocus; }
    }
    locked {
        bind "Ctrl g" { SwitchToMode "normal"; }
    }
}

Keys

Keys are defined in a single quoted string, with space delimiting modifier keys.

bind "a" // bind the individual character a
bind "Ctrl a" // bind a with the ctrl modifier
bind "Alt a" // bind a with the alt modifier
bind "F8" // bind the F8 key
bind "Left" // bind the left arrow key
  • Possible keys with the Ctrl modifier:
  • characters (eg. a)
  • Possible keys with the Alt modifier:
  • characters (eg. a)
  • Left | Right | Up | Down
  • Possible keys without a modifier
  • characters (eg. a)
  • Backspace
  • Left
  • Right
  • Up
  • Down
  • Home
  • End
  • PageUp
  • PageDown
  • Tab
  • Delete
  • Insert
  • Space
  • Enter
  • Esc

Overriding

When configured, keybindings override the default keybinds of the application individually (if a certain key was bound in the configuration, it overrides that key in the default configuration).
It's possible to explicitly unbind a key:

keybinds {
    unbind "Ctrl g" // unbind in all modes
    normal {
        unbind "Alt h" "Alt n" // unbind one or more keys in a specific mode
    }
}

It's also possible to use the special clear-defaults=true attribute either globally or in a specific mode:

keybinds clear-defaults=true { // will clear all default keybinds
    normal {
        // ...
    }
}
keybinds {
    normal clear-defaults=true { // will clear all keybinds in normal mode
        // ...
    }
}

Shared

There are three special node types that can be used when defining keybindings:

keybinds {
    shared {
        // these keybindings will be present in all modes
        bind "Ctrl g" { SwitchToMode "locked"; }
    }
    shared_except "resize" "locked" {
        // these keybindings will be present in all modes except "resize" and "locked"
        bind "Ctrl g" { SwitchToMode "locked"; }
    }
    shared_among "resize" "locked" {
        // these keybindings will be present in the "resize" and "locked" modes
        bind "Ctrl g" { SwitchToMode "locked"; }
    }
}

CLI actions

CLI actions documentation Zellij has a CLI interface that can be used to send actions to the current session.
zellij action <action_name> <optional_arguments>

It's also possible to send actions to another accessible session:

zellij --session <other-session-name> action <action_name> <optional_arguments>

Possible actions

Note: only the basic actions are specified here, most actions have additional capabilities. See their --help for more info, eg. zellij action new-pane --help

  • close-pane: Close the focused pane
  • close-tab: Close the current tab
  • dump-screen: Dumps the pane scrollback to a file
  • edit: Open the specified file in a new zellij pane with your default EDITOR
  • edit-scrollback: Open the pane scrollback in your default editor
  • focus-next-pane: Change focus to the next pane
  • focus-previous-pane: Change focus to the previous pane
  • go-to-next-tab: Go to the next tab
  • go-to-previous-tab: Go to the previous tab
  • go-to-tab: Go to tab with index [index]
  • half-page-scroll-down: Scroll down half page in focus pane
  • half-page-scroll-up: Scroll up half page in focus pane
  • help: Print this message or the help of the given subcommand(s)
  • move-focus: Move the focused pane in the specified direction. [right|left|up|down]
  • move-focus-or-tab: Move focus to the pane or tab (if on screen edge) in the specified direction [right|left|up|down]
  • move-pane: Change the location of the focused pane in the specified direction [right|left|up|down]
  • new-pane: Open a new pane in the specified direction [right|left|up|down] If no direction is specified, will try to use the biggest available space
  • new-tab: Create a new tab, optionally with a specified tab layout and name
  • page-scroll-down: Scroll down one page in focus pane
  • page-scroll-up: Scroll up one page in focus pane
  • quit: Quit Zellij
  • rename-pane: Renames the focused pane
  • rename-tab: Renames the focused pane
  • resize: Resize the focused pane in the specified direction. [right|left|up|down|+|-]
  • scroll-down: Scroll down in focus pane
  • scroll-to-bottom: Scroll down to bottom in focus pane
  • scroll-up: Scroll up in the focused pane
  • search-input: Search for String
  • search-next: Focus on next search occurrence
  • search-previous: Focus on previous search occurrence
  • switch-mode: Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session]
  • toggle-active-sync-tab: Toggle between sending text commands to all panes on the current tab and normal mode
  • toggle-floating-panes: Toggle the visibility of all fdirectionloating panes in the current Tab, open one if none exist
  • toggle-fullscreen: Toggle between fullscreen focus pane and normal layout
  • toggle-pane-embed-or-floating: Embed focused pane if floating or float focused pane if embedded
  • toggle-pane-frames: Toggle frames around panes in the UI
  • undo-rename-pane: Remove a previously set pane name
  • undo-rename-tab: Remove a previously set tab name
  • write: Write bytes to the terminal
  • write-chars: Write characters to the terminal

Zellij run

It's possible to run a specific command inside a new Zellij pane from the CLI.

zellij run -- htop // will open a new pane with "htop" and choose where to place it
zellij run -f -- htop // will open a new floating pane with htop

It's also possible to send the command to another accessible session:

zellij --session <other-session-name> run -- htop

For more information, see zellij command --help

In Zellij's shell integrations, this is aliased to zp (zellij pane) for tiled panes and zpf (zellij pane floating) for floating panes.
eg. zp tail -f /tmp/my-log.log (TODO: link to shell integrations section)

Zellij edit

It's possible to open an editor pane (eg. vim) from the command line.

Note: The editor must be set in the EDITOR or VISUAL environment variables, or alternatively in the scrollback_editor configuration parameter.

zellij edit /tmp/foo // will open the default editor to the "/tmp/foo" file
zellij edit /tmp/foo -f // will do the same in a floating pane
zellij edit /tmp/foo --line-number 10 // will open the default editor to "/tmp/foo" pointed to line 10

It's also possible to open an editor in another accessible session:

zellij --session <other-session-name> edit "/tmp/foo"

For more information, see zellij edit --help

In Zellij's shell integrations, this is aliased to zo (zellij open) for tiled panes and zof (zellij open floating) for floating panes.
eg. zof src/main.rs (TODO: link to shell integrations section)

@imsnif
Copy link
Member Author

imsnif commented Oct 3, 2022

Hey @har7an, thanks for the great and detailed comments!
You had some great finds with the actions hanging, the args mixing and the missing errors on unknown properties in layouts. I fixed them all and pushed the changes.

The quotation marks issue is a bit trickier to fix since this is a KDL de-serialization error which we catch and reword. I attempted to remedy the situation a little by changing it to this:

  × Failed to parse Zellij configuration
    ╭─[/tmp/dumped-layout.kdl:13:1]
 13 │         // name "none"
 14 │         args="-n1" "ls"; "-l"  
    ·         ──┬─
    ·           ╰── Failed to deserialize KDL node. 
Possible reasons:
- Missing `;` after a node name, eg. { node; another_node; }
- Missing quotations (") around an argument node eg. { first_node "argument_node"; }
- Missing an equal sign (=) between node arguments on a title line. eg. argument="value"
- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument="value" }
 15 │     }
    ╰────
  help: For more information, please see our configuration guide: https://zellij.dev/documentation/
        configuration.html

It might not be ideal, but I think along with a link to the documentation it's not overly bad.
The off-by-one error is (I'm pretty sure) an upstream issue, but I think we can leave it for now.

Otherwise - any more comments about the config/layout/cli experience? I think I'm mainly curious about the layout experience because that would be the most extreme change.

@imsnif imsnif temporarily deployed to cachix October 3, 2022 21:42 Inactive
@imsnif imsnif temporarily deployed to cachix October 4, 2022 07:37 Inactive
@har7an
Copy link
Contributor

har7an commented Oct 4, 2022

It might not be ideal, but I think along with a link to the documentation it's not overly bad.

Agreed, the link to the docs should help. I'm just not very happy with the formatting, because it's a lot of text squeezed into a very tight space imo. What would you say about something like this:

  × Failed to parse Zellij configuration
    ╭─[/tmp/dumped-layout.kdl:13:1]
 13 │         // name "none"
 14 │         args="-n1" "ls"; "-l"  
    ·         ──┬─
    ·           ╰── Failed to deserialize KDL node. 

       Possible reasons:
       - Missing `;` after a node name, eg. { node; another_node; }
       - Missing quotations (") around an argument node eg. { first_node "argument_node"; }
       - Missing an equal sign (=) between node arguments on a title line. eg. argument="value"
       - Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument="value" }

 15 │     }
    ╰────
  help: For more information, please see our configuration guide: https://zellij.dev/documentation/
        configuration.html

The off-by-one error is (I'm pretty sure) an upstream issue, but I think we can leave it for now.

It's not too bad, really, since most users will probably look for the underlined text, or the numbers that are printed to the left of it (which, curiously, is correct in any case).

Otherwise - any more comments about the config/layout/cli experience? I think I'm mainly curious about the layout experience because that would be the most extreme change.

I'll try and poke around some more until this evening, I have great plans for a custom shell script. :)

@har7an
Copy link
Contributor

har7an commented Oct 4, 2022

I notice new-pane is missing the option to name the pane at creation. Also left and up are still ignored e.g. when using zellij action create-pane. And it seems to ignore the --cwd argument as well?

Also some actions seem to get lost in oblivion when spamming actions too quickly. As an example, I wanted to write myself a little shell script that creates a custom layout with a split pane like so:

 14     devtab)
 13         if [[ $# -eq 2 ]]; then
 12             if [[ -d "$2" ]]; then
 11                 pushd "$2"
 10             fi
  9         fi
  8         $ZELLIJ action new-tab --name "devel"
  7         sleep 0.1
  6         $ZELLIJ action rename-pane shell
  5         sleep 0.1
  4         $ZELLIJ action new-pane --direction right --command "$EDITOR ."
  3         sleep 0.1
  2         $ZELLIJ action rename-pane editor
  1         sleep 0.1
60          $ZELLIJ action move-pane left
  1         ;;

But the results are rather mixed. Note that I added the sleep statements to get it to work reliably in the first place. Without these, sometimes it errors out telling me that no session exists, other times it creates only parts of the layout: It doesn't rename a pane, or it doesn't open a second pane, etc.

With the sleep statements it works, however. But that's not exactly ideal I think.

Also zellij action new-tab doesn't respect the $PWD of the pane it's called from. Is that on purpose?

@imsnif
Copy link
Member Author

imsnif commented Oct 4, 2022

I notice new-pane is missing the option to name the pane at creation.

Fair point, I think I'll add this after merging.

Also left and up are still ignored e.g. when using zellij action create-pane.

I forgot to mention this. This is an existing bug that I think happened because of a miscommunication between maintainers a while ago. Fixing it is out of scope here, IMO

And it seems to ignore the --cwd argument as well?

This works for me... it should only work if paired with "command" though. Is that what you mean maybe?

Also some actions seem to get lost in oblivion when spamming actions too quickly. As an example, I wanted to write myself a little shell script that creates a custom layout with a split pane like so:

This is true - I think I even mention in the docs somewhere that the actions will not necessarily run sequentially because with some actions we don't yet know when they end. Might be worth making this more explicit in the docs.

However for this case - why not create a layout instead? That's what they are there for and I think it will be much easier. Is there a feature there you're missing?

Also zellij action new-tab doesn't respect the $PWD of the pane it's called from. Is that on purpose?

That's definitely not on purpose! I think I'll also fix this after merging.

@imsnif imsnif temporarily deployed to cachix October 5, 2022 05:11 Inactive
@imsnif
Copy link
Member Author

imsnif commented Oct 5, 2022

To sum up what @har7an and I talked about offline:
We're good to go with this, and after merging are going to add the following things (I will add these things):

  1. Add a --name to the new-pane action
  2. Make cwd work with terminals as well as command panes
  3. Add a cwd attribute to layouts which will be used with all panes opened under that layout
  4. Relative paths in new-tab should be interpreted from said cwd
  5. Add a --cwd flag to the new-tab action that will optionally override the above root_cwd in the layouts

Points 2-5 should allow more dynamic layouts which replicate the same flow for different folders without us having to commit to some sort of templating language for now (even though we are not ruling out the idea of a templating language for more dynamic content in the future).

@raphCode
Copy link
Contributor

Before the release we should update the docs, since error messages point to it:
https://zellij.dev/documentation/configuration.html
Currently we still show yaml config stuff there.

@raphCode
Copy link
Contributor

There was a minor typo in the first post: pane_tempalte, I corrected it since this is supposed to be the documentation later.

There are still 3 occurrences of this typo in the code if you care about them.

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.

3 participants