perryrh0dan/taskline

View on GitHub
src/renderer.ts

Summary

Maintainability
F
3 days
Test Coverage
import { Signale, SignaleConstructorOptions } from '@perryrh0dan/signale';
import { addWeeks, isBefore, endOfDay, toDate } from 'date-fns';
import * as figures from 'figures';
import ora = require('ora');

const chalk = require('chalk');

import { Config } from './config';
import { Localization, format } from './localization';
import { Item } from './item';
import { TaskPriority, Task } from './task';
import { getRelativeHumanizedDate } from './libs/date';
import { Note } from './note';

const { underline } = chalk;

type Theme = {
  colors: {
    pale: string,
    task: {
      priority: {
        medium: string,
        high: string
      }
    },
    icons: {
      note: string,
      success: string,
      star: string,
      progress: string,
      pending: string,
      canceled: string
    }
  }
}

export class Renderer {
  private static _instance: Renderer;
  private spinner: ora.Ora;
  private signale: any;
  private theme: Theme;

  public static get instance(): Renderer {
    if (!this._instance) {
      this._instance = new Renderer();
    }

    return this._instance;
  }

  private constructor() {
    this.spinner = ora();
    this.loadTheme();
    this.configureSignale();
  }

  private loadTheme(): void {
    this.theme = this.configuration.theme;
  }

  private configureSignale(): void {
    const signaleOptions: SignaleConstructorOptions = {
      config: {
        displayLabel: false
      },
      types: {
        note: {
          badge: figures.bullet,
          color: this.getColorValue('icons.note'),
          label: 'note'
        },
        success: {
          badge: figures.tick,
          color: this.getColorValue('icons.success'),
          label: 'success'
        },
        star: {
          badge: figures.star,
          color: this.getColorValue('icons.star'),
          label: 'star'
        },
        await: {
          badge: figures.ellipsis,
          color: this.getColorValue('icons.progress'),
          label: 'awaiting'
        },
        pending: {
          badge: figures.checkboxOff,
          color: this.getColorValue('icons.pending'),
          label: 'pending'
        },
        fatal: {
          badge: figures.cross,
          color: this.getColorValue('icons.canceled'),
          label: 'error'
        }
      }
    };

    this.signale = new Signale(signaleOptions);
  }

  private getColorType(color: string): string {
    if (color.includes('rgb')) {
      return 'rgb';
    } else if (color.includes('#')) {
      return 'hex';
    } else {
      return 'keyword';
    }
  }

  private getColorValue(type: string): string {
    const configValue = type.split('.').reduce((p: any, prop: any) => { return p[prop]; }, this.theme.colors);
    const defaultValue = type.split('.').reduce((p: any, prop: any) => { return p[prop]; }, Config.instance.getDefault().theme.colors);
    switch (this.getColorType(configValue)) {
      case 'rgb':
        return configValue;
      case 'hex':
        return configValue;
      case 'keyword':
        if (typeof chalk[configValue] === 'function') {
          return configValue;
        }
        break;
    }
    return defaultValue;
  }

  private getColorMethod(type: string): Function {
    const color = this.getColorValue(type);
    let result;
    switch (this.getColorType(color)) {
      case 'rgb':
        result = /rgb\((\d*),(\d*),(\d*)\)/.exec(color);
        if (!result) break;
        const red = result[1];
        const green = result[2];
        const blue = result[3];
        return chalk.rgb(red, green, blue);
      case 'hex':
        result = /#(\w{6})/.exec(color);
        if (!result) break;
        const hex = result[0];
        return chalk.hex(hex);
    }
    return chalk[color];
  }

  private printColor(type: string, value: string): string {
    return this.getColorMethod(type)(value);
  }

  private get configuration(): any {
    return Config.instance.get();
  }

  private colorBoards(boards: any): any {
    return boards.map((x: any) => this.printColor('pale', x)).join(' ');
  }

  private isBoardComplete(items: Array<Item>): boolean {
    const { tasks, complete, notes } = this.getItemStats(items);
    return tasks === complete && notes === 0;
  }

