Русский
Русский
English
Статистика
Реклама

Todo-лист для командной строки на Deno

Вы уже наверняка слышали про Deno и, скорее всего, прочитали пару-тройку обзоров. В рамках своей статьи я предлагаю испытать Deno на практике, написав приложение для командной строки, попутно размышляя о преимуществах и возможных способах его применения.


Приложение для терминала позволит взглянуть на стандартную библиотеку, познакомиться с вводом и выводом без использования фреймворков.



Основные преимущества, такие как безопасность, TypeScript из коробки, встроенные инструменты, обширная стандартная библиотека, а также возможность импортировать и запускать скрипты по URL, выгодно отличают Deno от Node и Bash. С помощью Deno мы можем написать скрипт, залить на GitHub Gist и запускать при необходимости командой deno run https://path.to/script.ts, не заботясь о зависимостях и настройке окружения.


Для начала работы нужно только установить Deno одним из предложенных способов https://deno.land/#installation.


Todo для Todo


Todo-лист должен уметь:


  • добавлять, редактировать, удалять и отмечать задачи;
  • выводить список задач;
  • сохранять состояние;
  • работать в интерактивном режиме;
  • устанавливаться на компьютер.

Аргументы командной строки


Практически любая консольная утилита управляется с помощью передаваемых при запуске аргументов. В Deno они лежат массивом в Deno.args. А, к примеру, в Node мы бы использовали process.argv, в котором помимо аргументов находятся строки node и путь к скрипту.


// deno run args.ts first secondconsole.log(Deno.args) // [ "first", "second" ]

В простых случаях Deno.args будет достаточно, а в более сложных можно воспользоваться парсером из стандартной библиотеки: https://deno.land/std/flags или любым другим на выбор.


Итак, начнём


Реализуем первую команду вывод информации.


Создадим главный файл нашего приложения todo.ts. Лучше назвать файл осмысленно, так как в будущем он понадобится нам для запуска и установки.


Далее создадим файл /src/ui.ts, в котором будем хранить элементы пользовательского интерфейса. Добавим константу help, содержащую информационный текст.


export const help = `todo help - to show available commands`;

В todo.ts напишем свитч для обработки команд и выведем справку с помощью console.log().


#!/usr/bin/env deno runimport { help } from "./src/ui.ts";const [command, ...args] = Deno.args;switch (command) {  case "help":    console.log(help);}

Проверим работу команды:


deno run todo.ts help


Для того, чтобы при каждом запуске не писать deno run, добавим шебанг #!.


#!/usr/bin/env deno run

Разрешим запуск файла:


chmod +x todo.ts


Теперь для вывода справки можно использовать:


./todo.ts help


Стандартный вывод


В листинге выше мы использовали console.log(), но, мне кажется, идеологически log это инструмент разработчика, и реализовать с его помощью полноценный интерфейс не получится.


В Deno за стандартный вывод отвечает Deno.stdout, за ввод, соответственно, Deno.stdin.


/** A handle for `stdin`. */export const stdin: Reader & ReaderSync & Closer & { rid: number };/** A handle for `stdout`. */export const stdout: Writer & WriterSync & Closer & { rid: number };

И stdin, и stdout типизированы пересечением интерфейсов и объекта, содержащего rid, идентификатор ресурса.


Более подробно рассмотрим stdout и выведем Hello world! в консоль.


const textEncoder = new TextEncoder();const message: string = "Hello world!\n";const encodedMessage: Uint8Array = textEncoder.encode(message);await Deno.stdout.write(encodedMessage);

Запустим скрипт hello.ts:


deno run hello.ts


На экране появляется наш Hello world!.


export interface Writer {  write(p: Uint8Array): Promise<number>;}

Функция Deno.stdout.write принимает на вход массив байтов и возвращает количество записанных байтов. Deno.stdout.writeSync работает точно так же, только синхронно.


Строку необходимо предварительно закодировать с помощью объекта TextEncoder.
Можно не создавать TextEncoder, а импортировать функцию encode из стандартной библиотеки, которая делает то же самое, точно так же.


import { encode } from 'https://deno.land/std/encoding/utf8.ts';

Воспользуемся полученными знаниями и создадим файл terminal.ts с двумя функциями print и printLines. Последняя выводит строку и переводит каретку.


import { encode } from "https://deno.land/std/encoding/utf8.ts";export async function print(message: string) {  await Deno.stdout.write(encode(message));}export async function printLines(message: string) {  await print(message + "\n");}

Заменим console.log на printLines.


#!/usr/bin/env deno runimport { help } from "./src/ui.ts";import { printLines } from "./src/Terminal.ts";const [command, ...args] = Deno.args;switch (command) {  case "help":    await printLines(help);}

