zellij-org / zellij

A terminal workspace with batteries included
https://zellij.dev
MIT License
21.62k stars 652 forks source link

[Feature request] Restore Zellij environment after system restart #575

Closed VasanthakumarV closed 1 year ago

VasanthakumarV commented 3 years ago

Currently, all the Zellij sessions are lost after the System restarts, it will be nice to have them restored for a seamless experience.

Reference

a-kenji commented 3 years ago

This is an awesome feature!

From the faq: The vim and nvim sessions seem to be handled by tpope's obsession.

a-kenji commented 3 years ago

We might want to use XDG_STATE_HOME to store relevant data for this feature.

piperun commented 3 years ago

Should add that screen has this built in and might be something to look into.

TheSHEEEP commented 2 years ago

One thing I'd like to add, which would be more or less the same feature, but more manual control:

Allow storing / loading sessions from/to file. That way, you could even start a session, store it somewhere in the cloud, then resume it (within reason) on a different machine, three days later.

I feel like, once session restoration after restart is working, this would probably be a no-brainer: Just expose the save/load functionality to the user, e.g. ctrl + o -> (s)ave session to file / (l)oad session from file

ngirard commented 2 years ago

I second @TheSHEEEP's proposal. Having an ability to dump an existing session to a file would be great ! Also I think this feature would be worth being exposed from the command line too, e.g.

zellij --dump-session > dump.yml
bhfmts commented 2 years ago

Hi there! I am wondering if this feature was implemented already or is it still under development?

mike-lloyd03 commented 1 year ago

I think an MVP would be being able to restore all tabs, layouts, and each pane's current working directory. Restoring active processes like nvim sessions could be a follow on feature. Is this something that would be better implemented as a plugin?

RomkaHorokhov commented 1 year ago

I personally can not imagine myself using tmux without the session save/autoload. I love what you've achieved with Zellij and am considering to switching to it, but until there will be session saving/restoring - I can not switch for work, there's too many folders and panes and tabs to open each morning. I'll probably use Zellij for personal projects/off work coding. Session saving/restoring would be a killer feature if it's included out of the box, I'm sure Plugin would also work, as soon as there's session saving - I'm switching ;)

imsnif commented 1 year ago

Just to chime in:

  1. I'd be happy to have this feature implemented as a built-in offering. I agree with @mike-lloyd03 regarding the phases of implementation.
  2. The hardest part here, I believe, would be to serialize the current session geometry to a file. Zellij intentionally does not implement its pane map state as a nested logical tree, in order to allow the greater flexibility in non-directional resizes we all know and love. The layouts however are a nested logical tree.

The implementer should figure out a way to turn this: [ {x: 0, y: 0, width: 100, height: 50}, {x: 0, y: 50, height: 50, width: 50}, {x: 50, y: 50, height: 50, width: 50} ]

Into this:

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

The code base right now only does this the other way around. Similar implementations from other multiplexers - for the reason stated above - likely won't work. If someone wishes to take this up - awesome.

sundy-li commented 1 year ago

@imsnif let me try it!

mike-lloyd03 commented 1 year ago

@imsnif Do we need to save the session state to disk in layout format though? What's stopping us from writing to disk exactly what you have ([ {x: 0, y: 0, width: 100, height: 50}, ...]) and configuring session restoration to parse that?

Or are we hoping to use the layout argument in start_client() to achieve session restoration? If so, we'd have to parse the session state to the layout format and write it to disk every time the layout changes (new split pane, new tab, rename something, etc...).

Is that the best way forward?

@sundy-li have you done any work on this?

imsnif commented 1 year ago

@mike-lloyd03 - layout files is how we serialize our session state. While we could theoretically massage the entire code-base to add an additional serialization layer as you propose, it'll both be an incredible amount of work and an incredible lot to maintain.

It's logically possible (mostly) to perform this sort of serialization that I spoke about, and once we have that it'll be much cleaner to maintain and edit (you'd also be able to edit your saved layout/session files to fit other cases, for example).

So I essentially think this is the best way forward, yeah. :)

(There will probably be additional places where we need to update the session state to disk, eg. when opening a new app - which is something we cannot detect, so we'll probably end up doing this on every keystroke... but really, this is the easier part - once we have this serialization layer, we're home :) )

mike-lloyd03 commented 1 year ago

@imsnif Thank you. I'd like to at least take a whack at the algorithm for serializing structs into layouts. Can you provide me with a few more example cases (preferably edge cases).

