Hanji v0.0.1

Уже больше года я создаю CLI-инструменты для двух своих проектов с открытым исходным кодом. Я начал с Ink, так как у меня в команде есть разработчики React, и мы были под впечатлением, что он позволит нам быстро создавать вещи, но это оказалось обманчивым, если не сказать больше.

Мы попробовали Inquirer, Enquirer, Prompts и Ink, и по какой-то причине нет ни одной библиотеки, которая позволила бы вам настраивать материал. Каждая библиотека практически сфокусирована на определенном сценарии, не давая вам возможности двигаться дальше.

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

Как разработчик библиотеки, я могу справиться с 95% закулисных вещей и позволить пользователю выводить текст так, как он хочет, оставляя пространство для безграничного дизайна CLI.

Вот как вы получаете stdin, stdout и readline

  const stdin = process.stdin;
  const stdout = process.stdout;

  const readline = require("readline");
  const rl = readline.createInterface({
    input: stdin,
    escapeCodeTimeout: 50,
  });

  readline.emitKeypressEvents(stdin, rl);
Вход в полноэкранный режим Выход из полноэкранного режима

теперь мы можем прослушивать все события нажатия клавиш

// keystrokes are typed
type Key = {
  sequence: string;
  name: string | undefined;
  ctrl: boolean;
  meta: boolean;
  shift: boolean;
};

const keypress = (str: string | undefined, key: Key) => {
  // handle keypresses
}

stdin.on("keypress", keypress);

// whenever you're done, you just close readline
readline.close()
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь для вывода текста мы просто выводим его в stdout

let previousText = "";
stdout.write(clear(previousText, stdout.columns));

stdout.write(string);
previousText = string;

// here's how you clear cli
const strip = (str: string) => {
  const pattern = [
    "[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)",
    "(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PRZcf-ntqry=><~]))",
  ].join("|");

  const RGX = new RegExp(pattern, "g");
  return typeof str === "string" ? str.replace(RGX, "") : str;
};

const stringWidth = (str: string) => [...strip(str)].length;

export const clear = function (prompt: string, perLine: number) {
  if (!perLine) return erase.line + cursor.to(0);

  let rows = 0;
  const lines = prompt.split(/r?n/);
  for (let line of lines) {
    rows += 1 + Math.floor(Math.max(stringWidth(line) - 1, 0) / perLine);
  }

  return erase.lines(rows);
};
Вход в полноэкранный режим Выход из полноэкранного режима

При создании инструментария без дизайна я все еще стремлюсь предоставить пользователю как можно больше полезностей, поэтому я решил реализовать StateWrappers для специфичных для домена видов Inputs, таких как select.

вот как будет выглядеть обертка состояния select. Приведенный ниже вариант предназначен для простого массива строк, он обрабатывает нажатия клавиш up и down, отслеживает индекс selected и зацикливает его, когда он выходит за границы:

export class SelectState {
  public selectedIdx = 0;
  constructor(public readonly items: string[]) {}

  consume(str: string | undefined, key: AnyKey): boolean {
    if (!key) return false;

    if (key.name === "down") {
      this.selectedIdx = (this.selectedIdx + 1) % this.items.length;
      return true;
    }

    if (key.name === "up") {
      this.selectedIdx -= 1;
      this.selectedIdx =
        this.selectedIdx < 0 ? this.items.length - 1 : this.selectedIdx;
      return true;
    }

    return false;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это API Prompt, определенный библиотекой.

export abstract class Prompt<RESULT> {
  protected terminal: ITerminal | undefined;

  protected requestLayout() {
    this.terminal!.requestLayout();
  }

  attach(terminal: ITerminal) {
    this.terminal = terminal;
    this.onAttach(terminal);
  }

  detach(terminal: ITerminal) {
    this.onDetach(terminal);
    this.terminal = undefined;
  }

  onInput(str: string | undefined, key: AnyKey) {}

  abstract result(): RESULT;
  abstract onAttach(terminal: ITerminal): void;
  abstract onDetach(terminal: ITerminal): void;
  abstract render(status: "idle" | "submitted" | "aborted"): string;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь все, что вам, как разработчику, нужно сделать, это определить, как отображать элемент select и не беспокоиться об управлении состоянием и нажатием клавиш, вы просто оставляете это библиотеке и заменяете ее пользовательскими реализациями, когда это необходимо.

export class Select extends Prompt<{ index: number; value: string }> {
  private readonly data: SelectState;

  constructor(items: string[]) {
    super();
    this.data = new SelectState(items);
  }

  onAttach(terminal: ITerminal) {
    terminal.toggleCursor("hide");
  }

  onDetach(terminal: ITerminal) {
    terminal.toggleCursor("show");
  }

  override onInput(str: string | undefined, key: any) {
    super.onInput(str, key);
    const invlidate = this.data.consume(str, key);
    if (invlidate) {
      this.requestLayout();
      return;
    }
  }

  render(status: "idle" | "submitted" | "aborted"): string {
    if (status === "submitted" || status === "aborted") {
      return "";
    }

    let text = "";
    this.data.items.forEach((it, idx) => {
      text +=
        idx === this.data.selectedIdx ? `${color.green("❯ " + it)}` : `  ${it}`;
      text += idx != this.data.items.length - 1 ? "n" : "";
    });
    return text;
  }

  result() {
    return {
      index: this.data.selectedIdx,
      value: this.data.items[this.data.selectedIdx]!,
    };
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

const result = await render(new Select(["user1", "user2" ...]))
Вход в полноэкранный режим Выход из полноэкранного режима

Я потратил немного времени в выходные и опубликовал v0.0.1.
вы можете попробовать — https://www.npmjs.com/package/hanji

Скоро я собираюсь выпустить v0.0.2 с поддержкой CTRL+C и упрощением API.

Вы можете следить за новостями на twitter — https://twitter.com/_alexblokh

Оцените статью
devanswers.ru
Добавить комментарий