Чтение и запись файла


В Deno для чтения файла используется функция Deno.readFile и её синхронный аналог Deno.readFileSync.


function Deno.readFile(path: string | URL): Promise<Uint8Array>

const decoder = new TextDecoder("utf-8");const data = await Deno.readFile("hello.txt");console.log(decoder.decode(data));

Для записи в файл соответственно используются Deno.writeFile и Deno.writeFileSync.


function Deno.writeFile(path: string | URL, data: Uint8Array, options?: WriteFileOptions): Promise<void>

const encoder = new TextEncoder();const data = encoder.encode("Hello world\n");await Deno.writeFile("hello1.txt", data);  // overwrite "hello1.txt" or create it

В стандартной библиотеке Deno содержатся функции, позволяющие упростить взаимодействие с файловой системой: https://deno.land/std/fs. Например, код выше можно заменить вызовами функций readFileStr и writeFileStr.


Создадим интерфейс Task в src/Task.ts


export interface Task {   title: string;  isDone: boolean;}

и класс Todo в файле src/Todo.ts.


import {  writeJsonSync,  readJsonSync, } from "https://deno.land/std/fs/mod.ts"; import { Task } from "./Task.ts";export class Todo {  tasks: Task[] = [];  constructor(private file: string = "tasks.json") {    this.open();  }  open() {    try {      this.tasks = readJsonSync(this.file) as Task[];    } catch (e) {      console.log(e);      this.tasks = [];    }  }  save() {    writeJsonSync(this.file, this.tasks, { spaces: 2 });  }}

Как можно заметить, для чтения и записи данных я использовал специальные функции из стандартной библиотеки для работы с json-файлами.


Добавление задачи в список


Для реализации команды todo add "implement add command" добавляем в класс Todo метод add.


add(title: string) {    const task: Task = {        title,        isDone: false    };    this.tasks.push(task);    this.save();}

Так как мы добавили чтение и запись в файл, необходимо добавить права --allow-read --allow-write.


#!/usr/bin/env deno run --allow-read --allow-write import { help } from "./src/ui.ts";import { printLines } from "./src/terminal.ts";import { Todo } from "./src/Todo.ts";const [command, ...args] = Deno.args;const todo = new Todo();switch (command) {  case "help":    await printLines(help);    break;  case "add":    todo.add(args[0]);}

Вывод списка задач


В файл src/ui.ts добавляем функцию для форматирования списка задач.


import { Task } from "./Task.ts";import { green, red, yellow, bold } from "https://deno.land/std/fmt/colors.ts";export function formatTaskList(tasks: Task[]): string {  const title = `${bold("TODO LIST:")}`;  const list = tasks.map((task, index) => {    const number = yellow(index.toString());    const checkbox = task.isDone ? green("[*]") : red("[ ]");    return `${number} ${checkbox} ${task.title}`;  });  const lines = [    title,    ...list,  ];  return lines.join("\n");}

Для задания стиля и цвета текста используем функции форматирования из https://deno.land/std/fmt/colors.ts.


Добавляем метод list в класс Todo и команду ls в свитч.


async list() {  await printLines(formatTaskList(this.tasks));}

case "ls":  await todo.list();  break;

Таким же образом добавляем остальные методы и команды.


done(index: number): void {  const task = this.tasks[index];  if (task) {    task.isDone = true;    this.save();  }}undone(index: number): void {  const task = this.tasks[index];  if (task) {    task.isDone = false;    this.save();  }}edit(index: number, title: string) {  const task = this.tasks[index];  if (task) {    task.title = title;    this.save();  }}remove(index: number) {  this.tasks.splice(index);  this.save();}

case "edit": // todo edit 1 "edit second task"  await todo.edit(parseInt(args[0], 10), args[1]);  break;case "done" : // todo done 1  todo.done(parseInt(args[0], 10));  break;case "undone" : // todo undone 1  todo.undone(parseInt(args[0], 10));  break;case "remove" : // todo remove 1  todo.remove(parseInt(args[0], 10));  break;

Реализованные команды отвечают первым трём пунктам наших требований.


Работа в интерактивном режиме


Интерактивный режим подразумевает, что программа общается с пользователем, не закрываясь после выполнения команды.


Создадим в свитче кейс по умолчанию,


default:  await todo.interactive();

а в классе Todo асинхронный метод interactive.


