Tutorial 46 of 54 · for anyone whose terminal tool takes flags · Markdown

Read the command line

You'll pass flags to the task list after --, read them in a loader and hand them to the page as props. The loader runs once, when the terminal opens. It starts from the list you built in 610.

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.

Everything after -- is the app's#

fsr dev app --terminal reads its own flags up to --. The rest of the line goes to your app:

$ fsr dev app --terminal -- --file work.json --limit 2

The terminal turns that line into params, a map of strings:

You typeparams gets
--file work.jsonfile is "work.json"
--limit=2limit is "2"
--all with no value after itall is "true"
notes.txt with no flag before it"0" is "notes.txt", the next one "1"

Every value is a string. Convert a number yourself.

Write the loader#

Create app/routes/page.loader.ts beside the page:

app/routes/page.loader.ts
import type { Ctx } from "@snapfire/fsr/host";

export async function load({ params }: Ctx<"/">) {
  return {
    name: params.file ?? "tasks.json",
    limit: Number(params.limit ?? "10"),
  };
}

This is the same loader a web page has. On the web it runs for each request. In the terminal it runs once, before the first frame. What it returns becomes the root page's props.

Take the props#

The page destructures what the loader returned. RootProps from @generated/client is the type of those props:

app/routes/page.tsx
import { useState } from "react";
import type { RootProps } from "@generated/client";
import { Box, Newline, Spacer, Text, useInput } from "ink";

type Task = { title: string; done: boolean };

const START: Task[] = [
  { title: "Write the README", done: true },
  { title: "Tag a release", done: false },
  { title: "Answer the open issues", done: false },
];

export default function Tasks({ name, limit }: RootProps) {
  const [tasks, setTasks] = useState(START);
  const [selected, setSelected] = useState(0);

  useInput((input, key) => {
    if (key.upArrow && selected > 0) setSelected(selected - 1);
    if (key.downArrow && selected < shown.length - 1) setSelected(selected + 1);
    if (input === " ") {
      setTasks(tasks.map((task, index) => (index === selected ? { ...task, done: !task.done } : task)));
    }
  });

  const open = tasks.filter((task) => !task.done).length;
  const shown = tasks.slice(0, limit);

  return (
    <Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} width={44}>
      <Box>
        <Text bold>Tasks</Text>
        <Text dimColor> {name}</Text>
        <Spacer />
        <Text color={open === 0 ? "green" : "yellow"}>{open} open</Text>
      </Box>
      <Box flexDirection="column" marginY={1}>
        {shown.map((task, index) => (
          <Box key={task.title}>
            <Text color="cyan">{index === selected ? "> " : "  "}</Text>
            <Text bold={index === selected} dimColor={task.done} strikethrough={task.done}>
              {task.title}
            </Text>
          </Box>
        ))}
      </Box>
      {tasks.length > shown.length ? <Text dimColor>{tasks.length - shown.length} more not shown</Text> : null}
      <Text dimColor>
        up and down move, space marks one done
        <Newline />
        Ctrl-C quits
      </Text>
    </Box>
  );
}

shown is the first limit tasks. The rows, the down arrow and the line under the rows read it.

Run it#

With no flags the loader's defaults apply:

$ fsr dev app --terminal
╭──────────────────────────────────────────╮
│ Tasks tasks.json                  2 open │
│                                          │
│ > Write the README                       │
│   Tag a release                          │
│   Answer the open issues                 │
│                                          │
│ up and down move, space marks one done   │
│ Ctrl-C quits                             │
╰──────────────────────────────────────────╯

Quit and pass both flags:

$ fsr dev app --terminal -- --file work.json --limit 2
╭──────────────────────────────────────────╮
│ Tasks work.json                   2 open │
│                                          │
│ > Write the README                       │
│   Tag a release                          │
│                                          │
│ 1 more not shown                         │
│ up and down move, space marks one done   │
│ Ctrl-C quits                             │
╰──────────────────────────────────────────╯

--limit=1 works too and leaves one row with 2 more not shown. The name does nothing yet. In the next step it picks the file the list is saved to.

Next up: 630. Save to a file.