  private getAge(birthday: number): string {
    const daytime: number = 24 * 60 * 60 * 1000;
    const age: number = Math.round(Math.abs(birthday - Date.now()) / daytime);
    return age === 0 ? '' : this.printColor('pale', `${age}d`);
  }

  private getDueDate(dueTimestamp: number): string {
    const now = new Date();
    const dueDate = toDate(dueTimestamp);

    const humanizedDate = getRelativeHumanizedDate(dueDate);
    const text = `(${humanizedDate})`;

    const isSoon = isBefore(dueDate, addWeeks(now, 1));
    const isUrgent = isBefore(dueDate, endOfDay(now));

    if (isUrgent) {
      return this.printColor('task.priority.high', underline(text));
    }

    if (isSoon) {
      return this.printColor('task.priority.medium', text);
    }

    return this.printColor('pale', text);
  }

  private getPassedTime(passedTime: number): string {
    const seconds = passedTime / 1000;
    const minutes = Math.floor((seconds / 60) % 60);
    const hours = Math.floor(seconds / 3600);

    return `${hours + Math.round(minutes/60 * 100) / 100} hours`;
  }

  private getCorrelation(items: Array<Item>): string {
    const { tasks, complete } = this.getItemStats(items);
    return this.printColor('pale', `[${complete}/${tasks}]`);
  }

  private getItemStats(items: Array<Item>): any {
    let [tasks, complete, notes] = [0, 0, 0];

    items.forEach((item: Item) => {
      if (item.isTask) {
        tasks++;
        if (item instanceof Task && item.isComplete) {
          return complete++;
        }
      }

      return notes++;
    });

    return {
      tasks,
      complete,
      notes
    };
  }

  private getStar(item: Item): string {
    return item.isStarred ? this.printColor('icons.star', '★') : '';
  }

  private buildTitle(key: string, items: Array<Item>): any {
    const title =
      key === new Date().toDateString() ? `${underline(key)} ${this.printColor('pale', `[${Localization.instance.get('date.today')}]`)}` : underline(key);
    const correlation = this.getCorrelation(items);
    return {
      title,
      correlation
    };
  }

  private buildPrefix(item: Item): string {
    const prefix: Array<string> = [];

    prefix.push(' '.repeat(4 - String(item.id).length));
    prefix.push(this.printColor('pale', `${item.id}.`));

    return prefix.join(' ');
  }

  private getTaskColorMethod(task: Task): any {
    if (task.priority === 2) {
      return this.getColorMethod('task.priority.medium');
    } else {
      return this.getColorMethod('task.priority.high');
    }
  }

  private buildMessage(item: Item): string {
    let message: string = '';

    if (item instanceof Task) {
      message = this.buildTaskMessage(item);
    } else if (item instanceof Note) {
      message = this.buildNoteMessage(item);
    }

    return message;
  }

  private buildNoteMessage(item: Note): string {
    return item.description;
  }

  private buildTaskMessage(item: Task): string {
    const message: Array<string> = [];

    if (!item.isComplete && !item.isCanceled && item.priority > 1) {
      message.push(this.getTaskColorMethod(item).underline(item.description));
    } else if (item.isComplete || item.isCanceled) {
      message.push(this.printColor('pale', item.description));
    } else {
      message.push(item.description);
    }

    if (!item.isComplete && !item.isCanceled && item.priority == 2) {
      message.push(this.printColor('task.priority.medium', '(!)'));
    } else if (!item.isComplete && !item.isCanceled && item.priority == 3) {
      message.push(this.printColor('task.priority.high', '(!!)'));
    }

    return message.join(' ');
  }

  private displayTitle(board: string, items: Array<Item>): void {
    const { title: message, correlation: suffix } = this.buildTitle(
      board,
      items
    );
    const titleObj = {
      prefix: '\n ',
      message,
      suffix
    };

    return this.signale.log(titleObj);
  }