async interactive() {  while (true) {    // show list    // read keypress    // do action  }}

Метод interactive содержит бесконечный цикл, в котором выводится пользовательский интерфейс, ожидается ответ пользователя и, в зависимости от нажатой клавиши, совершается действие.


Для управления курсором в интерактивном режиме необходимо импортировать функции из библиотеки cursor.
Добавляем права для доступа к энвайронменту, так как библиотека проверяет, в каком окружении программа запущена --allow-env.


import { clearDown, goUp, goLeft } from "https://denopkg.com/iamnathanj/cursor@v2.0.0/mod.ts";

В файле terminal.ts создадим функцию printInteractive.


export async function printInteractive(message: string) {  const lines = message.split("\n");  const numberOfLines = lines.length;  const lengthOfLastLine = lines[numberOfLines - 1].length;  await clearDown();  await print(message);  await goLeft(lengthOfLastLine);  if (numberOfLines > 1) {    await goUp(numberOfLines - 1);  }}

Она выводит текст на экран и возвращает курсор в начало. При следующем вызове всё стирается и печатается заново. Для того чтобы квадрат курсора не мешался, его можно скрыть функцией hideCursor.


Добавим в файл ui.ts константу toolbar, в которой будут перечислены доступные действия.


const toolbar = `${yellow("d")}one ${yellow("a")}dd ${yellow("e")}dit ${yellow("r")}emove`;

В функцию formatTaskList добавляем параметр showToolbar:


export function formatTaskList(tasks: Task[], showToolbar: boolean): string {// ...if (showToolbar) {  lines.push(toolbar);}

В зависимости от флага showToolbar мы будем показывать или скрывать подсказку.


Дорабатываем метод list:


async list(interactive: boolean = false) {  if (interactive) {    await printInteractive(formatTaskList(this.tasks, true));  } else {    await printLines(formatTaskList(this.tasks, false));  }}

Вывод реализован, перейдём к вводу.


Стандартный ввод


Интерфейс ввода, аналогично выводу работает с Uint8Array, поэтому для декодирования используется TextDecoder.


const textDecoder = new TextDecoder();const buffer: Uint8Array = new Uint8Array(1024);const n: number = <number>await Deno.stdin.read(buffer);const message: string = textDecoder.decode(buffer.subarray(0, n));

Создаём буфер размером, например, в 1024 байта. Передаём его в функцию Deno.stdin.read и ждём пока пользователь закончит ввод. Как только будет нажата клавиша enter, функция наполнит буфер и вернёт количество прочитанных байтов. Стоит отметить, что перевод строки \n будет в конце прочитанной последовательности. Обрезаем лишнее и декодируем полученную строку. Синхронная функция Deno.stdin.readSync работает аналогично.


Обработка клавиш


Для того чтобы получить информацию о нажатии клавиш, необходимо воспользоваться функцией Deno.setRaw. На текущий момент она доступна под флагом --unstable. setRaw и позволяет получить символы по одному, без обработки. В функцию необходимо передать идентификатор ресурса и флаг. Как только мы воспользовались этим режимом, нажатие CTRL+C перестаёт приводить к закрытию программы, и это поведение нужно реализовать самостоятельно.


Deno.setRaw(Deno.stdin.rid, true);const length = <number> await Deno.stdin.read(buffer);Deno.setRaw(Deno.stdin.rid, false);

В файл terminal.ts добавляем функцию readKeypress:


export async function readKeypress(): Promise<string> {  const buffer = new Uint8Array(1024);  Deno.setRaw(Deno.stdin.rid, true);  const length = <number> await Deno.stdin.read(buffer);  Deno.setRaw(Deno.stdin.rid, false);  return decode(buffer.subarray(0, length));}

В метод interactive добавляем возможность выйти из приложения.


async interactive() {  while (true) {    await this.list(true);    const key = await readKeypress();    if (key == "\u0003") { // ctrl-c      Deno.exit();    }  }}

Не отходя далеко от ввода текста, реализуем добавление задачи в интерактивном режиме.


В файл terminal.ts добавляем 2 функции:


export async function readLine(): Promise<string> {  const buffer = new Uint8Array(1024);  const length = <number> await Deno.stdin.read(buffer);  return decode(buffer.subarray(0, length - 1));}export async function prompt(question: string): Promise<string> {  await clearDown();  await print(question);  const answer = await readLine();  await goLeft(question.length + answer.length);  await goUp(1);  return answer;}

Функция readLine позволяет прочитать одну строку, исключая последний символ, перевод строки.


Функция prompt очищает экран, печатает вопрос, читает ввод пользователя, а затем перемещает курсор в начало, аналогично функции printInteractive.


В метод interactive добавляем обработку клавиши а:


if (key === "a") {  const title = await prompt("Add task: ");  if (title) {    await this.add(title);  }}

Перемещение по списку задач


Добавим поддержку стрелок клавиатуры. Для того чтобы понимать, какой элемент сейчас выбран, добавляем свойство currentIndex в класс Todo:


  private currentIndex: number = 0;

А в функции formatTaskList активную строку выделяем жирным и красим в жёлтый:


export function formatTaskList(  tasks: Task[],  showToolbar: boolean = false,  activeIndex?: number,): string {  const title = `${bold("TODO LIST:")}`;  const list = tasks.map((task, index) => {    // ...    const isActive = activeIndex === index;    const title = isActive ? bold(yellow(task.title)) : task.title;    return `${number} ${checkbox} ${title}`;  });  // ...}

В Todo создаём методы, которые будут изменять currentIndex:


up() {  this.currentIndex = this.currentIndex === 0    ? this.tasks.length - 1    : this.currentIndex - 1;}down() {  this.currentIndex = this.currentIndex === this.tasks.length - 1    ? 0    : this.currentIndex + 1;}

В метод interactive добавляем обработку стрелок:


if (key === "\u001B\u005B\u0041" || key === "\u001B\u005B\u0044") { // вверх или влево  this.up();} else if (key === "\u001B\u005B\u0042" || key === "\u001B\u005B\u0043") { // вниз или вправо  this.down();}

Аналогичным образом добавляем возможность отмечать выполненные задачи:


toggle(index: number = this.currentIndex) {  const task = this.tasks[index];  if (task) {    task.isDone = !task.isDone;    this.save();  }}

if (key === "d" || key === " ") {    this.toggle();} else if ('0' <= key && key <= '9') {    this.toggle(parseInt(key, 10));} 

Дорабатываем удаление:


remove(index: number = this.currentIndex) {  if (index === this.tasks.length - 1) {    this.up();  }  this.tasks.splice(index, 1);  this.save();}

if (key === "r") {    this.remove();} 

И, наконец, редактирование:


if (key === "e") {  if (!this.tasks[this.currentIndex]) {    return;  }  const title = await prompt("Edit task (" + this.tasks[this.currentIndex].title + "): ");  if (title) {    this.edit(this.currentIndex, title);  }}

Проверяем работу приложения:


deno run --allow-read --allow-write --allow-env --unstable ./todo.ts

или


./todo.ts

Установка


Для того что бы наша программа была доступна в терминале, воспользуемся замечательной командой deno install.


deno install --allow-read --allow-write --allow-env --unstable ./todo.ts

Установка, так же как и запуск, может производиться по URL.


deno install --allow-read --allow-write --allow-env --unstable  https://raw.githubusercontent.com/dmitriytat/todo/master/todo.ts

Имя файла будет использовано как название программы по умолчанию, но с помощью опции -n/--name можно задать другое.


После чего todo можно использовать в командной строке.


Импорт зависимостей


Последняя фича, которую мы использовали, но ещё не обсудили импорт зависимостей. В отличие от Node, в Deno подход к импортированию несколько иной: предполагается, что зависимости импортируются по URL. В этом подходе, на мой взгляд, есть как достоинства, так и недостатки.


При разработке однофайловых скриптов, возможность импортировать по URL является огромным плюсом, но как только мы разработаем что-то большее, уследить за версиями станет труднее.


Для управления сложностью существует два основных решения: файл dep.ts и файл с картой импортов.


В первом случае мы делаем реэкспорт библиотек, собирая все импорты в одном месте.


export { green, red, yellow, bold } from "https://deno.land/std@0.55.0/fmt/colors.ts";export { decode, encode } from "https://deno.land/std@v0.55.0/encoding/utf8.ts";export { clearDown, goUp, goLeft } from "https://denopkg.com/iamnathanj/cursor@v2.0.0/mod.ts"; 

Во втором случае в JSON файле мы пишем alias:


import_map.json


{   "imports": {      "fmt/": "https://deno.land/std@0.55.0/fmt/"   }}

color.ts


import { red } from "fmt/colors.ts";console.log(red("hello world"));

При запуске программы указываем путь до карты и флаг --unstable, так как фича не готова для продакшена.


deno run --importmap=import_map.json --unstable color.ts.

Резюме


В целом, работой Deno я доволен. Новый инструмент позволяет не тратить время на настройку окружения и сразу приступать к написанию кода, что оказалось очень удобно.
Таким образом, я могу рекомендовать Deno для написания небольших скриптов и программ, упрощающих жизнь разработчика, но пока воздержался бы от использования Deno для создания продакшен-приложений.

Источник: habr.com
К списку статей
Опубликовано: 23.06.2020 10:15:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании funcorp

Javascript

Typescript

Программирование

Deno

Stdout

Stdin

Todo list

Cli

Terminal

Категории

Последние комментарии

© 2006-2020, personeltest.ru