And also is the example you gave above a vector of PaneGeoms?

imsnif commented 1 year ago

@mike-lloyd03 - sorry, I don't have a lot of time for doing back-and-forth on this. There are a lot of examples of layouts in the documentation, eg. here: https://zellij.dev/documentation/creating-a-layout.html

And yeah, my example assumed PaneGeom. We of course don't keep things exactly like this, but as mentioned: once you get this basic unit to work we can start working on integrating it with everything else.

alekspickle commented 1 year ago

I think this is not started yet because it seems big. I'm going to start this with saving at least tab layout to a file and see how it goes.

mike-lloyd03 commented 1 year ago

@alekspickle Go for it. I started working on it a bit. It seems pretty straightforward since it's just an algorithm problem. I just haven't had time.

Layouts now support floating panes though so that might complicate it somewhat.

eleijonmarck commented 1 year ago

how can one save a setup that they want as a default? i do not want to save my setup but basically i want to have a default setup when i make a new session with zellij

eleijonmarck commented 1 year ago

i guess, i could create a layout that is loaded by default on a new session? 🤔

I am also curious if i can specify the default session by default in the configuration, so far i have not found that option

alekspickle commented 1 year ago

@eleijonmarck hijacking a thread is not nice. It is generally better to ask all help questions in discord or create new issue that can be closed with an answer right away.

If you dump the config you can just specify the name of a default layout with default_layout name If you want help with configuration to avoid reading corresponding docs part or the config itself, just ask in discord, people usually active there.

eleijonmarck commented 1 year ago

@alekspickle the question is related as I was asking for a current workaround. @alekspickle it's also generally not nice to tell someone they have hijacked a thread in open source software. we are all here to freely ask question. you are rather shying away potential users from using the software by that behavior.

imsnif commented 1 year ago

@eleijonmarck , @alekspickle - I'm sure neither of you were meaning to be unkind or impolite to the other person. I ask you - let's please cool things down and enjoy our terminal emulators.

bazuka5801 commented 1 year ago

Any updates?

AlixBernard commented 1 year ago

Hi, as I could not find any mention of progress on this issue, I decided to give it a try as practice. I am not new to programming but I am new to Rust and it was challenging! However I have managed to solve the "hard part" of the problem according to @imsnif, ie serialize the session geometry (Vec<PaneGeom>) to a string; actually I did Vec<PaneGeom> -> Vec<TiledPaneLayout> -> String.

