Chapter 4 · Real programs

A console project of your own

You are reading the whole lesson on one page. Back to one step at a time

Step 1 of 20 · Concept

A tool of your own

By the end of this lesson you have finished a real tool of several files: a to-do list.

It reads commands, keeps tasks with tags, saves them to a file and prints a report. Every piece comes from an earlier lesson. You will:

  • write four functions, one in each file,
  • run the whole tool on a list of commands,
  • add a command of your own.
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";

class CommandError extends Error {
  constructor(message) {
    super(message);
    this.name = "CommandError";
  }
}

function runCommand(list, line) {
  const command = line.split(" ")[0];
  const rest = line.slice(command.length + 1);
  if (command === "add") {
    const task = list.add(rest);
    console.log(`Added ${list.tasks.length}: ${task.title}`);
  } else if (command === "done") {
    const n = Number(rest);
    if (n >= 1 && n <= list.tasks.length) {
      list.done(n);
      console.log(`Done: ${list.tasks[n - 1].title}`);
    } else {
      throw new CommandError(`No task ${rest}`);
    }
  } else if (command === "list") {
    console.log(report(list.tasks));
  } else {
    throw new CommandError(`Unknown command: ${command}`);
  }
}

const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);

const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
  try {
    runCommand(list, line);
  } catch (e) {
    if (e instanceof CommandError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }
}
await save(list, FILE);
export class Task {
  constructor(title, tags, done = false) {
    this.title = title;
    this.tags = tags;
    this.done = done;
  }

  hasTag(tag) {
    return this.tags.includes(tag);
  }
}

export class TaskList {
  constructor(tasks = []) {
    this.tasks = tasks;
  }

  // "Buy milk #home": "Buy milk", tags ["home"]
  add(text) {
    // words that start with # are tags (no #);
    // the other words, joined again, the title
    const task = new Task(text, []);
    this.tasks.push(task);
    return task;
  }

  // n counts from 1, as the report numbers them
  done(n) {
    this.tasks[n - 1].done = true;
  }

  open() {
    return this.tasks.filter((task) => !task.done);
  }
}
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";

const list = new TaskList();
list.add("Buy milk #home");
list.add("Write report #work");
list.add("Water plants #home");
await save(list, "data/tasks.json");
const again = await load("data/tasks.json");
console.log(`Loaded ${again.tasks.length} tasks`);
console.log(again.tasks[2].hasTag("home"));
// " 1 [ ] Buy milk             #home"
export function formatTask(task, n) {
  // n in 2 places, [x] or [ ], the title in 20,
  // then the tags with their #, one space between
  return task.title;
}

export function report(tasks) {
  const lines = [];
  for (let i = 0; i < tasks.length; i++) {
    lines.push(formatTask(tasks[i], i + 1));
  }
  return lines.join("\n");
}
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";

class CommandError extends Error {
  constructor(message) {
    super(message);
    this.name = "CommandError";
  }
}

function runCommand(list, line) {
  const command = line.split(" ")[0];
  const rest = line.slice(command.length + 1);
  // add: add rest, log Added 3: Water plants
  // done: mark task n, log Done: Buy milk;
  //   no task n: throw a CommandError
  // list: log the report
  throw new CommandError(`Unknown command: ${command}`);
}

const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);