  private displayItemByBoard(item: Item): void {
    const age = this.getAge(item.timestamp);
    let dueDate;
    if (item instanceof Task && item.dueDate !== 0 && !item.isComplete) {
      dueDate = this.getDueDate(item.dueDate);
    }

    let passedTime;
    if (item instanceof Task && item.passedTime > 0) {
      passedTime = this.getPassedTime(item.getRealPassedTime());
    }

    const star = this.getStar(item);

    const prefix = this.buildPrefix(item);
    const message = this.buildMessage(item);
    let suffix;
    if (dueDate) {
      suffix = `${dueDate} ${star}`;
    } else {
      suffix = age.length === 0 ? star : `${age} ${star}`;
    }

    if (passedTime) {
      suffix += ` (${passedTime})`;
    }

    suffix = suffix.replace('  ', ' ');

    const msgObj = {
      prefix,
      message,
      suffix
    };

    if (item instanceof Task) {
      return item.isComplete ? this.signale.success(msgObj) : item.inProgress ? this.signale.await(msgObj) : item.isCanceled ? this.signale.fatal(msgObj) : this.signale.pending(msgObj);
    }

    return this.signale.note(msgObj);
  }

  private displayItemByDate(item: Item): void {
    const boards = item.boards.filter((x: string) => x !== 'My Board');
    const star = this.getStar(item);

    const prefix = this.buildPrefix(item);
    const message = this.buildMessage(item);
    const suffix = `${this.colorBoards(boards)} ${star}`;

    const msgObj = {
      prefix,
      message,
      suffix
    };

    if (item instanceof Task) {
      return item.isComplete ? this.signale.success(msgObj) : item.inProgress ? this.signale.await(msgObj) : item.isCanceled ? this.signale.fatal(msgObj) : this.signale.pending(msgObj);
    }

    return this.signale.note(msgObj);
  }

  public startLoading(): void {
    this.spinner.start();
  }

  public stopLoading(): void {
    this.spinner.stop();
  }

  public displayByBoard(data: any): void {
    this.stopLoading();
    Object.keys(data).forEach((board: string) => {
      if (
        this.isBoardComplete(data[board]) &&
        !this.configuration.displayCompleteTasks
      ) {
        return;
      }

      this.displayTitle(board, data[board]);
      data[board].forEach((item: Item) => {
        if (item instanceof Task && item.isComplete && !Config.instance.get().displayCompleteTasks) {
          return;
        }

        this.displayItemByBoard(item);
      });
    });
  }

  public displayByDate(data: any): void {
    this.stopLoading();
    Object.keys(data).forEach((date: string) => {
      if (
        this.isBoardComplete(data[date]) &&
        !Config.instance.get().displayCompleteTasks
      ) {
        return;
      }

      this.displayTitle(date, data[date]);
      data[date].forEach((item: Item) => {
        if (
          item instanceof Task &&
          item.isTask &&
          item.isComplete &&
          !Config.instance.get().displayCompleteTasks
        ) {
          return;
        }

        this.displayItemByDate(item);
      });
    });
  }

  public displayStats(percent: number, complete: number, canceled: number, inProgress: number, pending: number, notes: number): void {
    if (!Config.instance.get().displayProgressOverview) {
      return;
    }

    const percentText = percent >= 75 ? this.printColor('icons.success', `${percent}%`) : percent >= 50 ? this.printColor('task.priority.medium', `${percent}%`) : `${percent}%`;

    const status: Array<string> = [
      `${this.printColor('icons.success', complete.toString())} ${this.printColor('pale', Localization.instance.get('stats.done'))}`,
      `${this.printColor('icons.canceled', canceled.toString())} ${this.printColor('pale', Localization.instance.get('stats.canceled'))}`,
      `${this.printColor('icons.progress', inProgress.toString())} ${this.printColor('pale', Localization.instance.get('stats.progress'))}`,
      `${this.printColor('icons.pending', pending.toString())} ${this.printColor('pale', Localization.instance.get('stats.pending'))}`,
      `${this.printColor('icons.note', notes.toString())} ${this.printColor('pale', Localization.instance.get('stats.note', { type: notes === 1 ? 0 : 1 }))}`
    ];

    if (complete !== 0 && inProgress === 0 && pending === 0 && notes === 0) {
      this.signale.log({
        prefix: '\n ',
        message: Localization.instance.get('stats.allDone'),
        suffix: this.printColor('icons.star', '★')
      });
    }

    if (pending + inProgress + canceled + complete + notes === 0) {
      return console.log(Localization.instance.get('help.default'));
    }

    this.signale.log({
      prefix: '\n ',
      message: this.printColor('pale', Localization.instance.getf('stats.percentage', { params: [percentText] }))
    });

    this.signale.log({
      prefix: ' ',
      message: status.join(this.printColor('pale', ' · ')),
      suffix: '\n'
    });
  }