Here is an example of geometry session to serialize: [{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]

A representation of the layout so you don't have to scramble your brain trying to decipher:

+-----------------------+
|     1     |     3     |
|           |-----------|
|-----------|  4  |  5  |
|     2     |-----------|
|           |     6     |
+-----------------------+

The kdl string:

tab {
    pane split_direction="vertical" {
        pane size=40 {
            pane size=30
            pane size=30
        }
        pane size=40 {
            pane size=20
            pane size=20 split_direction="vertical" {
                pane size=20
                pane size=20
            }
            pane size=20
        }
    }
}

I did a few more examples in order to test the edge cases and I think I cover them all. As I said I am new to Rust but I think I managed to do clean code and it is suitable to integrate to the code base.

Could some contributors help me and/or guide me on what should be done next?

alekspickle commented 1 year ago

@AlixBernard Great work! I think some comments discouraged me to finish it.

You can start by opening a PR so the contributors can at least evaluate. As a bonus points, do you think it covers new-ish swap_layouts?

imsnif commented 1 year ago

Hey @AlixBernard - this looks great! I'm looking forward to reading the algorithm.

So, the high level things we need to do to make this feature happen are:

  1. Implement this algorithm as a key binding and/or CLI command for Zellij to dump the current layout in a tab and the screen (all the tabs in the screen). (Bonus: also dump the x/y/width/height of the floating panes)
  2. Make this algorithm also be aware of fixed vs. flexible panes (right now the algorithm does fixed, as I understand it? But most of the time we'd want it to do flexible (percentages), and also a combination of the two - we have some ways of handling this, I can happily provide more info)
  3. Give the algorithm access to information about the running command/plugin inside each pane (we'll have to implement some stuff here, but a lot of it is already there - also happy to provide more drilled down guidance)
  4. Write some mechanism that dumps the current layout (possibly behind a config flag) on every change in 3.
  5. Done - we've got persistent sessions

I don't know how much of this you'd like to sign up for, but we can totally do it in chunks so that if you find you don't have the time or it's not fun for you, you won't lose the work you did. I find this feature to be high priority and so will try to make it a priority regarding guidance and such.

imsnif commented 1 year ago

(also, forgot to mention: I'm happy to drill down into each one of these points with more specific pointers to places in the code base if you'd like)

alekspickle commented 1 year ago

As for infrastucture code needed for actions to work you can take a look at this draft

AlixBernard commented 1 year ago

Thanks! I think I should have specified that it is still really basic though, so far I only have a few functions that do the jobs (the main one being kdl_string_from_geoms). It needs to be adapted to fit into the code base and to take into account everything else, I don't think it is advanced enough for a PR. I will add it below so you can judge by yourselves. I haven't investigated what are swap layouts so I assume they are not supported.

Here is some code with examples that can be put in zellij/zellij-utils/src/main.rs and run with cd zellij/zellij-utils && cargo run

Code ```rust use std::collections::HashMap; use zellij_utils::input::layout::{SplitDirection, SplitSize, TiledPaneLayout}; use zellij_utils::pane_size::{Dimension, PaneGeom}; fn main() { // Set test data let vec_string_geoms = vec![ r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"#, r#"[{"x": 0, "y": 0, "cols": 80, "rows": 30}, {"x": 0, "y": 30, "rows": 30, "cols": 30}, {"x": 30, "y": 30, "rows": 30, "cols": 50}]"#, r#"[{"x": 0, "y": 0, "cols": 60, "rows": 40}, {"x": 60, "y": 0, "rows": 40, "cols": 20}, {"x": 0, "y": 40, "rows": 20, "cols": 60}, {"x": 60, "y": 40, "rows": 20, "cols": 20}]"#, r#"[{"x": 0, "y": 0, "cols": 40, "rows": 20}, {"x": 40, "y": 0, "rows": 20, "cols": 40}, {"x": 0, "y": 20, "rows": 20, "cols": 25}, {"x": 25, "y": 20, "rows": 20, "cols": 30}, {"x": 55, "y": 20, "rows": 20, "cols": 25}, {"x": 0, "y": 40, "rows": 20, "cols": 40}, {"x": 40, "y": 40, "rows": 20, "cols": 40}]"#, r#"[{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]"#, r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, ]; let vec_hashmap_geoms: Vec>> = vec_string_geoms .iter() .map(|s| serde_json::from_str(s).unwrap()) .collect(); let vec_geoms: Vec> = vec_hashmap_geoms .iter() .map(|hms| { hms.iter() .map(|hm| get_panegeom_from_hashmap(&hm)) .collect() }) .collect(); for (i, geoms) in vec_geoms.iter().enumerate() { let kdl_string = kdl_string_from_geoms(&geoms); println!("========== {i} =========="); println!("{kdl_string}\n"); } } pub fn get_panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { PaneGeom { x: hm["x"] as usize, y: hm["y"] as usize, rows: Dimension::fixed(hm["rows"] as usize), cols: Dimension::fixed(hm["cols"] as usize), is_stacked: false, } } pub fn kdl_string_from_geoms(geoms: &Vec) -> String { let layout = get_layout_from_geoms(&geoms, None); let tab = if &layout.children_split_direction != &SplitDirection::default() { vec![layout] } else { layout.children }; kdl_string_from_tab(&tab) } fn kdl_string_from_tab(tab: &Vec) -> String { let mut kdl_string = String::from("tab {\n"); let indent = " "; let indent_level = 1; for layout in tab { kdl_string.push_str(&kdl_string_from_layout(&layout, indent, indent_level)); } kdl_string.push_str("}"); kdl_string } fn kdl_string_from_layout(layout: &TiledPaneLayout, indent: &str, indent_level: usize) -> String { let mut kdl_string = String::from(&indent.repeat(indent_level)); kdl_string.push_str("pane "); match layout.split_size { Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!("size={size} ")), Some(SplitSize::Percent(size)) => kdl_string.push_str(&format!("size={size}% ")), None => (), }; if layout.children_split_direction != SplitDirection::default() { let direction = match layout.children_split_direction { SplitDirection::Horizontal => "horizontal", SplitDirection::Vertical => "vertical", }; kdl_string.push_str(&format!("split_direction=\"{direction}\" ")); } if layout.children.is_empty() { kdl_string.push_str("\n"); } else { kdl_string.push_str("{\n"); for pane in &layout.children { kdl_string.push_str(&kdl_string_from_layout(&pane, indent, indent_level + 1)); } kdl_string.push_str(&indent.repeat(indent_level)); kdl_string.push_str("}\n"); } kdl_string } fn get_layout_from_geoms(geoms: &Vec, split_size: Option) -> TiledPaneLayout { let (children_split_direction, splits) = match get_splits(&geoms) { Some(x) => x, None => { return TiledPaneLayout { split_size, ..Default::default() } }, }; let mut children = Vec::new(); let mut remaining_geoms = geoms.clone(); for i in 1..splits.len() { let (v_min, v_max) = (splits[i - 1], splits[i]); let subgeoms: Vec; (subgeoms, remaining_geoms) = match children_split_direction { SplitDirection::Horizontal => remaining_geoms .clone() .into_iter() .partition(|g| g.y + g.rows.as_usize() <= v_max), SplitDirection::Vertical => remaining_geoms .clone() .into_iter() .partition(|g| g.x + g.cols.as_usize() <= v_max), }; let subsplit_size = SplitSize::Fixed(v_max - v_min); children.push(get_layout_from_geoms(&subgeoms, Some(subsplit_size))); } TiledPaneLayout { children_split_direction, split_size, children, ..Default::default() } } fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { match ( geoms.iter().map(|g| g.x).min(), geoms.iter().map(|g| g.x + g.cols.as_usize()).max(), ) { (Some(x_min), Some(x_max)) => Some((x_min, x_max)), _ => None, } } fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { match ( geoms.iter().map(|g| g.y).min(), geoms.iter().map(|g| g.y + g.rows.as_usize()).max(), ) { (Some(y_min), Some(y_max)) => Some((y_min, y_max)), _ => None, } } fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { if geoms.len() == 1 { return None; } let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) { (Some(x_lims), Some(y_lims)) => (x_lims, y_lims), _ => return None, }; let mut direction = SplitDirection::default(); let mut splits = match direction { SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), }; if splits.len() <= 2 { direction = !direction; splits = match direction { SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), }; } if splits.len() <= 2 { None } else { Some((direction, splits)) } } fn get_vertical_splits( geoms: &Vec, x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { let ((_, x_max), (y_min, y_max)) = (x_lims, y_lims); let height = y_max - y_min; let mut splits = Vec::new(); for x in geoms.iter().map(|g| g.x) { if splits.contains(&x) { continue; } if geoms .iter() .filter(|g| g.x == x) .map(|g| g.rows.as_usize()) .sum::() == height { splits.push(x); }; } splits.push(x_max); splits } fn get_horizontal_splits( geoms: &Vec, x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { let ((x_min, x_max), (_, y_max)) = (x_lims, y_lims); let width = x_max - x_min; let mut splits = Vec::new(); for y in geoms.iter().map(|g| g.y) { if splits.contains(&y) { continue; } if geoms .iter() .filter(|g| g.y == y) .map(|g| g.cols.as_usize()) .sum::() == width { splits.push(y); }; } splits.push(y_max); splits } ``` I use recursion in the code, as my first attempt without it was to messy and hard to read. The code could be optimized in some places by making x-sorted and y-sorted copies of `Vec` however as the number of `PaneGeom`s in a tab shouldn't be too high in practice so it may not be worth it, at least for now.

Here are some details on the algorithm implemented

Algorithm Assumptions: - The input is a `Vec` where the `PaneGeom`s form a partition of the domain - The rectangles defined by `PaneGeom`s are can be formed by recursive horizontal and/or vertical splits Limits: - Based only on `PaneGeom`s it cannot be inferred which split comes first (horizontal or vetical) when both are possible (imagine a rectangle with a cross splitting it for instance), in this case the default value of `SplitDirection` is prioritized Algorithm: - Reconstruct `TiledPaneLayout` 1. Consider all the `PaneGeom`s 2. For each value of `PaneGeom.x` (that is not equal to the minimum of `x`, as it would be the left edge of the domain) group all the `PaneGeom` that have this value of `x` and: - if the sum of their `rows` is equal to the height of the domain, then add it to a list keeping track of the values of `x` splitting the domain 3. If no value of `x` are found to split the domain then try again for the values of `y` with the sum of `cols` 4. At his point if there is more than 1 `PaneGeom` then you should have the values of `x` or `y` splitting the domain 5. Separate the `PaneGeom`s depending on where they are regarding the splitting values and repeat for each subdomain considering only their associated `PaneGeom`s - Serialize `TiledPaneLayout` to kdl format This one isn't complicated with recursion and can be seen in the code I hope it is clear enough to understand, if not let me know and I'll try to explain it better when I have more time.

I feel like I don't know enough the whole repo to know how to integrate it to a structure or make it as its own serialization module, if you could suggest an idea that will be helpful.

As you pointed out, there is still a lot to do and I don't have the time to do it all so anyone can feel free to use the algorithm or the code to do it. However I'm willing to try to improve this piece of code to allow flexible panes (I will need more info about this), work with swap layouts if possible, and adapt it so it can be properly integrated to the code base (please point to me anything that should be modified).

Anyway I'm happy to hear that this feature is considered a priority and I hope that my contribution will help its implementation. I'll try to help as I can)

alekspickle commented 1 year ago

I'll try to make a usable action out of this. There is a lot stuff to add, since layouts advanced quite a bit, but this is already good base. Huge effort, man!

imsnif commented 1 year ago

Thanks to the two of you! I'm very happy to see this collab and things moving forward on this.

Just a note about swap layouts: I think it's safe to ignore them. They can't be changed at runtime and are handled as default in various cases, so we can probably deal with them on the "session restoration" side rather than the session serialization side we're discussing now.

I haven't delved deeply into the algorithm, but I think even this implementation brings us 80% of the way there. Please do reach out here or on Discord if you have any questions (eg. things that need to be supported or not, to save you time!)

Looking forward to this!

AlixBernard commented 1 year ago

So I've been thinking about flexible panes and, if I understood correctly, they are simply PaneGeoms that have Dimension with Constraint::Percent(_) instead of Constraint::Fixed(_) for the attributes rows and/or cols, right?

It will add some complexity but I think it should be possible. However I would need to know exactly how the input would be and other details:

Maybe giving an example would be easier than explaining everything, so below is an example of kdl layout output that covers some edge cases I foresee; if someone can give (or let me know how to obtain) the equivalent input I would get, it would help a lot. Thank you)

Example kdl layout ```kdl layout { tab { pane split_direction="vertical" { pane size="10%" { pane pane } pane size=10 pane { pane pane } pane { pane pane split_direction="vertical" { pane size="20%" pane } } } } } ```

I tried to find the answers by myself but so far I'm still unsure and it is quite time consuming.

imsnif commented 1 year ago

Hey @AlixBernard - first of all: please do ask. There's no need for you to spend time needlessly - the interaction between fixed and flexible panes in Zellij is also especially chaotic (split across a few different parts of the code base and written and patched by many different people).

So I've been thinking about flexible panes and, if I understood correctly, they are simply PaneGeoms that have Dimension with Constraint::Percent(_) instead of Constraint::Fixed(_) for the attributes rows and/or cols, right?

Yes, exactly.

* is it only a `Vec<PaneGeom>` without extra information on the dimensions, min/max values, etc.?

I'm not sure I understand the question... but I think in essence we're looking for an algorithm that does Vec<PaneGeom> to stringified KDL layout. Everything else can be massaged into the logic later somewhere (eg. pane titles, commands and such). Makes sense, or did I misunderstand?

* if `Dimension` has a constraint `Constraint::Percent(_)`, then its `inner` value will always be 1 or can it change? If it can change, in which way?

I think the idea is that inner is the "real" size of the pane (because pane sizes have to be translated to a fixed number of columns/rows so we know how to draw them). So if the screen width is 50 columns and we have a pane with 50%, its inner would be 25.

It's important to note though that we shouldn't rely on this always being the case. There are some intermediate states where this is not true (eg. in the middle of a resizing operation, on startup... but also in some unexpected places :)...)

