package main // Tool panes: any terminal program this interface does not cover, opened beside // what you were reading and closed when you are done with it. // // Nothing is rebuilt. Lazygit, Hunk, Yazi, Neovim, fzf and ocr are the tools // people already use, and a pane is the correct place for them: they keep their // own keys, their own colours and their own way of working. // // A tool pane is not part of the layout. It is created on demand in the // workspace you are in and it disappears when the program exits, so the eight // workspaces stay as they are. import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strconv" "syscall" ) type tool struct { name string command string about string } // tools are offered in this order: the shell first because it can reach // anything, then the three that read code, then the review. var tools = []tool{ {"shell", "exec zsh -i", "a prompt in the project folder"}, {"lazygit", "lazygit", "branches, commits, staging, history"}, {"hunk", "hunk", "a change read as a review"}, {"yazi", "yazi", "files"}, {"nvim", "nvim", "editing one file"}, {"fzf", "fzf", "picking anything"}, // A review that finds nothing exits in a second, which looks like a broken // pane. It is given a last line so the output can be read before closing. {"ocr", "ocr review --color never; printf '\\npress enter to close\\n'; read -r _", "the review of the current change"}, {"pi sessions", "yazi ~/.pi/paperclips", "every Pi conversation Paperclip has recorded"}, {"pi resume", "pi --resume", "Pi's own list of sessions; pick one and read it"}, } // currentWorkspace finds the workspace this program is drawn in. TUIOS knows; // the environment names the pane, and the pane's window says which workspace. func currentWorkspace() (string, string, error) { session := os.Getenv("TUIOS_SESSION") pane := os.Getenv("TUIOS_PANE_ID") if session == "" || pane == "" { return "", "", fmt.Errorf("this is not running inside a TUIOS pane") } out, err := exec.Command("tuios", "list-windows", "-s", session, "--json").Output() if err != nil { return "", "", fmt.Errorf("cannot ask TUIOS where this pane is: %w", err) } ws := workspaceOf(string(out), pane) if ws == "" { return "", "", fmt.Errorf("this pane is not in the layout TUIOS reports") } return session, ws, nil } // workspaceOf finds the workspace number of one window in the JSON TUIOS // prints. It decodes properly rather than scanning text: a pane id appears in // more than one place, and the first match is not always the window. func workspaceOf(raw, paneID string) string { var doc struct { Windows []struct { WindowID string `json:"window_id"` Workspace int `json:"workspace"` } `json:"windows"` } if err := json.Unmarshal([]byte(raw), &doc); err != nil { return "" } for _, w := range doc.Windows { if w.WindowID == paneID { return strconv.Itoa(w.Workspace) } } return "" } // openTool starts a program as a new pane in the current workspace. func openTool(folder, name string) error { var chosen *tool for i := range tools { if tools[i].name == name { chosen = &tools[i] } } if chosen == nil { return fmt.Errorf("no tool called %q", name) } session, ws, err := currentWorkspace() if err != nil { return err } log, _ := os.Create(filepath.Join(os.TempDir(), "paperclip-harness-tool.log")) // A pane started this way gets a smaller PATH than an interactive shell, so // the places a person's tools actually live are put back before the tool // runs. Without this, ocr is "command not found" inside its own pane. prelude := `export PATH="$HOME/.local/bin:$HOME/.nix-profile/bin:` + `/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin:$PATH"; ` cmd := exec.Command("tuios", "new-window", chosen.name, "-s", session, "--workspace", ws, "--cwd", folder, "--", "/bin/sh", "-c", prelude+chosen.command) cmd.Stdout = log cmd.Stderr = log cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} if err := cmd.Start(); err != nil { return fmt.Errorf("cannot open %s: %w", chosen.name, err) } go func() { cmd.Wait() if log != nil { log.Close() } }() return nil } // --- the smallest JSON reader that does this one job ------------------------- type scanner struct { s string pos int } func newScanner(s string) *scanner { return &scanner{s: s} } // nextObject returns the next {...} block, or "" at the end. func (sc *scanner) nextObject() string { for sc.pos < len(sc.s) && sc.s[sc.pos] != '{' { sc.pos++ } if sc.pos >= len(sc.s) { return "" } depth := 0 start := sc.pos for sc.pos < len(sc.s) { switch sc.s[sc.pos] { case '{': depth++ case '}': depth-- if depth == 0 { sc.pos++ return sc.s[start:sc.pos] } } sc.pos++ } return "" } func contains(s, sub string) bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false } // fieldNumber reads "name": 12 out of an object. func fieldNumber(obj, name string) string { key := `"` + name + `":` for i := 0; i+len(key) <= len(obj); i++ { if obj[i:i+len(key)] != key { continue } j := i + len(key) for j < len(obj) && (obj[j] == ' ' || obj[j] == '"') { j++ } start := j for j < len(obj) && obj[j] >= '0' && obj[j] <= '9' { j++ } if _, err := strconv.Atoi(obj[start:j]); err == nil { return obj[start:j] } } return "" } // switchWorkspace shows a workspace by name. It is how one pane can hand you to // another without any message passing: the name is the address. func switchWorkspace(name string) error { session, _, err := currentWorkspace() if err != nil { return err } out, err := exec.Command("tuios", "list-workspaces", "-s", session, "--json").Output() if err != nil { return err } var doc struct { Workspaces []struct { Workspace int `json:"workspace"` Name string `json:"name"` } `json:"workspaces"` } if err := json.Unmarshal(out, &doc); err != nil { return err } for _, w := range doc.Workspaces { if w.Name == name { return exec.Command("tuios", "select-workspace", strconv.Itoa(w.Workspace), "-s", session).Run() } } return fmt.Errorf("no workspace called %q", name) }