  public displayConfig(config: any, path: string): void {
    this.signale.log({
      prefix: ' ',
      message: chalk.green(Localization.instance.getf('config.path', { params: [path] }))
    });

    this.signale.log({
      prefix: ' ',
      message: `#### ${Localization.instance.get('config.title')} ####`
    });

    this.iterateObject(config, 0);
  }

  private iterateObject(obj: any, depth: number): void {
    Object.keys(obj).forEach(key => {
      if (typeof obj[key] !== 'object') {
        // Dont show privat key
        let text: string = obj[key].toString() ? obj[key].toString() : '';
        if (text.includes('PRIVATE KEY')) {
          text = text.slice(0, 50).replace('\n', ' ').concat('...');
        }
        this.signale.log({
          prefix: ' ',
          message: `${key}: ${text}`
        });
      } else {
        this.iterateObject(obj[key], depth++);
      }
    });
  }

  public markComplete(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message: string = Localization.instance.getf('success.check', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markInComplete(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message: string = Localization.instance.getf('success.uncheck', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markStarted(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.start', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markPaused(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.pause', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markCanceled(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.cancel', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markRevived(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.revive', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markStarred(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.star', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public markUnstarred(ids: Array<number>): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.unstar', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public invalidCustomAppDir(path: string): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('warning.appDir', { params: [this.printColor('error', path)] });

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidFirestoreConfig(): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.get('warning.firestoreConfig');

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidLanguageFile(): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.get('Unable to load language file');

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidID(id: number): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('warning.id', { params: [this.printColor('pale', id.toString())] });

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidIDRange(range: string): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('warning.idRange', { params: [this.printColor('pale', range)] });

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidPriority(): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.get('warning.priority');

    this.signale.error({
      prefix,
      message
    });
  }

  public invalidDateFormat(date: string): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('warning.dateFormat', { params: [this.printColor('pale', date)] });

    this.signale.error({
      prefix,
      message
    });
  }

  public successCreate(item: Item): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('success.create', { type: item.isTask ? 0 : 1, params: [this.printColor('pale', item.id.toString())] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successEdit(id: number): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('success.edit', { params: [this.printColor('pale', id.toString())] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successDelete(ids: Array<number>): void {
    this.stopLoading();

    const prefix = '\n';
    const message: string = Localization.instance.getf('success.delete', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successMove(ids: Array<number>, boards: Array<string>): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('success.move', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', ')), this.printColor('pale', boards.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successPriority(ids: Array<number>, priority: TaskPriority): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix: string = '\n';
    const text = Localization.instance.get('success.priority', { type: ids.length > 1 ? 1 : 0 });
    const prioText = priority === 3 ? this.printColor('task.priority.high', TaskPriority[priority]) : priority === 2 ? this.printColor('task.priority.medium', TaskPriority[priority]) : this.printColor('icons.success', TaskPriority[priority]);
    const message = format(text, [this.printColor('pale', ids.join(', ')), prioText]);

    this.signale.success({
      prefix,
      message
    });
  }

  public successDueDate(ids: Array<number>, dueDate: Date): void {
    this.stopLoading();
    if (ids.length === 0) {
      return;
    }

    const prefix = '\n';
    const message = Localization.instance.getf('success.duedate', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', ')), dueDate] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successRestore(ids: Array<number>): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('success.restore', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successCopyToClipboard(ids: Array<number>): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.getf('success.clipboard', { type: ids.length > 1 ? 1 : 0, params: [this.printColor('pale', ids.join(', '))] });

    this.signale.success({
      prefix,
      message
    });
  }

  public successRearrangeIDs(): void {
    this.stopLoading();

    const prefix = '\n';
    const message = Localization.instance.get('success.rearrange');

    this.signale.success({
      prefix,
      message
    });
  }
}