Maybe giving an example would be easier than explaining everything,

Heh, nice edge case :) So, this is a good find and I think it's one of the reasons this is a bit of a hard problem. The "20%" pane in the layout you expressed means 20% out of its respective place in the tree, not in the whole of the screen (in the above case it would mean its direct neighbor gets 80%), but the data structures we hold mean 20% of the whole screen (since they don't know about the tree structure that originated them).

That being said, the vast majority of real-world user cases do not have such a complex setup. It's totally fine for this algorithm to fail with an error in all of these edge cases. If we can go for a "keep the fixed panes their size and divide the rest of the space as close as possible between the flexible panes" approach it would catch almost if not everything. Makes sense? Or did I totally misunderstand your question?

AlixBernard commented 1 year ago

Alright, thank you for your reply, you answered most of my questions! Just two points to clarify:

imsnif commented 1 year ago

if we could rely on inner only little change would be required in the code, are you sure it is not an option?

Hum, in that case sure. We can probably work out any inconsistencies from the outside if they arise. But we do need the end result to be percentages (or preferably even just bare pane nodes if it fits) unless the panes are explicitly fixed. Do you think it can work?

regarding the edge case, you mean that the 20% pane should have 20% of the (horizontal) size of its parent pane but the PaneGeom object will only display 20% without specifying, right? If this is the case and we cannot rely on inner then more tests will have to be performed but I think it should be possible

