Tutorial 50 of 54 · for anyone whose terminal tool runs another program · Markdown

Run git and show its output

You'll run git log --oneline from an effect, collect what it prints and show it in a pane. A key runs it again. It starts from the app you built in 650.

Before you start

Install from crates.io. You need no Node and no package manager.

cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version

Every command and screenshot on this page was captured with fsr 0.x.

Start a process#

An effect or a handler starts a process with system.spawn(command, args, options) from @snapfire/fsr/terminal. It takes the place of spawn from Node's child_process, which a terminal page cannot import:

`spawn` from `node:child_process`, which is not FSR's

fsr migrate rewrites Node's spawn to system.spawn when it brings an Ink app over, which 680 shows.

The process runs with its output piped back to the page. stdout and stderr give data with each chunk. The process gives exit and close with its exit code. It gives error when it cannot start. The options it reads are cwd, env, shell and stdio: "inherit".

Write the git pane#

Add system to the std import:

app/routes/page.tsx
import { file, path } from "@snapfire/fsr/host";
import { system } from "@snapfire/fsr/terminal";

Then add a pane above Tasks:

app/routes/page.tsx
function GitPane({ runs }: { runs: number }) {
  const [lines, setLines] = useState<string[]>([]);
  const [failure, setFailure] = useState("");

  useEffect(() => {
    let read = "";
    const git = system.spawn("git", ["log", "--oneline", "-5"]);
    git.stdout.on("data", (chunk) => {
      read = read + chunk;
    });
    git.on("close", (code) => {
      if (code === 0) {
        setLines(read.trim().split("\n"));
        setFailure("");
      } else {
        setFailure(`git log exited with ${code}`);
      }
    });
  }, [runs]);

  return (
    <Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
      <Text bold>git log</Text>
      {failure ? <Text color="red">{failure}</Text> : lines.map((line) => <Text key={line} wrap="truncate">{line}</Text>)}
    </Box>
  );
}

read is a local of the effect that the data listener writes. It keeps its value from one chunk to the next, so by close it holds everything git printed. Each run of the effect gets a fresh read, so a second run does not add to the first.

Each listener runs as a handler of the component, so it may call the setters. Output can arrive in several chunks, which is why the lines are only split once the process has closed.

wrap="truncate" cuts a line at the pane's edge instead of wrapping it.

Run it again on a key#

The effect depends on runs, so changing runs runs git again. In Tasks, add the state below saving:

app/routes/page.tsx
const [runs, setRuns] = useState(0);

Add a branch to the page's handler, after the focus lines:

app/routes/page.tsx
if (input === "g") setRuns(runs + 1);

Place the pane below the two panes and name the key in the footer:

app/routes/page.tsx
<Box>
  <ListPane tasks={shown} selected={selected} active={!adding} onMove={move} onToggle={toggle} />
  <DetailPane task={shown[selected]} active={!adding} onRemove={remove} />
</Box>
<GitPane runs={runs} />
app/routes/page.tsx
<Text dimColor>{adding ? "Enter adds, Esc cancels" : "Tab switches panes, a adds, g reruns git"}</Text>

Run it#

git runs in the directory you start the app from. Make the project a repository with a few commits:

$ git init -q
$ git commit -q --allow-empty -m "start the task list"
$ git commit -q --allow-empty -m "save tasks to a file"
$ git commit -q --allow-empty -m "add a git pane"
$ fsr dev app --terminal
 Tasks tasks.json                                        1 open
╭──────────────────────────────╮╭──────────────────────────────╮
│ > Tag a release              ││ Tag a release                │
│   Answer the open issues     ││ open                         │
│                              ││ x removes it                 │
╰──────────────────────────────╯╰──────────────────────────────╯
╭──────────────────────────────────────────────────────────────╮
│ git log                                                      │
│ 80204f3 add a git pane                                       │
│ 12d479d save tasks to a file                                 │
│ 96f7519 start the task list                                  │
╰──────────────────────────────────────────────────────────────╯
 Tab switches panes, a adds, g reruns git                 saved

Your hashes differ. Leave it running and commit from a second terminal:

$ git commit -q --allow-empty -m "rerun git on g"

Press g in the app. git runs again and the new commit is on top:

╭──────────────────────────────────────────────────────────────╮
│ git log                                                      │
│ 467741c rerun git on g                                       │
│ 80204f3 add a git pane                                       │
│ 12d479d save tasks to a file                                 │
│ 96f7519 start the task list                                  │
╰──────────────────────────────────────────────────────────────╯

Started outside a repository, git exits with 128 and the pane says so in red:

│ git log                                                      │
│ git log exited with 128                                      │

Next up: 670. Keep time.