package multiplex import ( "fmt" "math" "path/filepath" "strings" ) // openZellij writes a layout with one tab per chunk of panes. Inside zellij // each tab is added to the current session; outside, a new session starts. func openZellij(panes []Pane, opts Options, r Runner) error { groups := chunks(panes, opts.PerWindow) if r.Getenv("ZELLIJ") != "" { for i, group := range groups { path := filepath.Join(opts.LayoutDir, fmt.Sprintf("netssh-multi-%d.kdl", i+1)) tab := zellijTab(windowLabel(opts.Label, i, len(groups)), group, true) if err := r.WriteFile(path, []byte(zellijLayout(tab))); err != nil { return err } if _, err := r.Output("zellij", "action", "new-tab", "--layout", path); err != nil { return err } if opts.Sync { if _, err := r.Output("zellij", "action", "toggle-active-sync-tab"); err != nil { return err } } } return nil } var tabs strings.Builder for i, group := range groups { tabs.WriteString(zellijTab(windowLabel(opts.Label, i, len(groups)), group, i == 0)) } path := filepath.Join(opts.LayoutDir, "netssh-multi.kdl") if err := r.WriteFile(path, []byte(zellijLayout(tabs.String()))); err != nil { return err } if opts.Sync { opts.Hint("zellij starts without broadcast: press Ctrl+t, then s, to type into all panes") } else { opts.Hint("toggle broadcast with Ctrl+t, then s (or netssh sync)") } return r.Exec("zellij", "--layout", path) } // zellijLayout wraps tabs with zellij's tab and status bars. func zellijLayout(tabs string) string { return `layout { default_tab_template { pane size=1 borderless=true { plugin location="zellij:tab-bar" } children pane size=2 borderless=true { plugin location="zellij:status-bar" } } ` + tabs + "}\n" } // zellijTab arranges panes in a grid: rows of ceil(sqrt(n)) columns. func zellijTab(name string, panes []Pane, focus bool) string { cols := int(math.Ceil(math.Sqrt(float64(len(panes))))) var b strings.Builder fmt.Fprintf(&b, " tab name=%s", kdlString(name)) if focus { b.WriteString(" focus=true") } b.WriteString(" {\n") for start := 0; start < len(panes); start += cols { row := panes[start:min(start+cols, len(panes))] indent := " " if len(row) > 1 { b.WriteString(indent + "pane split_direction=\"vertical\" {\n") indent += " " } for _, p := range row { fmt.Fprintf(&b, "%spane name=%s command=%s {\n", indent, kdlString(p.Title), kdlString(p.Argv[0])) if len(p.Argv) > 1 { args := make([]string, 0, len(p.Argv)-1) for _, arg := range p.Argv[1:] { args = append(args, kdlString(arg)) } fmt.Fprintf(&b, "%s args %s\n", indent, strings.Join(args, " ")) } b.WriteString(indent + "}\n") } if len(row) > 1 { b.WriteString(" }\n") } } b.WriteString(" }\n") return b.String() } // kdlString quotes s as a KDL string. func kdlString(s string) string { return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`).Replace(s) + `"` }