const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
  try {
    runCommand(list, line);
  } catch (e) {
    if (e instanceof CommandError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";

class CommandError extends Error {
  constructor(message) {
    super(message);
    this.name = "CommandError";
  }
}

function runCommand(list, line) {
  const command = line.split(" ")[0];
  const rest = line.slice(command.length + 1);
  if (command === "add") {
    const task = list.add(rest);
    console.log(`Added ${list.tasks.length}: ${task.title}`);
  } else if (command === "done") {
    const n = Number(rest);
    if (n >= 1 && n <= list.tasks.length) {
      list.done(n);
      console.log(`Done: ${list.tasks[n - 1].title}`);
    } else {
      throw new CommandError(`No task ${rest}`);
    }
  } else if (command === "list") {
    console.log(report(list.tasks));
  } else {
    throw new CommandError(`Unknown command: ${command}`);
  }
}

const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);

const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
  try {
    runCommand(list, line);
  } catch (e) {
    if (e instanceof CommandError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";

class CommandError extends Error {
  constructor(message) {
    super(message);
    this.name = "CommandError";
  }
}

function runCommand(list, line) {
  const command = line.split(" ")[0];
  const rest = line.slice(command.length + 1);
  if (command === "add") {
    const task = list.add(rest);
    console.log(`Added ${list.tasks.length}: ${task.title}`);
  } else if (command === "done") {
    const n = Number(rest);
    if (n >= 1 && n <= list.tasks.length) {
      list.done(n);
      console.log(`Done: ${list.tasks[n - 1].title}`);
    } else {
      throw new CommandError(`No task ${rest}`);
    }
  } else if (command === "list") {
    console.log(report(list.tasks));
  } else {
    throw new CommandError(`Unknown command: ${command}`);
  }
}

const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);

const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
  try {
    runCommand(list, line);
  } catch (e) {
    if (e instanceof CommandError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";

class CommandError extends Error {
  constructor(message) {
    super(message);
    this.name = "CommandError";
  }
}

function runCommand(list, line) {
  const command = line.split(" ")[0];
  const rest = line.slice(command.length + 1);
  if (command === "add") {
    const task = list.add(rest);
    console.log(`Added ${list.tasks.length}: ${task.title}`);
  } else if (command === "done") {
    const n = Number(rest);
    if (n >= 1 && n <= list.tasks.length) {
      list.done(n);
      console.log(`Done: ${list.tasks[n - 1].title}`);
    } else {
      throw new CommandError(`No task ${rest}`);
    }
  } else if (command === "list") {
    console.log(report(list.tasks));
  } else {
    throw new CommandError(`Unknown command: ${command}`);
  }
}

const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);

const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
  try {
    runCommand(list, line);
  } catch (e) {
    if (e instanceof CommandError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }
}
await save(list, FILE);
Terminal
Step 1 of 8The finished tool

Keyboard shortcuts

The step

ContinueEnter
Previous / next stepAlt+[ · Alt+]
Move along the steps bar← → Home End, then Enter
From a quiz's title to its answers↓
Skip to the stepTab, then Enter (first on the page)
Course outlineAlt+O
These shortcuts?

The workspace

Run the stepCtrl+Enter (⌘↵ on a Mac)
Lesson → editor → consoleF6 (Shift+F6 back)
Output ↔ TerminalCtrl+`
Focus the editorCtrl+. (⌘. on a Mac)
Hide or show the lessonAlt+L
Restore, close a messageEsc (outside the editor and the terminal)

The editor

Leave the editorF6 (Shift+F6 back); or Ctrl+M (⌃⇧M on a Mac), then Tab moves the focus (not in the vim mode)
Open a file by nameCtrl+P (⌘P on a Mac)
All commandsCtrl+Shift+P (⌘⇧P on a Mac) or F1
Go to a lineCtrl+G (⌃G on a Mac)
Show or hide the filesCtrl+B (⌘B on a Mac)
Previous / next file tabCtrl+Shift+[ · Ctrl+Shift+] (⌘⇧[ · ⌘⇧] on a Mac)
The file tabs← → Home End on a tab; Delete closes it
Save the files nowCtrl+S (⌘S on a Mac)
Go to a symbol in the fileCtrl+Shift+O (⌘⇧O on a Mac)
Go to the definitionF12
Format the fileCtrl+Shift+F (⌘⇧F on a Mac), or Shift+Alt+F
Go back / forward after a jumpAlt+← · Alt+→ (⌃- · ⌃⇧- on a Mac)