한지 v0.0.1

저는 1년 넘게 오픈 소스 프로젝트 중 2개를 위한 CLI 도구를 구축해 왔습니다. 나는 팀에 React 개발자가 있기 때문에 Ink로 시작했고 우리는 빠르게 무언가를 만들 수 있고 목록을 말하는 것이 오해의 소지가 있는 것으로 판명될 것이라는 인상을 받았습니다.

우리는 Inquirer, Enquirer, Prompts 및 Ink를 시도했지만 어떤 이유로 사용자 정의할 수 있는 단일 라이브러리가 없습니다. 모든 도서관은 이동할 공간 없이 특정 시나리오에 거의 초점을 맞추지 않습니다.

나는 라이브러리 내부를 파헤치는 데 시간을 보냈고 라이브러리의 핵심이 매우 단순하다는 것을 알게 되었습니다.



라이브러리 개발자로서 저는 비하인드 스토리의 95%를 처리할 수 있으며 사용자가 원하는 방식으로 텍스트를 렌더링하여 무한한 CLI 디자인을 위한 공간을 남겨둘 수 있습니다.

이것이 stdin , stdoutreadline 를 얻는 방법입니다.

  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);
};


디자인 없는 툴킷을 구축하는 동안 나는 여전히 사용자에게 가능한 한 많은 유틸리티를 제공하기를 원했기 때문에 select와 같은 도메인 특정 종류의 입력에 대해 StateWrappers를 구현하기로 결정했습니다.



이것이 select state wrapper의 모습입니다. 아래는 간단한 문자열 배열을 위한 것으로, updown 키 누르기를 처리하고 선택된 인덱스를 추적하고 범위를 벗어날 때마다 반복합니다.

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;
  }
}


라이브러리 정의Prompt API입니다.

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

적절한 CTRL+C 지원 및 API 단순화와 함께 v0.0.2를 곧 중단할 예정입니다.

당신은 트위터에서 계속 지켜볼 수 있습니다-

좋은 웹페이지 즐겨찾기