Yes! But this is not a case you'd get as input. As input any percentage you get would be of the whole screen. And then you can make the translation to absolute size and then to percentage of its place in the tree once you build it. But I guess this is why using inner would be helpful, right?

AlixBernard commented 1 year ago

Obtaining the percentage for the kdl string shouldn't be a problem, and omitting them when not required too. It is possible not to use inner though this would imply more computation and a more convoluted code.

As input any percentage you get would be of the whole screen.

This should be fine as well but then what would happen when this is mixed with fixed panes? Are the fixed value always excluded, meaning that the sum of percentage from all panes that are "side by side" will always add up to 100%?

imsnif commented 1 year ago

Yeah, the fixed panes are always excluded. Percentages should always add up to 100%.

imsnif commented 1 year ago

Hey @AlixBernard - gentlest of pings. Do you think you will have time to work on this in the near future? No candidates to pick up the work so far, but I think it would be good to indicate either way so we're clear on where things stand.

I'd be super happy to wait for as long as needed if you want to see this through and need more time.

AlixBernard commented 1 year ago

Hey! I should have more time from mid July to keep working on it. However, I would like to have some concrete test data (the PaneGeom vector at least) as it would clear any doubts arising for the tricky parts and it would also allow me to make better tests directly. Let me know if you can provide it or, better yet, explain me how to get it myself.

imsnif commented 1 year ago

Hey! I should have more time from mid July to keep working on it. However, I would like to have some concrete test data (the PaneGeom vector at least) as it would clear any doubts arising for the tricky parts and it would also allow me to make better tests directly. Let me know if you can provide it or, better yet, explain me how to get it myself.

Glad to hear it!

Here's a way to do it yourself:

  1. Add this function somewhere in TiledPanes (https://github.com/zellij-org/zellij/blob/main/zellij-server/src/panes/tiled_panes/mod.rs#L77):
    pub fn debug_print_geoms(&self) {
        log::info!("***** PRINTING PANE GEOMS *****");
        for (pane_id, pane) in &self.panes {
            log::info!("pane_id: {:?}, pane_geom: {:?}", pane_id, pane.position_and_size());
        }
        log::info!("*******************************");
    }
  2. Call it with this line at the end of the set_pane_frames function (https://github.com/zellij-org/zellij/blob/main/zellij-server/src/panes/tiled_panes/mod.rs#L288)
    self.debug_print_geoms();
  3. tail the output in the zellij log (by default it should be something like /tmp/zellij-1000/zellij-log/zellij.log - I think there's more info abot it in the CONTRIBUTING.md doc (this should be triggered every time there's a change, eg. when opening a new pane)
  4. Example output:
    
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:115]: ***** PRINTING PANE GEOMS *****
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:117]: pane_id: Terminal(0), pane_geom: PaneGeom { x: 0, y: 1, rows: Dimension { constraint: Percent(100.0), inner: 17 }, cols: Dimension { constraint: Percent(50.0), inner: 117 }, is_stacked: false }
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:117]: pane_id: Terminal(1), pane_geom: PaneGeom { x: 117, y: 1, rows: Dimension { constraint: Percent(100.0), inner: 17 }, cols: Dimension { constraint: Percent(50.0), inner: 117 }, is_stacked: false }
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:117]: pane_id: Plugin(0), pane_geom: PaneGeom { x: 0, y: 0, rows: Dimension { constraint: Fixed(1), inner: 1 }, cols: Dimension { constraint: Percent(100.0), inner: 234 }, is_stacked: false }
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:117]: pane_id: Plugin(1), pane_geom: PaneGeom { x: 0, y: 18, rows: Dimension { constraint: Fixed(2), inner: 2 }, cols: Dimension { constraint: Percent(100.0), inner: 234 }, is_stacked: false }
    INFO   |zellij_server::panes::til| 2023-06-28 16:44:05.786 [screen    ] [zellij-server/src/panes/tiled_panes/mod.rs:119]: *******************************


Please let me know if I can help with anything else!
alekspickle commented 1 year ago

Oh this would've saved me days in time and lots of confusion. goddammit 🥲

mike-lloyd03 commented 1 year ago

Haha yeah this was a blocker for me a few months ago as well.

alekspickle commented 1 year ago

@AlixBernard I added this to my draft, so you can experiment with it running zellij session, tailing zellij.log and running cargo xtask run -- action dump-layout

mikem-zed commented 1 year ago

When saving geometry we should convert absolute coordinates to percentage so it can be resorted in window of any size

alekspickle commented 1 year ago

@mikem-zed it's kind of in the essense of a constraint field I think :thinking:

alekspickle commented 1 year ago

I had to alter an algo a bit. It was not able to serialize the simplest layout so I doubled down to make this layout work first:


layout {
    pane size=1 borderless=true {
        plugin location="zellij:tab-bar"
    }
    pane
    pane size=2 borderless=true {
        plugin location="zellij:status-bar"
    }
}

It outputs something obviously wrong:


 HORIZONTAL:3
 initial splits: [40], direction: Horizontal
 VERTICAL:3
 second step splits: [0, 37],  direction: Vertical
mikem-zed commented 1 year ago

@mikem-zed it's kind of in the essense of a constraint field I think thinking

yeah, I overlooked it while looking into the code.

alekspickle commented 1 year ago

@AlixBernard can you hint why switch direction after initial split number is <= 2?

AlixBernard commented 1 year ago

Yes! The get_splits_vertical (and horizontal) functions maybe should be named get_edges_vertical (and horizontal) as it actually gets the x (or y) coordinates of each edge/delimitation that spans the whole domain considered. Therefore, the edges of the domain are considered and there will always be at least two edges/delim found (<= could be replaced by ==). If after looking in one direction we only found 2 edges, it means either that there is only 1 pane in the domain (which is not possible as this possibility is taken care of before) or that the domain is split across the whole domain in the other direction (or that the domain is partitioned in a way that should not be possible).

Anyway, the last time I worked on this issue I made modifications to take care of flexible panes but haven't posted them here yet as it is not ready. I'm starting to look at it again but as I said I'll have more time from mid July so I don't know if I'll be able to post the new version until then.

Lastly I had come to the conclusion that for now it will be better to use the inner attribute to figure out the layout but still taking into account the percentage so that flexible panes are serializable. Not using inner might prevent the proper serialization of some cases but most of all I think it makes the code too convoluted for now and should be investigated later. Maybe a solution would be to create some sort of lock to prevent the change of the inner values during serialization? This can be figured out once I have the code working

imsnif commented 1 year ago

Hey @AlixBernard @alekspickle - I hope it's okay if I ask about progress? Anything I can do to help speed things along?

AlixBernard commented 1 year ago

Hi, sorry for the late answer, I got busy and forgot about it..

Being able to see the real PaneGeoms data from layouts cleared a lot of confusion for me as well, so some impossible cases can be ignored. I have working code with fixed panes but that should work with flexible panes too but couldn't test it much; having tests that can test dynamically would be better, would you be able to make them? Eg:

#[test]
fn layout_with_one_pane_serialize() {
    let expected_kdl_layout = r#"
        layout {
            pane
        }
    "#;
    let layout = Layout::from_kdl(expected_kdl_layout, "layout_file_name".into(), None, None).unwrap();
    let geoms = geoms_from_layout(layout);  // step to obtain the `PaneGeom`s of the layout
    let kdl_layout = kdl_string_froms_geoms(geoms);  // step serializing the layout into a kdl string
    assert_eq!(kdl_layout, expected_kdl_layout);
}

It's more or less the same as the tests in zellij-utils/src/input/unit/layout_test.rs

There is also the problem of what other data should be added to the kdl string, such as the current working directory or the command/last command run (I don't know what is feasible). For now, my implementation reconstruct a TiledPaneLayout from a Vec<PaneGeom> and use the TiledPaneLayout to construct the kdl string which contains only information on the panes and their size.

I have some free time for the coming weeks so let me know if you want to discuss this more extensively via discord or an other way to speed up the process @imsnif @alekspickle