User Guide

This guide describes the most common usecases for using chipmunk.

Searching and Filtering

Searching through huge logfiles

Bookmarks

Add bookmarks to mark and remember important log entries. Jump between bookmarks with shortcuts (j and k).

Commenting

Comment line(s) of the log to have additional information about specific places in the file.

Charts

To better understand what's going on in a large logfile, it can be helpful to visualize data over time. chipmunk let's you define regular expressions that match a number and to use this expression to capture a value throughout a logfile.

Concatenating logfiles

chipmunk can combine multiple log file. This is useful for example when you just want to reassamble a logfile that were stored in parts.

Merging

Merging is useful if you have several log files e.g. from different devices/processors and you want combine them by merging according to their timestamps.

Time Ranges

To measure how much time passed between lines of a logfile chipmunk provides the Time Range Feature.

DLT - Diagnostic Log and Trace

View and search and filter DLT files. Programming in Rust!

Keyboard shortcuts

An overview of all the keyboard shortcuts.

Command line

It's also possible to open a file with Chipmunk directly from the console.

Searching with Filters

Of course multiple searches are supported and filters can be saved and restored.

Create filter

Type a search request at the bottom and click the floppy disc icon next to the search input.

Keyboard shortcut: Enter + Enter or Enter + CTRL/CMD (depending on the OS)

Edit filter

Created filters can be modified afterwards by simply right-clicking on the desired filter and selecting Edit.

When the edit is done press Enter to apply the changes.

Remove filter

Filters can be removed one by one by right-clicking on the filter and selecting Remove.

Another way is to drag&drop the filter on the bin icon at the bottom of the sidebar which appears as soon as the filter is picked up and moved around.

To remove all created filters at once right-click on any filter and select Remove all.

Convert into chart

If a filter needs to be converted into a chart, right-click on the filter and select Convert To Chart.

Another way to convert a filter into a chart is to drag&drop the filter in the Charts section on the sidebar (only visible when at least one chart is already created).

Note: This option is only available for filters that consist of regular expressions.

Create Time Range

By selecting two or more filters time ranges can be created. To select multiple filters, hold SHIFT while left-clicking on the desired filters. After selecting the filters right-click on one of the selected filters and select Create Time Range.

Note: More about Time Range here

En-/Disable filter

In search results

Filters can be en-/disabled in the search results by un-/checking the checkbox next to the corresponding filter.

In search results and output

Filters can also be en-/disabled completely by right-clicking the corresponding filter and select Disable.

If the disabled area is visible due to a filter or any other search component is disabled, the filter can be dragged and dropped into the area directly.

Save and load filters

To save the created filters and other search settings click on the Save button in the Manage section on the sidebar.

To load previously created filters and other search settings click on the Load button in the Manage section on the sidebar.

Bookmarks

Bookmarks can be used to pin down log entries that are important to the user.

bookmarks

Lines that are bookmarked will stick in the result view.

Commenting

There might be cases when you want to add a remark to a few places in the log. Simply mark lines, right-click and select Comment to add a comment to the specified lines.

Reply to comments

In addition to creating comment, you can also reply to comments with no limit.

Colors

By changing the color of a comment the priority changes. The priority order is defined by default and cannot be changed. It is also possible to display only one specific color of comments.

Charts

Getting a quick overview of what happened during a 24h-trace period can be daunting if the logfiles are huge (millions of log entries). A very neat way to get a quick overview is to show some graphs for what is happening.

In this graph we captured processor workload for different cores. Any numerical value can be captured by using a regular expression with a group.

Supported are

  • integers
  • floats

An example for a regular expression with such a capture group looks like this:

measured:\s(\d+)

This will match measured: 42 and pull out the value 42. Here is an example of how this looks in action.

Create chart

Type a search request at the bottom and click the graph icon next to the search input.

Keyboard shortcut: Shift + Enter

Edit chart

Created charts can be modified afterwards by simply right-clicking on the desired chart and selecting Edit.

When the edit is done press Enter to apply the changes.

Remove chart

Charts can be removed one by one by right-clicking on the chart and selecting Remove.

Another way is to drag&drop the chart on the bin icon at the bottom of the sidebar which appears as soon as the chart is picked up and moved around.

To remove all created charts at once right-click on any chart and select Remove all.

Convert to filter

If a chart needs to be converted into a chart, right-click on the chart and select Convert To Filter.

Another way to convert a chart into a filter is to drag&drop the chart in the Filters section on the sidebar.

En-/Disable chart

In search results

Charts can be en-/disabled in the search results by un-/checking the checkbox next to the corresponding chart.

In search results and output

Charts can also be en-/disabled completely by right-clicking the corresponding chart and select Disable.

If the disabled area is visible due to a chart or any other search component is disabled, the chart can be dragged and dropped into the area directly.

Save and load charts

To save the created charts and other search settings click on the Save button in the Manage section on the sidebar.

To load previously created charts and other search settings click on the Load button in the Manage section on the sidebar.

Assembling files

There is handy support for combining multiple files into one view. You simple drag & drop the files you need into a fresh tab. Then select the concatenate option.

Now you will have the chance to quickly check for a search expression to see if it is present in the dropped files. Here you can potentially include or exclude files.

Merging

To help developers to deal with multiple logfiles, chipmunk can automatically detect timestamps and merge logs from multiple files sorting the lines of the files by their timestamp.

Here is an animation to show how it works:

To merge files either drag and drop multiple files into the output window of chipmunk

NOTE: It's possible to drag and drop additional files onto the dialog window.

Another option is to open the merging tab on the sidebar and click on Add file(s).

NOTE: You can select multiple files at once by clicking Add file(s)

Once the files are added, you have an overview of all added files and their filesize along with the detected datetime formats.

By left-clicking one of the files the details are being shown. The details display the content of the file along with the datetime format which colorizes the components to identify them easier in the content window.

The format can also be modfied by hand along with adding a year. Another option is to set the offset of a file after what time in ms the selected file should be included.

When the configuration and selection of the files is done, just press Merge.

NOTE: In the output window you can still see which line is from which file by the colors or hover with the mouse over them which will show the filename.

Time Range

Chipmunk provides the measurement of time that passed between lines and makes it possible to compare them in a chart.

Create and remove time ranges

Start time range

By right-clicking on a line either select the option Open time measurement view or when it already is open Start Time Range.

Add time range

If you want to another time range to the current selection right-click and select Add time range <line begin>-<line end>.

Close time range

The currently opened time range can be closed by right-clicking and selecting Close time range <line begin>-<line end>.

Remove time range

Time ranges can easily be removed by right-clicking on a line which is part of a time range select:

  • Remove a single range: Remove this range
  • Remove all except the selected range: Remove all except selected
  • Remove all: Remove all ranges

Analyze time ranges

In the tabs below select Time Measurement to see all of the created ranges.

On the bottom left the format to detect timestamps is shown. On the bottom right the ... symbol offers options to add, remove and set default timestamp formats.

View modes

There are two modes available on viewing the charts:

  1. Scaled (default)

Sorts the ranges by their position in the logfile and lets you scale on the ranges.

Scrolling inside the scaler changes the scaling of the view. By holding left-mouse in the upper part of the scaler, the scaler can be moved to view specific parts of the created ranges. By holding left-mouse in the lower part of the scaler, the scaler size can be change to that specific part and that marked size.

  1. Aligned

Aligns all ranges to the left and sorts ranges by their position in the logfile.

NOTE: To change the view mode right-click anywhere in the Time Measurement area and select Switch to: ...

DLT support

The Diagnostic Log and Trace AUTOSAR format is widely used in the automotive industry and is a binary log format. chipmunk can understand and process DLT content in large quantities.

Import file

When opening a dlt file, you are prompted with this dialog. Here you can also provide the path to a FIBEX file that contains descriptions for you non-verbose messages.

This can be expanded so can select what components or loglevels you want to include. Note that you are presented with a statistics of how many log messages exist e.g. for a component with a certain log level.

The columns can be configured by right-clicking in one of the column titles or hover over one of the column titles and left-click the .... Then you can filter out columns and adjust colors.

Keyboard Shortcuts

Important to most developers: a good and intuitive set of keyboard shortcuts.

Just hit ? and you should get the overview documentation.

Command Line

Here's a way to open a file with Chipmunk directly from the command line.

Run Chipmunk and open the File section of the toolbar at the top and install cm.

NOTE: This only needs to be done once

Now you're ready to go!

Simply open the command line of your choice and type:

cm <filename>

Extending Chipmunk with plugins

In this section everything about plugins will be explained as well as how to create custom plugins for Chipmunk.

About plugins

In computing, a plug-in (or plugin, add-in, addin, add-on, or addon) is a software component that adds a specific feature to an existing computer program. When a program supports plug-ins, it enables customization.[1]

Plugins in Chipmunk extend the default functionalities making it possible to receive and analyze data from different kind of sources (e.g. serial connections).

Plugin structure

Chipmunk plugins mainly consist of a render and a process part.

Render

The render part is responsible for the visual part of the plugin itself. With the help of Angular components are created using Typescript, HTML and CSS. The component will be automatically included in Chipmunk after building the plugin.

Process

The process part is responsible for the background processing of the plugin and modifying the output stream.

Chipmunk Store

The Chipmunk Store provides different plugins to install on Chipmunk. By simply clicking on the desired plugin and then on install will add the plugin to Chipmunk. The Chipmunk Store also provides the option to upgrade the already installed plugin to a newer version.

References


[1] https://en.wikipedia.org/wiki/Plug-in_(computing)

Create a plugin

This section provides a step by step guide on how to get Chipmunk and the Quickstart repository as well as how to build them. The repository Quickstart provides a variety of plugins which can be copied and modifed to create new plugins.

Installation of plugins and Chipmunk

First off clone the create a folder for Chipmunk and Quickstart by typing the following commands:

mkdir chipmunk-developing
cd chipmunk-developing

After creating the folder, clone both the Chipmunk and the Quickstart repository into the folder and navigate into it.

git clone https://github.com/esrlabs/chipmunk.git
git clone https://github.com/esrlabs/chipmunk-quickstart.git
cd chipmunk-quickstart

The Quickstart repository has a few examples of plugins which are located in the folder plugin:

└── plugins
    ├── plugin.helloworld
    ├── plugin.row.columns
    ├── plugin.row.parser
    ├── plugin.selection.parser
    └── plugin.sh

To build the plugin, type the following command with the path to the target plugin:

rake build[./plugins/plugin.helloworld]

Optionally you can define a version of your plugin:

rake build[./plugins/plugin.helloworld, 1.0.1]

WINDOWS Note: To call a rake task with multiple arguments, it could be command should be wrapped as string rake 'build[./plugins/plugin.helloworld, 1.0.1]'

Note: The very first build always takes some noticeable time because of build script downloads and compiles necessary infrastructure.

As a result the folder releases will be created with the compiled plugin:

├── plugins
│    ├── plugin.helloworld
│    ├── plugin.row.columns
│    ├── plugin.row.parser
│    ├── plugin.selection.parser
│    └── plugin.sh
└── releases
     └── plugin.helloworld

To build Chipmunk run the following commands to navigate into the folder where the repository was copied:

rake full_pipeline

The building of Chipmunk will take some time but only needs to be built once. After the build is finished Chipmunk can be executed from the releases folder.

Before starting Chipmunk some environment variables need to be passed in as a preparation.

First off define a full path to the folder that will hold the release of your plugin.

export CHIPMUNK_PLUGINS_SANDBOX=../chipmunk-quickstart/releases

By default Chipmunk stores plugins the home folder in .chipmunk/plugins but by running the command above it can be modified.

To prevent the installation of default plugins, run the following command

export CHIPMUNK_PLUGINS_NO_DEFAULTS=true

If your has plugin already been published in the Chipmunk Store these commands would prevent it from updating:

export CHIPMUNK_PLUGINS_NO_UPGRADE=true
export CHIPMUNK_PLUGINS_NO_UPDATES=true

To enable logs in the Console making it easier to debug:

export CHIPMUNK_DEV_LOGLEVEL=ENV

Now Chipmunk is ready to be executed.

Rake commands

The following Rake commands are vital for the compilation of Chipmunk, which is why in this section the most important rake commands are going to be mentioned and described.

rake start                                          # Start Chipmunk
rake full_pipeline                                  # Build Chipmunk (whole application)
rake build[./plugins/plugin.helloworld]             # Build plugin
rake build[./plugins/plugin.helloworld, 1.0.1]      # Build plugin with version
rake 'build[./plugins/plugin.helloworld, 1.0.1]'    # Build plugin with version (Windows) 

If a new update of Chipmunk is available, first run rake clobber (to remove all compiled files) and then rake full_pipeline to update and re-build Chipmunk.

The next section gives a thorough explanation of the default plugins provided by Quickstart

Default plugins

In the first part of this section all default plugins provided by Quickstart will be thoroughly explained as of what they do and explain each line. In the second part other useful things such as popup windows or notifications will demonstrated with examples.

Hello World

The plugin Hello World creates a button which prints 'Hello World!' in the debug console whenever it is clicked.

├── process
│   ├── src
│   │   └── main.ts
│   ├── package.json
│   └── tsconfig.json
└── render
    ├── src
    │   ├── lib
    │   │   ├── views
    │   │   │    └── sidebar.vertical
    │   │   │        ├── compontent.ts
    │   │   │        ├── styles.less
    │   │   │        └── template.html
    │   │   ├── module.ts
    │   │   └── service.ts
    │   └── public-api.ts
    ├── ng-package.json
    ├── package.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json
<p>Example</p>
<button (click)="_ng_click()"></button>   <!-- Create a button which calls _ng_click from the components.ts file -->
p {
    color: #FFFFFF;
}
button {
    height: 20px;
    width: 50px;
}
import { Component } from '@angular/core';  // Import the Angular component that is necessary for the setup below
@Component({
    selector: 'example',                    // Choose the selector name of the plugin
    templateUrl: './template.html',         // Assign HTML file as template
    styleUrls: ['./styles.less']            // Assign LESS file as style sheet file
})
export class ExampleComponent {             // Create an example class for the method
    public _ng_click() {                    // Create a method for the button in template.html
        console.log('Hello World!');        // Initiate a console output
    }
}
import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { Example } from './component';                      // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ Example ],                              // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ Example ]                                    // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}
export * from './lib/component';    // Export the main component of the plugin
export * from './lib/module';       // Export the module file of the plugin

IMPORTANT: Make sure the PluginModule class inherits from Toolkit.PluginNgModule or else the modules won't be part of the plugin!

IMPORTANT: Exporting the component and module is required by Angular and necessary for the plugin to work!

Row Columns

The plugin Row Columns creates a custom render for CSV files to show its conent in columns.

└── render
    ├── src
    │   ├── lib
    │   │   ├── row.columns.api.ts
    │   │   └── row.columns.api.ts
    │   └── public-api.ts
    ├── ng-package.json
    ├── package.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json
import * as Toolkit from 'chipmunk.client.toolkit';
// Delimiter for CSV files.
export const CDelimiters = [';', ',', '\t'];
// For now chipmunk supports only predefined count of columns. Developer cannot change 
// it dynamically. Here we are defining some columns headers
export const CColumnsHeaders = [
    'A',
    'B',
    'C',
    'D',
    'F',
    'G',
    'H',
    'I',
    'J',
    'K',
];
let delimiter: string | undefined;
/**
 * @class ColumnsAPI
 * @description Implementation of custom row's render, based on TypedRowRenderAPIColumns
 */
export class ColumnsAPI extends Toolkit.TypedRowRenderAPIColumns {
    constructor() {
        super();
    }
    /**
     * Returns list of column's headers
     * @returns { string[] } - column's headers
     */
    public getHeaders(): string[] {
        return CColumnsHeaders;
    }
    /**
     * Should returns parsed row value as array of columns. Length of columns here
     * should be equal to length of columns (see getHeaders)
     * @param str { string } - string value of row
     * @returns { string[] } - values of columns for row
     */
    public getColumns(str: string): string[] {
        const columns: string[] = str.split(this._getDelimiter(str));
        // Because we don't know, how much columns file will have, we are adding missed
        // or removing no needed columns
        if (columns.length < CColumnsHeaders.length) {
            for (let i = CColumnsHeaders.length - columns.length; i >= 0; i += 1) {
                columns.push('-');
            }
        } else if (columns.length > CColumnsHeaders.length) {
            const rest: string[] = columns.slice(CColumnsHeaders.length - 2, columns.length);
            columns.push(rest.join(this._getDelimiter(str)));
        }
        return columns;
    }
    /**
     * This method will be called by chipmunk's core once before render column's headers.
     * @returns { Array<{ width: number, min: number }> } - default width and minimal width for
     * each column
     */
    public getDefaultWidths(): Array<{ width: number, min: number }> {
        return [
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
            { width: 50, min: 30 },
        ];
    }
    private _getDelimiter(input: string): string {
        if (delimiter !== undefined) {
            return delimiter;
        } else {
            let score: number = 0;
            CDelimiters.forEach((del: string) => {
                let length = input.split(del).length;
                if (length > score) {
                    score = length;
                    delimiter = del;
                }
            });    
        }
        return delimiter;        
    }
}
import * as Toolkit from 'chipmunk.client.toolkit';
import { ColumnsAPI } from './row.columns.api';
/**
 * @class Columns
 * @description Chipmunk supports custom renders for rows. It means, before row 
 * (in main view and view of search result) will be show, chipmunk will apply 
 * available custom renders. Render could be bound with type of income data. 
 * For example with type of opened file.
 * To bind render with type of income data developer should use abstract class
 * TypedRowRender. As generic class, developer should provide render, which
 * should be used for such data. 
 * In this example we are using predefined render of columns
 */
export class Columns extends Toolkit.TypedRowRender {
    // Store instance of custom render to avoid recreating of it with each new
    // chipmunk's core request
    private _api: ColumnsAPI = new ColumnsAPI();
    constructor() {
        super();
    }
    /**
     * This method will be called by chipmunk to detect, which kind of render we are
     * going to use.
     * @returns { ETypedRowRenders } - tells chipmunk's core, which kind of render will be used
     */
    public getType(): Toolkit.ETypedRowRenders {
        // We will use columns render.
        return Toolkit.ETypedRowRenders.columns;
    }
    /**
     * This method will be called for each row in main view and view of search results
     * @param sourceName { string } - name of source of incoming data. For example for file
     * it will be filename. For plugin - plugin name.
     * @param sourceMeta { string } - optional data, which could better describe incoming data.
     * For example for text file it will be "plain/text"; for DLT file - "dlt"
     * @returns { boolean } - in "true" custom render will be applied for row; in "false" custom
     * render will be ignored
     */
    public isTypeMatch(sourceName: string, sourceMeta?: string): boolean {
        // In this example, we are creating custom render for CSV file, to show its content
        // as columns.
        // So, let's just check file name for expected extension.
        return sourceName.search(/\.csv$/gi) !== -1;
    }
    /**
     * Caller for API of custom render. Would be called by chipmunk's core for each row, which returns
     * "true" via "isTypeMatch"
     * @returns { Class }
     */
    public getAPI(): ColumnsAPI {
        return this._api;
    }
}
/*
 * Public API Surface of terminal
 */
import { Columns } from './lib/row.columns';
const columns = new Columns();
// For Angular based plugin would be enough to make export with instance of
// render. No needs to use gateway
export { columns };

Row Parser

The plugin Row Parser creates a custom render for DLT files to colorize keywords.

└── render
    ├── src
    │   └── index.ts
    ├── package.json
    ├── tsconfig.json
    ├── tslint.json
    └── webpack.config.js
import * as Toolkit from 'chipmunk.client.toolkit';
import { default as AnsiUp } from 'ansi_up';
const ansiup = new AnsiUp();
ansiup.escape_for_html = false;
const REGS = {
    COLORS: /\x1b\[[\d;]{1,}[mG]/,
    COLORS_GLOBAL: /\x1b\[[\d;]{1,}[mG]/g,
};
const ignoreList: { [key: string]: boolean } = {};
// To create selection parse we should extend class from RowCommonParser class
// Our class should have at least one public methods:
// - parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo)
export class ASCIIColorsParser extends Toolkit.RowCommonParser {
    /**
     * Method with be called by chipmunk for each row in main view and search results view.
     * @param str { string } - row value as string
     * @param themeTypeRef { EThemeType } - name of current color theme
     * @param row { IRowInfo } - information about row
     * @returns { string } - parsed row as string. It can include HTML tags
     */
    public parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo): string {
        if (typeof row.sourceName === "string") {
            if (ignoreList[row.sourceName] === undefined) {
                ignoreList[row.sourceName] = row.sourceName.search(/\.dlt$/gi) !== -1;
            }
            if (!ignoreList[row.sourceName]) {
                if (row.hasOwnStyles) {
                    // Only strip ANSI escape-codes
                    return str.replace(REGS.COLORS_GLOBAL, "");
                } else if (REGS.COLORS.test(str)) {
                    // ANSI escape-codes to html color-styles
                    return ansiup.ansi_to_html(str);
                }
            }
        }
        return str;
    }
}
// To delivery plugin into chipmunk we should use chipmunk's gateway
// It's stored in global variable "chipmunk"
// Gateway has a method "setPluginExports". With this method we can
// define a list of exported parsers.
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;
if (gate === undefined) {
    console.error(`Fail to find chipmunk gate.`);
} else {
    gate.setPluginExports({
        // Name of property (in this case it's "ascii" could be any. Chipmunk doesn't check
        // a name of property, but detecting a parent class.
        ascii: new ASCIIColorsParser(),
    });
}

Selection Parser

The plugin Selection Parser creates a parser that parses the selected string in the output console. The parser can be selected by right-clicking and opening the option menu.

└── render
    ├── src
    │   └── index.ts
    ├── package.json
    ├── tsconfig.json
    ├── tslint.json
    └── webpack.config.js
import * as Toolkit from 'chipmunk.client.toolkit';
// To create selection parse we should extend class from SelectionParser class
// Our class should have at least two public methods:
// - getParserName(selection: string): string | undefined
// - parse(selection: string, themeTypeRef: Toolkit.EThemeType)
export class SelectionParser extends Toolkit.SelectionParser {
    /**
     * Method with be called by chipmunk before show context menu in main view.
     * If selection acceptable by parser, method should return name on menu item
     * in context menu of chipmunk.
     * If selection couldn't be parsered, method should return undefined. In this
     * case menu item in context menu for this parser will not be show.
     * @param selection { string } - selected text in main view of chipmunk
     * @returns { string } - name of menu item in context menu
     * @returns { undefined } - in case if menu item should not be shown in context menu
     */
    public getParserName(selection: string): string | undefined {
        const date: Date | undefined = this._getDate(selection);
        return date instanceof Date ? 'Convert to DateTime' : undefined;
    }
    /**
     * Method with be called by chipmunk if user will select menu item (see getParserName)
     * in context menu of selection in main view.
     * @param selection { string } - selected text in main view of chipmunk
     * @param themeTypeRef { EThemeType } - name of current color theme
     * @returns { string } - parsed string
     */
    public parse(selection: string, themeTypeRef: Toolkit.EThemeType): string {
        const date: Date | undefined = this._getDate(selection);
        return date !== undefined ? date.toUTCString() : '';
    }
    private _getDate(selection: string): Date | undefined {
        const num: number = parseInt(selection, 10);
        if (!isFinite(num) || isNaN(num)) {
            return undefined;
        }
        const date: Date = new Date(num);
        return date instanceof Date ? date : undefined;
    }
}
// To delivery plugin into chipmunk we should use chipmunk's gateway
// It's stored in global variable "chipmunk"
// Gateway has a method "setPluginExports". With this method we can
// define a list of exported parsers.
const gate: Toolkit.PluginServiceGate | undefined = (window as any).chipmunk;
if (gate === undefined) {
    // This situation isn't possible, but let's check it also
    console.error(`Fail to find chipmunk gate.`);
} else {
    gate.setPluginExports({
        // Name of property (in this case it's "datetime" could be any. Chipmunk doesn't check
        // a name of property, but detecting a parent class.
        datetime: new SelectionParser(),
    });
}

Plugin with Settings

The plugin Plugin with Settings gives the opportunity to create custom settings for plugins.

At the top there is an input area to search for specific settings quickly. On the left side are the sections of the settings.

To create settings the first step is to create the component for the setting. In this example SomestringSetting will create an input field which takes a string in. Currently there are 3 types of settings available to implement: string input, number input and checkboxes

The validate method to checks the value of the setting and only saves if it is valid.

The Service class is necessary to register the created settings and make them accessable. The setup registers the created settings with: key - Keyname of setting (necessary to define subsettings) path - Keyname of supersetting (under which section the settings should be put) name - Title of setting (e.g. title of input) desc - Description under setting type - Type of setting (standard - show settings, advanced - button to show/hide advanced settings)

By creating a read method the value of the setting can be read and put out into the console. It's important to note, that the get method needs the data type of the setting and takes the relative path of the setting (e.g. 'selectionparser') and the full path (all superpathes seperated by a '.') as the second arguement (e.g. 'plugins.selectionparser').

Settings can be created for both the UI part or the process part of the plugin. Settings for the UI part are located in the render folder, whereas the settings for the process part are located in the process folder.

NOTE: Settings can only be added under Plugins

NOTE: No restriction on amount of sub-settings

└── process
    ├── src
    │   └── main.ts
    ├── package.json
    └── tsconfig.json
└── render
    ├── src
    │   └── index.ts
    ├── package.json
    ├── tsconfig.json
    ├── tslint.json
    └── webpack.config.js
import PluginIPCService, { ServiceState, ServiceSettings, Entry, ESettingType, IPCMessages, Field, ElementInputStringRef } from 'chipmunk.plugin.ipc';
/**
 * To create settings field we should use abstract class Field.
 * T: string, boolean or number
 */
export class SomestringSetting extends Field {
    /**
     * We should define reference to one of controller to render setting:
     */
    private _element: ElementInputStringRef = new ElementInputStringRef({
        placeholder: 'Test placeholder',
    });
    /**
     * Returns default value
     */
    public getDefault(): Promise {
        return new Promise((resolve) => {
            resolve('');
        });
    }
    /**
     * Validation of value. Called each time user change it. Also called before save data into
     * setting file.
     * Should reject on error and resolve on success.
     */
    public validate(state: string): Promise {
        return new Promise((resolve, reject) => {
            if (typeof state !== 'string') {
                return reject(new Error(`Expecting string type for "SomestringSetting"`));
            }
            if (state.trim() !== '' && (state.length < 5 || state.length > 15)) {
                return reject(new Error(`${state.length} isn't valid length. Expected 5 < > 15`));
            }
            resolve();
        });
    }
    /**
     * Should return reference to render component
     */
    public getElement(): ElementInputStringRef {
        return this._element;
    }
}
class Plugin {
    constructor() {
        this._setup();
        this._read();
    }
    private _setup() {
        // Create a group (section) for settings
        ServiceSettings.register(new Entry({
            key: 'testBEEntry',
            path: '', // Put settings into root of settings tree
            name: 'Backend Plugin Settings',
            desc: 'This is some kind of settings, delivered from backend',
            type: ESettingType.standard,
        })).then(() => {
            console.log(`Group is registred`);
            ServiceSettings.register(new SomestringSetting({
                key: 'pluginBESetting',
                path: 'testBEEntry',
                name: 'BE String setting',
                desc: 'This is test of string setting',
                type: ESettingType.standard,
                value: '',
            })).then(() => {
                console.log(`Setting is registred`);
            }).catch((error: Error) => {
                console.log(`Fail due: ${error.message}`);
            });
        }).catch((error: Error) => {
            console.log(`Fail due: ${error.message}`);
        });
    }
    private _read() {
        // Read some settings
        ServiceSettings.get('PluginsUpdates', 'general.plugins').then((value: boolean) => {
            console.log(value);
        }).catch((error: Error) => {
            console.log(error);
        });
    }
}
const app: Plugin = new Plugin();
// Notify core about plugin
ServiceState.accept().catch((err: Error) => {
    console.log(`Fail to notify core about plugin due error: ${err.message}`);
});
// tslint:disable: max-classes-per-file
import * as Toolkit from 'chipmunk.client.toolkit';
import { Field, ElementInputStringRef } from 'chipmunk.client.toolkit';
/**
 * To create settings field we should use abstract class Field.
 * T: string, boolean or number
 */
export class SomestringSetting extends Field {
    /**
     * We should define reference to one of controller to render setting:
     */
    private _element: ElementInputStringRef = new ElementInputStringRef({
        placeholder: 'Test placeholder',
    });
    /**
     * Returns default value
     */
    public getDefault(): Promise {
        return new Promise((resolve) => {
            resolve('');
        });
    }
    /**
     * Validation of value. Called each time user change it. Also called before save data into
     * setting file.
     * Should reject on error and resolve on success.
     */
    public validate(state: string): Promise {
        return new Promise((resolve, reject) => {
            if (typeof state !== 'string') {
                return reject(new Error(`Expecting string type for "SomestringSetting"`));
            }
            if (state.trim() !== '' && (state.length < 3 || state.length > 10)) {
                return reject(new Error(`${state.length} isn't valid length. Expected 3 < > 10`));
            }
            resolve();
        });
    }
    /**
     * Should return reference to render component
     */
    public getElement(): ElementInputStringRef {
        return this._element;
    }
}
/**
 * Create service (to have access to chipmunk API)
 */
class Service extends Toolkit.APluginService {
    constructor() {
        super();
        // Listen moment when API will be available
        this.onAPIReady.subscribe(() => {
            this.read();
            this.setup();
        });
    }
    public setup() {
        const api: Toolkit.IAPI | undefined = this.getAPI();
        if (api === undefined) {
            return;
        }
        // Create a group (section) for settings
        api.getSettingsAPI().register(new Toolkit.Entry({
            key: 'selectionparser',
            path: '', // Put settings into root of settings tree
            name: 'Datetime Converter',
            desc: 'Converter any format to datetime format',
            type: Toolkit.ESettingType.standard,
        }));
        // Create setting
        api.getSettingsAPI().register(new SomestringSetting({
            key: 'pluginselectionparser',
            path: 'selectionparser',
            name: 'String setting',
            desc: 'This is test of string setting',
            type: Toolkit.ESettingType.standard,
            value: '',
        }));
    }
    public read() {
        const api: Toolkit.IAPI | undefined = this.getAPI();
        if (api === undefined) {
            return;
        }
        // Read some settings
        api.getSettingsAPI().get('PluginsUpdates', 'general.plugins').then((value: boolean) => {
            console.log(value);
        }).catch((error: Error) => {
            console.log(error);
        });
    }
}
// To create selection parse we should extend class from SelectionParser class
// Our class should have at least two public methods:
// - getParserName(selection: string): string | undefined
// - parse(selection: string, themeTypeRef: Toolkit.EThemeType)
// tslint:disable-next-line: max-classes-per-file
export class SelectionParser extends Toolkit.SelectionParser {
    /**
     * Method with be called by chipmunk before show context menu in main view.
     * If selection acceptable by parser, method should return name on menu item
     * in context menu of chipmunk.
     * If selection couldn't be parsered, method should return undefined. In this
     * case menu item in context menu for this parser will not be show.
     * @param selection { string } - selected text in main view of chipmunk
     * @returns { string } - name of menu item in context menu
     * @returns { undefined } - in case if menu item should not be shown in context menu
     */
    public getParserName(selection: string): string | undefined {
        const date: Date | undefined = this._getDate(selection);
        return date instanceof Date ? 'Convert to DateTime' : undefined;
    }
    /**
     * Method with be called by chipmunk if user will select menu item (see getParserName)
     * in context menu of selection in main view.
     * @param selection { string } - selected text in main view of chipmunk
     * @param themeTypeRef { EThemeType } - name of current color theme
     * @returns { string } - parsed string
     */
    public parse(selection: string, themeTypeRef: Toolkit.EThemeType): string {
        const date: Date | undefined = this._getDate(selection);
        return date !== undefined ? date.toUTCString() : '';
    }
    private _getDate(selection: string): Date | undefined {
        const num: number = parseInt(selection, 10);
        if (!isFinite(num) || isNaN(num)) {
            return undefined;
        }
        const date: Date = new Date(num);
        return date instanceof Date ? date : undefined;
    }
}
// To delivery plugin into chipmunk we should use chipmunk's gateway
// It's stored in global variable "chipmunk"
// Gateway has a method "setPluginExports". With this method we can
// define a list of exported parsers.
const gate: Toolkit.PluginServiceGate | undefined = (window as any).chipmunk;
if (gate === undefined) {
    // This situation isn't possible, but let's check it also
    console.error(`Fail to find chipmunk gate.`);
} else {
    gate.setPluginExports({
        // Name of property (in this case it's "datetime" could be any. Chipmunk doesn't check
        // a name of property, but detecting a parent class.
        datetime: new SelectionParser(),
        // Share service with chipmunk
        service: new Service(),
    });
}

Shell

The plugin Shell creates an input in which console commands can be typed whereas the output will be directed into the output section of Chipmunk.

├── process
│   ├── src
│   │   ├── env.logger.parameters.ts
│   │   ├── env.logger.ts
│   │   ├── main.ts
│   │   ├── process.env.ts
│   │   ├── process.fork.ts
│   │   └── service.stream.ts
│   ├── package.json
│   └── tsconfig.json
└── render
    ├── src
    │   ├── lib
    │   │   ├── common
    │   │   │   ├── host.events.ts
    │   │   │   └── interface.settings.ts
    │   │   ├── parsers
    │   │   │   ├── parser.rest.ts
    │   │   │   └── parser.row.ts
    │   │   ├── tools
    │   │   │   └── ansi.colors.ts
    │   │   ├── views
    │   │   │   └── sidebar.vertical
    │   │   │       ├── compontent.ts
    │   │   │       ├── styles.less
    │   │   │       └── template.html
    │   │   ├── module.ts
    │   │   └── service.ts
    │   └── public-api.ts
    ├── ng-package.json
    ├── package.json
    ├── tsconfig.lib.json
    ├── tsconfig.spec.json
    └── tslint.json

Process

const DEFAUT_ALLOWED_CONSOLE = {
    DEBUG: true,
    ENV: true,
    ERROR: true,
    INFO: true,
    VERBOS: false,
    WARNING: true,
};
export type TOutputFunc = (...args: any[]) => any;
/**
 * @class
 * Settings of logger
 *
 * @property {boolean} console - Show / not show logs in console
 * @property {Function} output - Sends ready string message as argument to output functions
 */
export class LoggerParameters {
    public console: boolean = true;
    public allowedConsole: {[key: string]: boolean} = {};
    public output: TOutputFunc | null = null;
    constructor(
        {
            console         = true,
            output          = null,
            allowedConsole  = DEFAUT_ALLOWED_CONSOLE,
        }: {
            console?: boolean,
            output?: TOutputFunc | null,
            allowedConsole?: {[key: string]: boolean },
        }) {
        this.console = console;
        this.output = output;
        this.allowedConsole = allowedConsole;
    }
}
import { inspect } from 'util';
import { LoggerParameters } from './env.logger.parameters';
enum ELogLevels {
    INFO = 'INFO',
    DEBUG = 'DEBUG',
    WARNING = 'WARNING',
    VERBOS = 'VERBOS',
    ERROR = 'ERROR',
    ENV = 'ENV',
}
export type TOutputFunc = (...args: any[]) => any;
/**
 * @class
 * Logger
 */
export default class Logger {
    private _signature: string = '';
    private _parameters: LoggerParameters = new LoggerParameters({});
    /**
     * @constructor
     * @param {string} signature        - Signature of logger instance
     * @param {LoggerParameters} params - Logger parameters
     */
    constructor(signature: string, params?: LoggerParameters) {
        params instanceof LoggerParameters && (this._parameters = params);
        this._signature = signature;
    }
    public setOutput(output: TOutputFunc) {
        if (typeof output !== 'function') {
            this.error(`Fail to setup output function, because it should function, but had gotten: ${typeof output}`);
        }
        this._parameters.output = output;
    }
    /**
     * Publish info logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public info(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.INFO);
    }
    /**
     * Publish warnings logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public warn(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.WARNING);
    }
    /**
     * Publish verbose logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public verbose(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.VERBOS);
    }
    /**
     * Publish error logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public error(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.ERROR);
    }
    /**
     * Publish debug logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public debug(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.DEBUG);
    }
    /**
     * Publish environment logs (low-level stuff, support or tools)
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    public env(...args: any[]) {
        return this._log(this._getMessage(...args), ELogLevels.ENV);
    }
    private _console(message: string, level: ELogLevels) {
        if (!this._parameters.console) {
            return false;
        }
        /* tslint:disable */
        this._parameters.allowedConsole[level] && console.log(message);
        /* tslint:enable */
    }
    private _output(message: string) {
        if (typeof this._parameters.output === 'function') {
            this._parameters.output(message);
            return true;
        } else {
            return false;
        }
    }
    private _getMessage(...args: any[]) {
        let message = ``;
        if (args instanceof Array) {
            args.forEach((smth: any, index: number) => {
                if (typeof smth !== 'string') {
                    message = `${message} (type: ${(typeof smth)}): ${inspect(smth)}`;
                } else {
                    message = `${message}${smth}`;
                }
                index < (args.length - 1) && (message = `${message},\n `);
            });
        }
        return message;
    }
    private _getTime(): string {
        const time: Date = new Date();
        return `${time.toJSON()}`;
    }
    private _log(message: string, level: ELogLevels) {
        message = `[${this._signature}]: ${message}`;
        if (!this._output(`[${this._getTime()}]${message}`)) {
            this._console(`[${this._getTime()}]${message}`, level);
        }
        return message;
    }
}
import Logger from './env.logger';
import * as path from 'path';
import PluginIPCService, { ServiceState } from 'chipmunk.plugin.ipc';
import { IPCMessages } from 'chipmunk.plugin.ipc';
import StreamsService, { IStreamInfo } from './service.streams';
import { IForkSettings } from './process.fork';
class Plugin {
    private _logger: Logger = new Logger('Processes');
    constructor() {
        this._onStreamOpened = this._onStreamOpened.bind(this);
        this._onStreamClosed = this._onStreamClosed.bind(this);
        this._onIncomeRenderIPCMessage = this._onIncomeRenderIPCMessage.bind(this);
        PluginIPCService.subscribe(IPCMessages.PluginInternalMessage, this._onIncomeRenderIPCMessage);
        StreamsService.on(StreamsService.Events.onStreamOpened, this._onStreamOpened);
        StreamsService.on(StreamsService.Events.onStreamClosed, this._onStreamClosed);
    }
    private _onIncomeRenderIPCMessage(message: IPCMessages.PluginInternalMessage, response: (res: IPCMessages.TMessage) => any) {
        switch (message.data.command) {
            case 'command':
                return this._income_command(message).then(() => {
                    response(new IPCMessages.PluginInternalMessage({
                        data: {
                            status: 'done'
                        },
                        token: message.token,
                        stream: message.stream
                    }));
                }).catch((error: Error) => {
                    return response(new IPCMessages.PluginError({
                        message: error.message,
                        stream: message.stream,
                        token: message.token,
                        data: {
                            command: message.data.command
                        }
                    }));
                });
            case 'stop':
                return this._income_stop(message).then(() => {
                    response(new IPCMessages.PluginInternalMessage({
                        data: {
                            status: 'done'
                        },
                        token: message.token,
                        stream: message.stream
                    }));
                }).catch((error: Error) => {
                    return response(new IPCMessages.PluginError({
                        message: error.message,
                        stream: message.stream,
                        token: message.token,
                        data: {
                            command: message.data.command
                        }
                    }));
                });
            case 'write':
                return this._income_write(message).then(() => {
                    response(new IPCMessages.PluginInternalMessage({
                        data: {
                            status: 'done'
                        },
                        token: message.token,
                        stream: message.stream
                    }));
                }).catch((error: Error) => {
                    return response(new IPCMessages.PluginError({
                        message: error.message,
                        stream: message.stream,
                        token: message.token,
                        data: {
                            command: message.data.command
                        }
                    }));
                });
            case 'getSettings':
                return this._income_getSettings(message).then((settings: IForkSettings) => {
                    response(new IPCMessages.PluginInternalMessage({
                        data: {
                            settings: settings
                        },
                        token: message.token,
                        stream: message.stream
                    }));
                }).catch((error: Error) => {
                    return response(new IPCMessages.PluginError({
                        message: error.message,
                        stream: message.stream,
                        token: message.token,
                        data: {
                            command: message.data.command
                        }
                    }));
                });
            default:
                this._logger.warn(`Unknown commad: ${message.data.command}`);
        }
    }
    private _income_command(message: IPCMessages.PluginInternalMessage): Promise {
        return new Promise((resolve, reject) => {
            const streamId: string | undefined = message.stream;
            if (streamId === undefined) {
                return reject(new Error(this._logger.warn(`No target stream ID provided`)));
            }
            // Get a target stream
            const stream: IStreamInfo | undefined = StreamsService.get(streamId);
            if (stream === undefined) {
                return reject(new Error(this._logger.warn(`Fail to find a stream "${streamId}" in storage.`)));
            }
            const cmd: string | undefined = message.data.cmd;
            if (typeof cmd !== 'string') {
                return reject(new Error(this._logger.warn(`Fail to execute command for a stream "${streamId}" because command isn't a string, but ${typeof cmd}.`)));
            }
            // Check: is it "cd" command. If yes, change cwd of settings and resolve
            const cd: boolean | Error = this._cwdChange(cmd, stream);
            if (cd === true) {
                return resolve();
            } else if (cd instanceof Error) {
                return reject(cd);
            }
            // Ref fork to stream
            StreamsService.refFork(streamId, cmd);
            resolve();            
        });
    }
    private _income_stop(message: IPCMessages.PluginInternalMessage): Promise {
        return new Promise((resolve, reject) => {
            const streamId: string | undefined = message.stream;
            if (streamId === undefined) {
                return reject(new Error(this._logger.warn(`No target stream ID provided`)));
            }
            // Get a target stream
            const stream: IStreamInfo | undefined = StreamsService.get(streamId);
            if (stream === undefined) {
                return reject(new Error(this._logger.warn(`Fail to find a stream "${streamId}" in storage.`)));
            }
            // Ref fork to stream
            StreamsService.unrefFork(streamId);
            resolve();            
        });
    }
    private _income_write(message: IPCMessages.PluginInternalMessage): Promise {
        return new Promise((resolve, reject) => {
            const streamId: string | undefined = message.stream;
            if (streamId === undefined) {
                return reject(new Error(this._logger.warn(`No target stream ID provided`)));
            }
            // Get a target stream
            const stream: IStreamInfo | undefined = StreamsService.get(streamId);
            if (stream === undefined) {
                return reject(new Error(this._logger.warn(`Fail to find a stream "${streamId}" in storage.`)));
            }
            const input: any = message.data.input;
            if (input === undefined) {
                return reject(new Error(this._logger.warn(`Fail to write into stream "${streamId}" because input is undefined.`)));
            }
            // Check: is fork still running
            if (stream.fork === undefined || stream.fork.isClosed()) {
                return reject(new Error(this._logger.warn(`Fail to write into stream "${streamId}" because fork is closed.`)));
            }
            // Write data
            stream.fork.write(input).then(resolve).catch(reject);
        });
    }
    private _income_getSettings(message: IPCMessages.PluginInternalMessage): Promise {
        return new Promise((resolve, reject) => {
            const streamId: string | undefined = message.stream;
            if (streamId === undefined) {
                return reject(new Error(this._logger.warn(`No target stream ID provided`)));
            }
            // Get a target stream
            const stream: IStreamInfo | undefined = StreamsService.get(streamId);
            if (stream === undefined) {
                return reject(new Error(this._logger.warn(`Fail to find a stream "${streamId}" in storage.`)));
            }
            resolve(stream.settings);            
        });
    }
    private _cwdChange(command: string, stream: IStreamInfo): boolean | Error {
        const cdCmdReg = /^cd\s*([^\s]*)/gi;
        const match: RegExpExecArray | null = cdCmdReg.exec(command.trim());
        if (match === null) {
            return false;
        }
        if (match.length !== 2) {
            return false;
        }
        try {
            stream.settings.cwd = path.resolve(stream.settings.cwd, match[1]);
        } catch (e) {
            this._logger.error(`Fail to make "cd" due error: ${e.message}`);
            return e;
        }
        StreamsService.updateSettings(stream.streamId, stream.settings);
        return true;
    }
    private _onStreamOpened(streamId: string) {
        // Get a target stream
        const stream: IStreamInfo | undefined = StreamsService.get(streamId);
        if (stream === undefined) {
            return this._logger.warn(`Event "onStreamOpened" was triggered, but fail to find a stream "${streamId}" in storage.`);
        }
        const error: Error | undefined = StreamsService.updateSettings(stream.streamId);
        if (error instanceof Error) {
            return this._logger.warn(`Event "onStreamOpened" was triggered, but fail to notify host due error: ${error.message}.`);
        }
    }
    private _onStreamClosed(streamId: string) {
    }
}
const app: Plugin = new Plugin();
// Notify core about plugin
ServiceState.accept().catch((err: Error) => {
    console.log(`Fail to notify core about plugin due error: ${err.message}`);
});
import { exec, ExecOptions } from 'child_process';
import * as OS from 'os';
import * as Path from 'path';
import * as shellEnv from 'shell-env';
export function shell(command: string, options: ExecOptions = {}): Promise {
    return new Promise((resolve, reject) => {
        options = typeof options === 'object' ? (options !== null ? options : {}) : {};
        exec(command, options, (error: Error | null, stdout: string, stderr: string) => {
            if (error instanceof Error) {
                return reject(error);
            }
            if (stderr.trim() !== '') {
                return reject(new Error(`Finished deu error: ${stderr}`));
            }
            resolve(stdout);
        });
    });
}
export enum EPlatforms {
    aix = 'aix',
    darwin = 'darwin',
    freebsd = 'freebsd',
    linux = 'linux',
    openbsd = 'openbsd',
    sunos = 'sunos',
    win32 = 'win32',
    android = 'android',
}
export type TEnvVars = { [key: string]: string };
export function getOSEnvVars(shell: string): Promise {
    return new Promise((resolve) => {
        if (OS.platform() !== EPlatforms.darwin) {
            return resolve(Object.assign({}, process.env) as TEnvVars);
        }
        shellEnv(shell).then((env) => {
            // console.log(`Next os env variables were detected:`);
            // console.log(env);
            resolve(env);
        }).catch((error: Error) => {
            console.log('Shell-Env Error:');
            console.log(error);
            resolve(Object.assign({}, process.env) as TEnvVars);
        });
    });
}
export function defaultShell(): Promise {
    return new Promise((resolve) => {
        let shellPath: string | undefined = '';
        let command: string = '';
        switch (OS.platform()) {
            case EPlatforms.aix:
            case EPlatforms.android:
            case EPlatforms.darwin:
            case EPlatforms.freebsd:
            case EPlatforms.linux:
            case EPlatforms.openbsd:
            case EPlatforms.sunos:
                shellPath = process.env.SHELL;
                command = 'echo $SHELL';
                break;
            case EPlatforms.win32:
                shellPath = process.env.COMSPEC;
                command = 'ECHO %COMSPEC%';
                break;
        }
        if (shellPath) {
            return resolve(shellPath);
        }
        // process didn't resolve shell, so we query it manually
        shell(command).then((stdout: string) => {
            resolve(stdout.trim());
        }).catch((error: Error) => {
            // COMSPEC should always be available on windows.
            // Therefore: we will try to use /bin/sh as error-mitigation
            resolve("/bin/sh");
        });
    });
}
export function shells(): Promise {
    return new Promise((resolve) => {
        let command: string = '';
        switch (OS.platform()) {
            case EPlatforms.aix:
            case EPlatforms.android:
            case EPlatforms.darwin:
            case EPlatforms.freebsd:
            case EPlatforms.linux:
            case EPlatforms.openbsd:
            case EPlatforms.sunos:
                command = 'cat /etc/shells';
                break;
            case EPlatforms.win32:
                // TODO: Check solution with win
                command = 'cmd.com';
                break;
        }
        shell(command).then((stdout: string) => {
            const values: string[] = stdout.split(/[\n\r]/gi).filter((value: string) => {
                return value.indexOf('/') === 0;
            });
            resolve(values);
        }).catch((error: Error) => {
            resolve([]);
        });
    });
}
export function getExecutedModulePath(): string {
    return Path.normalize(`${Path.dirname(require.main === void 0 ? __dirname : require.main.filename)}`);
}
export function getHomePath(): string {
    return Path.normalize(`${OS.homedir()}`);
}
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
export interface IForkSettings {
    env: { [key: string]: string };
    shell: string | boolean;
    cwd: string;
}
export interface ICommand {
    cmd: string;
    settings: IForkSettings;
}
export default class Fork extends EventEmitter {
    public static Events = {
        data: 'data',
        exit: 'exit'
    };
    public Events = Fork.Events;
    private _process: ChildProcess | undefined;
    private _closed: boolean = true;
    private _command: ICommand;
    constructor(command: ICommand) {
        super();
        this._command = command;
    }
    public execute() {
        this._process = spawn(this._command.cmd, {
            cwd: this._command.settings.cwd,
            env: this._command.settings.env,
            shell: this._command.settings.shell,
        });
        this._closed = false;
        this._process.stdout.on('data', this._onStdout.bind(this));
        this._process.stderr.on('data', this._onStderr.bind(this));
        this._process.on('exit', this._onExit.bind(this));
        this._process.on('close', this._onClose.bind(this));
        this._process.on('disconnect', this._onDisconnect.bind(this));
        this._process.on('error', this._onError.bind(this));
    }
    public write(data: any): Promise {
        return new Promise((resolve, reject) => {
            if (this._process === undefined) {
                return reject(new Error(`Shell process isn't available. It was destroyed or wasn't created at all.`));
            }
            this._process.stdin.write(data, (error: Error | null | undefined) => {
                if (error) {
                    return reject(error);
                }
                resolve();
            });
        });
    }
    public destroy() {
        this._closed = true;
        if (this._process === undefined) {
            return;
        }
        this.removeAllListeners();
        this._process.removeAllListeners();
        this._process.kill();
        this._process = undefined;
    }
    public isClosed(): boolean {
        return this._closed;
    }
    private _onStdout(chunk: any) {
        this.emit(this.Events.data, chunk);
    }
    private _onStderr(chunk: any) {
        this.emit(this.Events.data, chunk);
    }
    private _onExit() {
        this.emit(this.Events.exit);
        this.destroy();
    }
    private _onClose() {
        this.emit(this.Events.exit);
        this.destroy();
    }
    private _onDisconnect() {
        this.emit(this.Events.exit);
        this.destroy();
    }
    private _onError(error: Error) {
        this.emit(this.Events.data, error.message);
        this.emit(this.Events.exit);
        this.destroy();
    }
}
import Logger from './env.logger';
import PluginIPCService from 'chipmunk.plugin.ipc';
import Fork, { IForkSettings } from './process.fork';
import * as EnvModule from './process.env';
import { EventEmitter } from 'events';
import * as os from 'os';
export interface IStreamInfo {
    fork: Fork | undefined;
    streamId: string;
    settings: IForkSettings;
}
class StreamsService extends EventEmitter {
    public Events = {
        onStreamOpened: 'onStreamOpened',
        onStreamClosed: 'onStreamClosed',
    };
    private _logger: Logger = new Logger('StreamsService');
    private _streams: Map = new Map();
    constructor() {
        super();
        this._onOpenStream = this._onOpenStream.bind(this);
        this._onCloseStream = this._onCloseStream.bind(this);
        PluginIPCService.on(PluginIPCService.Events.openStream, this._onOpenStream);
        PluginIPCService.on(PluginIPCService.Events.closeStream, this._onCloseStream);
    }
    public get(streamId: string): IStreamInfo | undefined {
        return this._streams.get(streamId);
    }
    public refFork(streamId: string, command: string): Error | undefined {
        const stream: IStreamInfo | undefined = this._streams.get(streamId);
        if (stream === undefined) {
            return new Error(`Stream ${streamId} is not found. Cannot set fork.`);
        }
        // Check: does fork already exist (previous commands still running)
        if (stream.fork !== undefined) {
            return new Error(`Stream ${streamId} has running fork, cannot start other.`);
        }
        // Create fork to execute command
        const fork: Fork = new Fork({ 
            cmd: command,
            settings: stream.settings
        });
        // Attach listeners
        fork.on(Fork.Events.data, (chunk) => {
            PluginIPCService.sendToStream(chunk, streamId);
        });
        fork.on(Fork.Events.exit, () => {
            this.unrefFork(streamId);
        });
        // Save fork
        stream.fork = fork;
        this._streams.set(streamId, stream);
        // Start forl
        fork.execute();
        PluginIPCService.sendToPluginHost(streamId, {
            event: 'ForkStarted',
            streamId: streamId
        });
    }
    public unrefFork(streamId: string): Error | undefined {
        const stream: IStreamInfo | undefined = this._streams.get(streamId);
        if (stream === undefined) {
            return new Error(`Stream ${streamId} is not found. Cannot set fork.`);
        }
        if (stream.fork !== undefined && !stream.fork.isClosed()) {
            stream.fork.destroy();
        }
        stream.fork = undefined;
        this._streams.set(streamId, stream);
        PluginIPCService.sendToPluginHost(streamId, {
            event: 'ForkClosed',
            streamId: streamId
        });
    }
    public updateSettings(streamId: string, settings?: IForkSettings): Error | undefined {
        const stream: IStreamInfo | undefined = this._streams.get(streamId);
        if (stream === undefined) {
            return new Error(`Stream ${streamId} is not found. Cannot update settings.`);
        }
        if (settings !== undefined) {
            stream.settings = Object.assign({}, settings);
            this._streams.set(streamId, stream);
        }
        PluginIPCService.sendToPluginHost(streamId, {
            event: 'SettingsUpdated',
            settings: stream.settings,
            streamId: streamId
        });
    }
    private _getInitialOSEnv(defaults: EnvModule.TEnvVars): EnvModule.TEnvVars {
        defaults.TERM = 'xterm-256color';
        return defaults;
    }
    private _onOpenStream(streamId: string) {
        if (this._streams.has(streamId)) {
            return this._logger.warn(`Stream ${streamId} is already created.`);
        }
        EnvModule.defaultShell().then((userShell: string) => {
            console.log(`Detected default shell: ${userShell}`);
            EnvModule.getOSEnvVars(userShell).then((env: EnvModule.TEnvVars) => {
                //Apply default terminal color scheme
                this._createStream(streamId, os.homedir(), this._getInitialOSEnv(env), userShell);
            }).catch((error: Error) => {
                this._logger.warn(`Failed to get OS env vars for stream ${streamId} due to error: ${error.message}. Default node-values will be used .`);
                this._createStream(streamId, os.homedir(), this._getInitialOSEnv(Object.assign({}, process.env) as EnvModule.TEnvVars), userShell);
            });
        }).catch((gettingShellErr: Error) => {
            this._logger.env(`Failed to create stream "${streamId}" due to error: ${gettingShellErr.message}.`)
        });
    }
    private _onCloseStream(streamId: string) {
        const stream: IStreamInfo | undefined = this._streams.get(streamId);
        if (stream === undefined) {
            return this._logger.warn(`Stream ${streamId} is already closed.`);
        }
        // Check fork before (if it's still working)
        if (stream.fork !== undefined) {
            stream.fork.destroy();
        }
        // Remove stream now
        this._streams.delete(streamId);
        this.emit(this.Events.onStreamClosed, streamId);
    }
    private _createStream(streamId: string, cwd: string, env: EnvModule.TEnvVars, shell: string) {
        this._streams.set(streamId, {
            fork: undefined,
            streamId: streamId,
            settings: {
                cwd: cwd,
                shell: shell,
                env: env
            }
        });
        this.emit(this.Events.onStreamOpened, streamId);
        this._logger.env(`Stream "${streamId}" is bound with cwd "${cwd}".`);
    }
}
export default (new StreamsService());

Render

/*
 * Public API Surface of terminal
 */
export * from './lib/views/sidebar.vertical/component';
export * from './lib/module';
export { parserRow } from './lib/parsers/parser.row';
export { parserRest } from './lib/parsers/parser.rest';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SidebarVerticalComponent } from './views/sidebar.vertical/component';
import { PrimitiveModule } from 'chipmunk-client-material';
import * as Toolkit from 'chipmunk.client.toolkit';
@NgModule({
    entryComponents: [ SidebarVerticalComponent],
    declarations: [ SidebarVerticalComponent],
    imports: [ CommonModule, FormsModule, PrimitiveModule ],
    exports: [ SidebarVerticalComponent]
})
export class PluginModule extends Toolkit.PluginNgModule {
    constructor() {
        super('OS', 'Allows to execute local processes');
    }
}
import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class TerminalService {
  constructor() { }
}
export enum EHostEvents {
    ForkStarted = 'ForkStarted',
    ForkClosed = 'ForkClosed',
    SettingsUpdated = 'SettingsUpdated',
}
export enum EHostCommands {
    command = 'command',
    write = 'write',
    stop = 'stop',
    getSettings = 'getSettings',
}
export interface IForkSettings {
    env: { [key: string]: string };
    shell: string | boolean;
    cwd: string;
}
import { AnsiEscapeSequencesColors } from '../tools/ansi.colors';
export function parserRest(str: string): string {
    const colors: AnsiEscapeSequencesColors = new AnsiEscapeSequencesColors();
    return colors.getHTML(str);
}
import { AnsiEscapeSequencesColors } from '../tools/ansi.colors';
export function parserRow(str: string): string {
    const colors: AnsiEscapeSequencesColors = new AnsiEscapeSequencesColors();
    return colors.getHTML(str);
}
// tslint:disable:max-line-length
// tslint:disable:no-inferrable-types
// Base: https://en.wikipedia.org/wiki/ANSI_escape_code
const RegExps = {
    color: /\x1b\[([0-9;]*)m/gi
};
class AnsiColorDefinition {
    private _value: string;
    private readonly _map: { [key: string]: (key: string, params: string[]) => string } = {
        '0': this._fn_drop.bind(this), '1': this._fn_bold.bind(this), '2': this._fn_ubold.bind(this), '3': this._fn_italic, '4': this._fn_underline.bind(this),
        '5': this._fn_dummy.bind(this), '6': this._fn_dummy.bind(this), '7': this._fn_dummy.bind(this), '8': this._fn_dummy.bind(this), '9': this._fn_dummy.bind(this),
        '10': this._fn_dummy.bind(this), '11': this._fn_dummy.bind(this), '12': this._fn_dummy.bind(this), '13': this._fn_dummy.bind(this), '14': this._fn_dummy.bind(this),
        '15': this._fn_dummy.bind(this), '16': this._fn_dummy.bind(this), '17': this._fn_dummy.bind(this), '18': this._fn_dummy.bind(this), '19': this._fn_dummy.bind(this),
        '20': this._fn_dummy.bind(this), '21': this._fn_dummy.bind(this), '22': this._fn_dummy.bind(this), '23': this._fn_nounderline.bind(this), '24': this._fn_nounderline.bind(this),
        '25': this._fn_dummy.bind(this), '26': this._fn_dummy.bind(this), '27': this._fn_dummy.bind(this), '28': this._fn_dummy.bind(this), '29': this._fn_dummy.bind(this),
        '30': this._fn_foreground.bind(this), '31': this._fn_foreground.bind(this), '32': this._fn_foreground.bind(this), '33': this._fn_foreground.bind(this), '34': this._fn_foreground.bind(this),
        '35': this._fn_foreground.bind(this), '36': this._fn_foreground.bind(this), '37': this._fn_foreground.bind(this), '38': this._fn_foreground.bind(this), '39': this._fn_foreground.bind(this),
        '40': this._fn_background.bind(this), '41': this._fn_background.bind(this), '42': this._fn_background.bind(this), '43': this._fn_background.bind(this), '44': this._fn_background.bind(this),
        '45': this._fn_background.bind(this), '46': this._fn_background.bind(this), '47': this._fn_background.bind(this), '48': this._fn_background.bind(this), '49': this._fn_background.bind(this),
        '50': this._fn_dummy.bind(this), '51': this._fn_dummy.bind(this), '52': this._fn_dummy.bind(this), '53': this._fn_dummy.bind(this), '54': this._fn_dummy.bind(this),
        '55': this._fn_dummy.bind(this), '56': this._fn_dummy.bind(this), '57': this._fn_dummy.bind(this), '58': this._fn_dummy.bind(this), '59': this._fn_dummy.bind(this),
        '60': this._fn_dummy.bind(this), '61': this._fn_dummy.bind(this), '62': this._fn_dummy.bind(this), '63': this._fn_dummy.bind(this), '64': this._fn_dummy.bind(this),
        '65': this._fn_dummy.bind(this),
    };
    private readonly _mapLength: { [key: string]: number } = {
        '0': 0, '1': 0, '2': 0, '3': 0, '4': 0,
        '5': 0, '6': 0, '7': 0, '8': 0, '9': 0,
        '10': 0, '11': 0, '12': 0, '13': 0, '14': 0,
        '15': 0, '16': 0, '17': 0, '18': 0, '19': 0,
        '20': 0, '21': 0, '22': 0, '23': 0, '24': 0,
        '25': 0, '26': 0, '27': 0, '28': 0, '29': 0,
        '30': 0, '31': 0, '32': 0, '33': 0, '34': 0,
        '35': 0, '36': 0, '37': 0, '38': 0, '39': 0,
        '40': 0, '41': 0, '42': 0, '43': 0, '44': 0,
        '45': 0, '46': 0, '47': 0, '48': 0, '49': 0,
        '50': 0, '51': 0, '52': 0, '53': 0, '54': 0,
        '55': 0, '56': 0, '57': 0, '58': 0, '59': 0,
        '60': 0, '61': 0, '62': 0, '63': 0, '64': 0,
        '65': 0,
    };
    constructor(value: string) {
        this._value = value;
    }
    public getStyle(): string {
        const parts: string[] = this._value.split(';');
        let styles: string = '';
        do {
            const key: string = parts[0];
            if (!this._isKeyValid(key)) {
                // Here is should be some log message, because key is unknown
                parts.splice(0, 1);
            } else {
                // Remove current key
                parts.splice(0, 1);
                // Get styles
                styles += this._map[key](key, parts);
            }
        } while (parts.length > 0);
        return styles;
    }
    private _isKeyValid(key: string): boolean {
        return this._mapLength[key] !== undefined;
    }
    private _decode8BitAnsiColor(ansi: number): string {
        // https://gist.github.com/MightyPork/1d9bd3a3fd4eb1a661011560f6921b5b
        const low_rgb = [
            '#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
            '#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
        ];
        if (ansi < 0 || ansi > 255)  { return '#000'; }
        if (ansi < 16) { return low_rgb[ansi]; }
        if (ansi > 231) {
          const s = (ansi - 232) * 10 + 8;
          return `rgb(${s},${s},${s})`;
        }
        const n = ansi - 16;
        let b = n % 6;
        let g = (n - b) / 6 % 6;
        let r = (n - b - g * 6) / 36 % 6;
        b = b ? b * 40 + 55 : 0;
        r = r ? r * 40 + 55 : 0;
        g = g ? g * 40 + 55 : 0;
        return `rgb(${r},${g},${b})`;
    }
    private _fn_bold(key: string, params: string[]): string {
        return 'fontWeight: bold;';
    }
    private _fn_ubold(key: string, params: string[]): string {
        return 'fontWeight: normal;';
    }
    private _fn_italic(key: string, params: string[]): string {
        return 'fontStyle: italic;';
    }
    private _fn_underline(key: string, params: string[]): string {
        return 'textDecoration: underline;';
    }
    private _fn_nounderline(key: string, params: string[]): string {
        return 'textDecoration: none;';
    }
    private _fn_foreground(key: string, params: string[]): string {
        switch (key) {
            case '30':
                return 'color: rgb(0,0,0);';
            case '31':
                return 'color: rgb(170,0,0);';
            case '32':
                return 'color: rgb(0,170,0);';
            case '33':
                return 'color: rgb(170,85,0);';
            case '34':
                return 'color: rgb(0,0,170);';
            case '35':
                return 'color: rgb(170,0,170);';
            case '36':
                return 'color: rgb(0,170,170);';
            case '37':
                return 'color: rgb(170,170,170);';
            case '38':
                if (params[0] === '5' && params.length >= 2) {
                    const cut = params.splice(0, 2);
                    return `color: ${this._decode8BitAnsiColor(parseInt(cut[1], 10))};`;
                } else if (params[0] === '2' && params.length >= 4) {
                    const cut = params.splice(0, 4);
                    return `color: rgb(${cut[1]}, ${cut[2]}, ${cut[3]});`;
                } else {
                    return '';
                }
            case '39':
                return 'color: inherit;';
            default:
                return '';
        }
    }
    private _fn_background(key: string, params: string[]): string {
        switch (key) {
            case '40':
                return 'backgroundColor: rgb(0,0,0);';
            case '41':
                return 'backgroundColor: rgb(128,0,0);';
            case '42':
                return 'backgroundColor: rgb(0,128,0);';
            case '43':
                return 'backgroundColor: rgb(128,128,0);';
            case '44':
                return 'backgroundColor: rgb(0,0,128);';
            case '45':
                return 'backgroundColor: rgb(128,0,128);';
            case '46':
                return 'backgroundColor: rgb(0,128,128);';
            case '47':
                return 'backgroundColor: rgb(192,192,192);';
            case '48':
                if (params[0] === '5' && params.length >= 2) {
                    const cut = params.splice(0, 2);
                    return `backgroundColor: ${this._decode8BitAnsiColor(parseInt(cut[1], 10))};`;
                } else if (params[0] === '2' && params.length >= 4) {
                    const cut = params.splice(0, 4);
                    return `backgroundColor: rgb(${cut[1]}, ${cut[2]}, ${cut[3]});`;
                } else {
                    return '';
                }
            case '49':
                return 'backgroundColor: inherit;';
            default:
                return '';
        }
    }
    private _fn_drop(key: string, params: string[]): string {
        return '';
    }
    private _fn_dummy(key: string, params: string[]): string {
        return '';
    }
}
export class AnsiEscapeSequencesColors {
    constructor() {
    }
    public getHTML(input: string): string {
        let opened: number = 0;
        input = input.replace(RegExps.color, (substring: string, match: string, offset: number, whole: string) => {
            const styleDef: AnsiColorDefinition = new AnsiColorDefinition(match);
            const style: string = styleDef.getStyle();
            opened ++;
            return style !== '' ? `` : ``;
        });
        input += ``.repeat(opened);
        input = input.replace(/<\/span>/gi, '');
        return input;
    }
}
// tslint:disable:no-inferrable-types
import { Component, OnDestroy, ChangeDetectorRef, AfterViewInit, Input, ElementRef, ViewChild } from '@angular/core';
import { EHostEvents, EHostCommands } from '../../common/host.events';
import { IForkSettings } from '../../common/interface.settings';
import * as Toolkit from 'chipmunk.client.toolkit';
export interface IEnvVar {
    key: string;
    value: string;
}
interface IState {
    _ng_envvars: IEnvVar[];
    _ng_settings: IForkSettings | undefined;
    _ng_working: boolean;
    _ng_cmd: string;
}
const state: Toolkit.ControllerState = new Toolkit.ControllerState();
@Component({
    selector: 'lib-sidebar-ver',
    templateUrl: './template.html',
    styleUrls: ['./styles.less']
})
export class SidebarVerticalComponent implements AfterViewInit, OnDestroy {
    @ViewChild('cmdinput', {static: false}) _ng_input: ElementRef;
    @Input() public api: Toolkit.IAPI;
    @Input() public session: string;
    @Input() public sessions: Toolkit.ControllerSessionsEvents;
    public _ng_envvars: IEnvVar[] = [];
    public _ng_settings: IForkSettings | undefined;
    public _ng_working: boolean = false;
    public _ng_cmd: string = '';
    private _subscriptions: { [key: string]: Toolkit.Subscription } = {};
    private _logger: Toolkit.Logger = new Toolkit.Logger(`Plugin: processes: inj_output_bot:`);
    private _destroyed: boolean = false;
    constructor(private _cdRef: ChangeDetectorRef) {
    }
    ngOnDestroy() {
        this._destroyed = true;
        this._saveState();
        Object.keys(this._subscriptions).forEach((key: string) => {
            this._subscriptions[key].unsubscribe();
        });
    }
    ngAfterViewInit() {
        // Subscription to income events
        this._subscriptions.incomeIPCHostMessage = this.api.getIPC().subscribe((message: any) => {
            if (typeof message !== 'object' && message === null) {
                // Unexpected format of message
                return;
            }
            if (message.streamId !== this.session) {
                // No definition of streamId
                return;
            }
            this._onIncomeMessage(message);
        });
        // Subscribe to sessions events
        this._subscriptions.onSessionChange = this.sessions.subscribe().onSessionChange(this._onSessionChange.bind(this));
        this._subscriptions.onSessionOpen = this.sessions.subscribe().onSessionOpen(this._onSessionOpen.bind(this));
        this._subscriptions.onSessionClose = this.sessions.subscribe().onSessionClose(this._onSessionClose.bind(this));
        // Restore state
        this._loadState();
    }
    public _ng_onKeyUp(event: KeyboardEvent) {
        if (this._ng_working) {
            this._sendInput(event);
        } else {
            this._sendCommand(event);
        }
    }
    public _ng_onStop(event: MouseEvent) {
        this._sendStop();
    }
    private _sendCommand(event: KeyboardEvent) {
        if (event.key !== 'Enter') {
            return;
        }
        if (this._ng_cmd.trim() === '') {
            return;
        }
        this.api.getIPC().request({
            stream: this.session,
            command: EHostCommands.command,
            cmd: this._ng_cmd,
            shell: this._ng_settings.shell,
        }, this.session).catch((error: Error) => {
            console.error(error);
        });
    }
    private _sendStop() {
        if (!this._ng_working) {
            return;
        }
        this.api.getIPC().request({
            stream: this.session,
            command: EHostCommands.stop,
        }, this.session).catch((error: Error) => {
            console.error(error);
        });
    }
    private _sendInput(event: KeyboardEvent) {
        this.api.getIPC().request({
            stream: this.session,
            command: EHostCommands.write,
            input: event.key
        }, this.session).catch((error: Error) => {
            console.error(error);
        });
        this._ng_cmd = '';
    }
    private _onIncomeMessage(message: any) {
        if (typeof message.event === 'string') {
            // Process events
            return this._onIncomeEvent(message);
        }
    }
    private _onIncomeEvent(message: any) {
        switch (message.event) {
            case EHostEvents.ForkStarted:
                this._ng_working = true;
                break;
            case EHostEvents.ForkClosed:
                this._ng_working = false;
                this._ng_cmd = '';
                break;
            case EHostEvents.SettingsUpdated:
                this._ng_settings = message.settings;
                this._settingsUpdated();
                break;
        }
        this._forceUpdate();
    }
    private _settingsUpdated(settings?: IForkSettings) {
        if (settings !== undefined) {
            this._ng_settings = settings;
        }
        if (this._ng_settings === undefined) {
            return;
        }
        this._ng_envvars = [];
        Object.keys(this._ng_settings.env).forEach((key: string) => {
            this._ng_envvars.push({
                key: key,
                value: this._ng_settings.env[key]
            });
        });
        this._forceUpdate();
    }
    private _onSessionChange(guid: string) {
        this._saveState();
        this.session = guid;
        this._loadState();
    }
    private _onSessionOpen(guid: string) {
        //
    }
    private _onSessionClose(guid: string) {
        //
    }
    private _saveState() {
        if (this._ng_envvars.length === 0) {
            // Do not save, because data wasn't gotten from backend
            return;
        }
        state.save(this.session, {
            _ng_envvars: this._ng_envvars,
            _ng_settings: this._ng_settings,
            _ng_working: this._ng_working,
            _ng_cmd: this._ng_cmd === undefined ? '' : this._ng_cmd,
        });
    }
    private _loadState() {
        this._ng_envvars  = [];
        this._ng_settings = undefined;
        this._ng_working = false;
        this._ng_cmd = '';
        const stored: IState | undefined = state.load(this.session);
        if (stored === undefined) {
            this._initState();
        } else {
            Object.keys(stored).forEach((key: string) => {
                (this as any)[key] = stored[key];
            });
        }
        if (this._ng_input !== null && this._ng_input !== undefined) {
            this._ng_input.nativeElement.value = this._ng_cmd;
        }
        this._forceUpdate();
    }
    private _initState() {
        // Request current settings
        this.api.getIPC().request({
            stream: this.session,
            command: EHostCommands.getSettings,
        }, this.session).then((response) => {
            this._settingsUpdated(response.settings);
        });
        // Request current cwd
        this.api.getIPC().request({
            stream: this.session,
            command: EHostCommands.getSettings,
        }, this.session).then((response) => {
            this._forceUpdate();
        }).catch((error: Error) => {
            this._logger.env(`Cannot get current setting. It could be stream just not created yet. Error message: ${error.message}`);
        });
    }
    private _forceUpdate() {
        if (this._destroyed) {
            return;
        }
        this._cdRef.detectChanges();
    }
}

@import '../../../../../../theme/variables.less';
:host {
    position: absolute;
    display: block;
    top:0.5rem;
    left:0.5rem;
    right: 0.5rem;
    bottom: 0.5rem;
    overflow-x: hidden;
    overflow-y: auto;
    & * {
        color: @scheme-color-0;
    }
    & div.wrapper{
        position: relative;
        display: block;
        width: 100%;
        & ul.env-vars{
            position: relative;
            display: block;
            padding: 0;
            margin: 0;
            list-style: none;
            max-height: 15rem;
            overflow-x: hidden;
            overflow-y: auto;
            & li{
                position: relative;
                display: block;
                padding: 0;
                margin: 0;
                list-style: none;
                height: 1rem;
                white-space: nowrap;
                border-bottom: thin dotted grey;
                & * {
                    display: inline-block;
                    vertical-align: top;
                    overflow: hidden;
                    text-overflow: ellipsis;
                }
                & span.key{
                    width: 30%;
                }
                & span.value{
                    width: 70%;
                }
            }
        }
        & p.crop{
            font-size: 0.8rem;
            line-height: 0.8rem;
            font-family: 'console', monospace;
            height: 1rem;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: pre;
        }
    }
    & .container{
        position: relative;
        display: block;
        box-sizing: border-box;
        width: 100%;
        overflow: hidden;
        & div.input-wrapper {
            position: relative;
            display: block;
            height: 1.5rem;
            width: ~"calc(100% + 1rem)";
            margin-left: -0.5rem;
        }
        &.working{
            & div.input-wrapper {
                position: relative;
                display: block;
                height: 1.5rem;
                width: ~"calc(100% - 5.5rem)";
                margin-left: 2.5rem;
            }
            & .input-area{
                width: auto;
                margin-right: 2.5rem;
            }
        }
        & div.spinner{
            position: absolute;
            left: 0.25rem;
            top: 0.25rem;
            width: 2rem;
            height: 1rem;
        }
        & div.buttons{
            position: absolute;
            right: 0;
            top: -0.35rem;
            height: 100%;
            width: 3rem;
        }
    }
}
<div class="wrapper" *ngIf="_ng_settings !== undefined">
    <p class="t-normal">Environment vars</p>
    <ul class="env-vars">
        <li *ngFor="let envvar of _ng_envvars">
            <span class="key t-console">{{envvar.key}}</span>
            <span class="value t-console">{{envvar.value}}</span>
        </li>
    </ul>
    <p class="t-normal">Shell</p>
    <p class="t-console crop">{{_ng_settings.shell}}</p>
    <p class="t-normal">Cwd</p>
    <p class="t-console crop">{{_ng_settings.cwd}}</p>
</div>
<div [attr.class]="'container ' + (_ng_working ? 'working' : 'free')">
    <div class="comstyle-input-holder input-wrapper">
        <div class="comstyle-input">
            <input #cmdinput
                class="comstyle-input"
                [attr.disabled]="_ng_working ? '' : null"
                type="text"
                placeholder="command or path to program to be executed"
                [(ngModel)]="_ng_cmd" 
                (keyup)="_ng_onKeyUp($event)"/>
        </div>
    </div>
    <div class="buttons" *ngIf="_ng_working">
        <span class="small-button" (click)="_ng_onStop($event)">Stop</span>
    </div>
    <div class="spinner" *ngIf="_ng_working">
        <lib-primitive-spinner-regular></lib-primitive-spinner-regular>
    </div>
</div>

Additional features

In this part a few additional features will be explained with an example as well as a line by line description of the example code.

Popup

A popup is a window that appears on the most upper layer of Chipmunk and blocks any kind of interaction outside of the popup until closed. It can be closed by either clicking the 'x' button on the upper right To create and remove popups, the API is required. Chipmunk provides an API which gives access to major core events and different modules. The API for the UI is named chipmunk.client.toollkit.

Example

In this example, a plugin with a button will be created. When the button is pressed, a popup with a message (provided by the plugin) will be shown along with a button to close the popup window.

Popup component

Plugin component

<p>Example</p>
<button (click)="_ng_popup()"></button>   <!-- Button to open popup -->
p {
    color: #FFFFFF;
}
import { Component, Input } from '@angular/core';       // Import necessary components for plugin
import { PopupComponent } from './popup/components';    // Import the popup module
@Component({
    selector: 'example',                                // Choose the selector name of the popup
    templateUrl: './template.html',                     // Assign HTML file as template
    styleUrls: ['./styles.less']                        // Assign LESS file as style sheet file})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                  // API assignment
    @Input() public msg: string;                        // Expect input from host component
    constructor() { }
    public _ng_popup() {
        this.api.addPopup({
            caption: 'Example',
            component: {
                factory: PopupComponent,                // Assign the popup module to factory
                inputs: {
                    msg: 'Hello World!',                // Provide the popup with a message as input
                }
            },
            buttons: [                                  // Create a button on the popup to close it
                {
                    caption: 'Cancel',
                    handler: () => {
                        this.api.removePopup();         // Close popup
                    }
                }
            ]
        });
    }
}

NOTE: For more information how the API works check out Chapter 5 - API

Notifications

To create notifications, the API is required. Chipmunk provides an API which gives access to major core events and different modules. The API for the UI is named chipmunk.client.toollkit.

Example

The following example shows an example plugin with a line of text and a button which creates a notification.

<p>Example</p>
<button (click)="_ng_notify()"></button>   <!-- Create a button with a method to be called from the components.ts file -->
p {
    color: #FFFFFF;
}
button {
    height: 20px;
    width: 50px;
}
import { Component } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
import { ENotificationType } from 'chipmunk.client.toolkit';    // Import notification type
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    constructor() { }
    public _ng_notify() {
        this.api.addNotification({
            caption: 'Info',                                    // Caption of the notification
            message: 'You just got notified!',                  // Message of the notification
            options: {
                type: ENotificationType.info                    // Notification type
            }
        });
    }
}

NOTE: For more information how the API works check out Chapter 5 - API

Logger

To use the logger, the API is required. Chipmunk provides an API which gives access to major core events and different modules. The API for the UI is named chipmunk.client.toollkit.

Example

In the example below a plugin is created which logs a message as soon as the plugin is created.

<p>Example</p>     <!-- Create a line of text -->
p {
    color: #FFFFFF;
}
import { Component } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                                // Assign LESS file as style sheet file})
export class ExampleComponent {
    private _logger: Toolkit.Logger = new Toolkit.Logger('Plugin: example: ');  // Instantiate logger with signature
    constructor() {
        this._logger.debug('Plugin started!');                                  // Create debug message
    }
}

NOTE: For more information how the API works check out Chapter 5 - API

Debugging

Debugging in the UI part

The developer mode can be very helpful at developing (especially for the development in the UI). To enable the developing mode, type the following command in the command line, in which the application is started:

export CHIPMUNK_DEVELOPING_MODE=ON

The developer mode will create a debugger console with which console outputs made in the UI can be seen.

Another feature which the debugger provides is creating breakpoints as well as the ability to select HTML elements which then will be highlighted in the code along with its attributes.

NOTE: The keyword debugger serves as a breakpoint in the UI part.

Debugging in the process part

To debug in the process part, simply put breakpoints in the .js files located in the folder releases from Chipmunk Quickstart

Example

In the example below a plugin is created which has a breakpoint in the constructor, so the application stops as soon as the application is created.

<p>Example</p>
p {
    color: #FFFFFF;
}
import { Component } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                            // Choose the selector name of the plugin
    templateUrl: './template.html',                 // Assign HTML file as template
    styleUrls: ['./styles.less']                    // Assign LESS file as style sheet file})
export class ExampleComponent {
    constructor() {
        console.log('Stop after!');                 // Console output to see where the breakpoint will appear
        debugger;                                   // Creating a breakpoint in the constructor
    }
}

API

Chipmunk provides an API for the UI, which gives access to major core events, UI of the core and plugin IPC (required for communication beteween the host and render of plugin). The API for the UI is named chipmunk.client.toollkit and holds different modules.

1. How to use the API

Method 1: Bind the api to the component

One way to use the API is by binding it to the main component of the plugin (component.ts).

IMPORTANT: When the API is bound to component directly, the API is bound to the life cycle of the component and gets destroyed together with the component. It is advised to bind the API to the component if it is going to be used locally only by the component itself and nothing else. If the API should be used globally (in scope of the plugin), the second method is more suited.

The example code below shows an example plugin with the API bound to it. The example also includes three methods that are being called upon specific events from the sessions/tabs. To demonstrate how to use the API, each time the session changes the session ID will be printed out in the console.


<p>Example</p>

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                                                                                // Choose the selector name of the plugin
    templateUrl: './template.html',                                                                                     // Assign HTML file as template
    styleUrls: ['./styles.less']                                                                                        // Assign LESS file as style sheet file})
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                                                                                  // API assignment
    @Input() public session: string;                                                                                    // [optional] Session ID assignment
    @Input() public sessions: Toolkit.ControllerSessionsEvents;                                                         // [optional] Session event listener assignment
    private _subs: { [key: string]: Toolkit.Subscription } = {};                                                        // [optional] Hashlist for session events
    constructor() {
        this._subs.onSessionChange = this.sessions.subscribe().onSessionChange(this._onSessionChange.bind(this));       // [optional] Subscribe to session change event
        this._subs.onSessionOpen = this.sessions.subscribe().onSessionOpen(this._onSessionOpen.bind(this));             // [optional] Subscribe to new session open event
        this._subs.onSessionClose = this.sessions.subscribe().onSessionClose(this._onSessionClose.bind(this));          // [optional] Subscribe to session close event
    }
    private _onSessionChange(session: string) {                                                                         // [optional] Method when session changes
        this.session = session;                                                                                         // Reassign the session to the session, to which has been changed to
        console.log(`Session id: ${this.api.getActiveSessionId()}`);                                                    // Print session ID in the console when the session changes
    }
    private _onSessionOpen(session: string) { }                                                                         // [optional] Method when new session opens
    private _onSessionClose(session: string) { }                                                                        // [optional] Method when session closes
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

NOTE: The lines commented with [optional] will be covered in ControllerSessionsEvents and serves in this example just for demonstration

Method 2: Create a service for the api

Another way to make use of the API is by creating a service, which can be accessed from any part of the plugin. To make it work, it is important to export the service file in public_api.ts, the library management file of the plugin (generated by Angular automatically). To demonstrate how to use the API, each time the session changes the session ID will be printed out in the console.

IMPORTANT: Compared to the first method, when the API is created in a service file, the API will be accessable globally (in scope of the plugin) and will only get destroyed when the application is closed.


<p>Example</p>

p {
    color: #FFFFFF;
}

import * as Toolkit from 'chipmunk.client.toolkit';
export class Service extends Toolkit.PluginService {                                                                                // The service class has to inherit the PluginService from chipmunk.client.toolkit to get access the the API methods
    private api: Toolkit.IAPI | undefined;                                                                                          // Instance variable to assign API
    private session: string;                                                                                                        // [optional] 
    private _subs: { [key: string]: Toolkit.Subscription } = {};                                                                    // [optional] Hashlist of subscriptions for API
    constructor() {
        super();                                                                                                                    // Call parent constructor
        this._subs.onReady = this.onAPIReady.subscribe(this._onReady.bind(this));                                                   // Subscribe to the onAPIReady method from the API to see when the API is ready
    }
    private _onReady() {                                                                                                            // Method to be called when the API is ready
        this.api = this.getAPI();                                                                                                   // Assign the API to instance variable
        if (this.api === undefined) {                                                                                               // Check if the API is defined to prevent errors
            console.log('API not defined!');
            return;
        }
        this._subs.onSessionOpen = this.api.getSessionsEventsHub().subscribe().onSessionOpen(this._onSessionOpen.bind(this));       // [optional] Subscribe to session change event
        this._subs.onSessionClose = this.api.getSessionsEventsHub().subscribe().onSessionClose(this._onSessionClose.bind(this));    // [optional] Subscribe to new session open event
        this._subs.onSessionChange = this.api.getSessionsEventsHub().subscribe().onSessionChange(this._onSessionChange.bind(this)); // [optional] Subscribe to session close event
    }
    private _onSessionChange(session: string) {                                                                                     // [optional] Method when session changes
        this.session = session;                                                                                                     // Reassign the session to the session, to which has been changed to
        console.log(`Session id: ${this.api.getActiveSessionId()}`);                                                                // Print session ID in the console when the session changes
    }
    private _onSessionOpen(session: string) { }                                                                                     // [optional] Method when new session opens
    private _onSessionClose(session: string) { }                                                                                    // [optional] Method when session closes
}
export default (new Service());                                                                                                     // Export the instantiated service class

NOTE:The lines commented with [optional] will be covered in ControllerSessionsEvents and serves in this example just for demonstration


import { Component } from '@angular/core';
@Component({
    selector: 'example',                // Choose the selector name of the plugin
    templateUrl: './template.html',     // Assign HTML file as template
    styleUrls: ['./styles.less']        // Assign LESS file as style sheet file})
export class ExampleComponent {
    constructor() { }                   // Constructor not necessary for API assignment
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

import Service from './service';
export * from './component';
export * from './module';
export { Service };

IMPORTANT: It's important to note, that the Service HAS to be exported to be used globally (in scope of the plugin)

API - Interfaces

2. IAPI interface

// Typescript

export interface IAPI {
    /**
     * @returns {PluginIPC} Returns PluginAPI object for host and render plugin communication
     */
    getIPC: () => PluginIPC | undefined;
    /**
     * @returns {string} ID of active stream (active tab)
     */
    getActiveSessionId: () => string;
    /**
     * Returns hub of viewport events (resize, update and so on)
     * Should be used to track state of viewport
     * @returns {ControllerViewportEvents} viewport events hub
     */
    getViewportEventsHub: () => ControllerViewportEvents;
    /**
     * Returns hub of sessions events (open, close, changed and so on)
     * Should be used to track active sessions
     * @returns {ControllerSessionsEvents} sessions events hub
     */
    getSessionsEventsHub: () => ControllerSessionsEvents;
    /**
     * Open popup
     * @param {IPopup} popup - description of popup
     */
    addPopup: (popup: IPopup) => string;
    /**
     * Closes popup
     * @param {string} guid - id of existing popup
     */
    removePopup: (guid: string) => void;
    /**
     * Adds sidebar title injection.
     * This method doesn't need "delete" method, because sidebar injection would be
     * removed with a component, which used as sidebar tab render.
     * In any way developer could define an argument as "undefined" to force removing
     * injection from the title of sidebar
     * @param {IComponentDesc} component - description of Angular component
     * @returns {void}
     */
    setSidebarTitleInjection: (component: IComponentDesc | undefined) => void;
    /**
     * Opens sidebar app by ID
     * @param {string} appId - id of app
     * @param {boolean} silence - do not make tab active
     */
    openSidebarApp: (appId: string, silence: boolean) => void;
    /**
     * Opens toolbar app by ID
     * @param {string} appId - id of app
     * @param {boolean} silence - do not make tab active
     */
    openToolbarApp: (appId: string, silence: boolean) => void;
    /**
     * Adds new notification
     * @param {INotification} notification - notification to be added
     */
    addNotification: (notification: INotification) => void;
}

getIPC

    /**
     * @returns {PluginIPC} Returns PluginAPI object for host and render plugin communication
     */
    getIPC: () => PluginIPC | undefined;

Example - getIPC

In this example the API will be assigned to the instance variable of the main component of the plugin

<p>Example</p>     <!-- Show session ID -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                    // Choose the selector name of the plugin
    templateUrl: './template.html',                         // Assign HTML file as template
    styleUrls: ['./styles.less']                            // Assign LESS file as style sheet file
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                      // API assignment
    public api_copy: Toolkit.IAPI;
    constructor() {
        this.api_copy = this.api.getIPC();                  // Assign API to instance variable
    }
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

getActiveSessionId

    /**
     * @returns {string} ID of active stream (active tab)
     */
    getActiveSessionId: () => string;

Example - getActiveSessionId

In this example the session id will be shown in the plugin


<p>{{sessionID}}</p>   <!-- Show session ID -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    public sessionID: string;
    constructor() {
        this.sessionID = this.api.getActiveSessionId();         // Assign session id to local variable
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

getViewportEventsHub

    /**
     * Returns hub of viewport events (resize, update and so on)
     * Should be used to track state of viewport
     * @returns {ControllerViewportEvents} viewport events hub
     */
    getViewportEventsHub: () => ControllerViewportEvents;

Example - getViewportEventsHub


<p #element>Example</p>     <!-- Create a line of text -->

p {
    color: #FFFFFF;
}

import { Component, Input, ViewChild } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                                                                                // Choose the selector name of the plugin
    templateUrl: './template.html',                                                                                     // Assign HTML file as template
    styleUrls: ['./styles.less']                                                                                        // Assign LESS file as style sheet file})
})
export class ExampleComponent {
    @ViewChild('element', {static: false}) _element: HTMLParagraphElement;
    @Input() public api: Toolkit.IAPI;                                                                                  // API assignment
    private _subs: { [key: string]: Toolkit.Subscription } = {};                                                        // Hashlist for subscriptions
    constructor() {
        this._subs.onRowSelected() = this.api.getViewportEventsHub().subscribe().onRowSelected(this._onRow.bind(this)); // Subscribe to the row selection event and call _onRow in case a row is selected
    }
    private _onRow() {
        const selected = this.api.getViewportEventsHub().getSelected();
        this._element.innerHTML = `Line: ${selected.row}: ${selected.str}`;                                             // Reassign the text of the plugin paragraph with the selected line and its text
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

getSessionsEventsHub

    /**
     * Returns hub of sessions events (open, close, changed and so on)
     * Should be used to track active sessions
     * @returns {ControllerSessionsEvents} sessions events hub
     */
    getSessionsEventsHub: () => ControllerSessionsEvents;

Example - getSessionsEventsHub

This example shows the usage of getSessionsEventsHub by creating methods to be called when a session opens/closes/changes:


<p>Example</p>      <!-- Create a line of text -->

p {
    color: #FFFFFF;
}

import * as Toolkit from 'chipmunk.client.toolkit';
export class Service extends Toolkit.PluginService {                                                                                // The service class has to inherit the PluginService from chipmunk.client.toolkit to get access the the API methods
    private api: Toolkit.IAPI | undefined;                                                                                          // Instance variable to assign API
    private session: string;                                                                                                        // Instance variable to assign session ID 
    private _subs: { [key: string]: Toolkit.Subscription } = {};                                                                    // Hashlist of subscriptions for API
    constructor() {
        super();                                                                                                                    // Call parent constructor
        this._subs.onReady = this.onAPIReady.subscribe(this._onReady.bind(this));                                                   // Subscribe to the onAPIReady method from the API to see when the API is ready
    }
    private _onReady() {                                                                                                            // Method to be called when the API is ready
        this.api = this.getAPI();                                                                                                   // Assign the API to instance variable
        if (this.api === undefined) {                                                                                               // Check if the API is defined to prevent errors
            console.log('API not defined!');
            return;
        }
        this._subs.onSessionOpen = this.api.getSessionsEventsHub().subscribe().onSessionOpen(this._onSessionOpen.bind(this));       // <-- Subscribe to session change event
        this._subs.onSessionClose = this.api.getSessionsEventsHub().subscribe().onSessionClose(this._onSessionClose.bind(this));    // <-- Subscribe to new session open event
        this._subs.onSessionChange = this.api.getSessionsEventsHub().subscribe().onSessionChange(this._onSessionChange.bind(this)); // <-- Subscribe to session close event
    }
    private _onSessionChange(session: string) {                                                                                     // Method when session changes
        this.session = session;                                                                                                     // Reassign the session to the session, to which has been changed to
        console.log(`Session id: ${this.api.getActiveSessionId()}`);                                                                // Print session ID in the console when the session changes
    }
    private _onSessionOpen(session: string) { }                                                                                     // Method when new session opens
    private _onSessionClose(session: string) { }                                                                                    // Method when session closes
}
export default (new Service());                                                                                                     // Export the instantiated service class

import { Component } from '@angular/core';
import Service from './service'
@Component({
    selector: 'example',                                                                                                            // Choose the selector name of the plugin
    templateUrl: './template.html',                                                                                                 // Assign HTML file as template
    styleUrls: ['./styles.less']                                                                                                    // Assign LESS file as style sheet file})
export class ExampleComponent {
    constructor() { }
}

import { NgModule } from '@angular/core';                                                                                           // Import the Angular component that is necessary for the setup below
import { Example } from './component';                                                                                              // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';                                                                                 // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ]                                                                                              // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                                                                                                   // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                                                                                                   // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {                                                                          // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');                                                                               // Call the constructor of the parent class
    }
}

import Service from './service';
export * from './component';
export * from './module';
export { Service };

IMPORTANT: It's important to note, that the Service HAS to be exported to be used globally (in scope of the plugin)

addPopup

    /**
     * Open popup
     * @param {IPopup} popup - description of popup
     */
    addPopup: (popup: IPopup) => string;

Example - addPopup

To create a popup, a plugin to host the popup and the popup itself have to be defined.


<p>{{msg}}</p>      <!-- Show message from component -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';           // Import necessary components for popup
@Component({
    selector: 'example-popup-com',                          // Choose the selector name of the popup
    templateUrl: './template.html',                         // Assign HTML file as template
    styleUrls: ['./styles.less']                            // Assign LESS file as style sheet file})
export class PopupComponent {
    constructor() { }
    @Input() public msg: string;                            // Expect input from host component
}

<p>Example</p>
<button (click)="_ng_popup()"></button>       <!-- Button to open popup -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';           // Import necessary components for plugin
import { PopupComponent } from './popup/components';        // Import the popup module
@Component({
    selector: 'example',                                    // Choose the selector name of the popup
    templateUrl: './template.html',                         // Assign HTML file as template
    styleUrls: ['./styles.less']                            // Assign LESS file as style sheet file})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                      // API assignment
    @Input() public msg: string;                            // Expect input from host component
    constructor() { }
    public _ng_popup() {
        this.api.addPopup({
            caption: 'Example',
            component: {
                factory: PopupComponent,                    // Assign the popup module to factory
                inputs: {
                    msg: 'Hello World!',                    // Provide the popup with a message as input
                }
            },
            buttons: []
        });
    }
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

removePopup

    /**
     * Closes popup
     * @param {string} guid - id of existing popup
     */
    removePopup: (guid: string) => void;

Example - removePopup

To remove the popup, one way is to create a button on the popup, which calls the method to remove the popup upon clicking.


<p>{{msg}}</p>      <!-- Show message from component -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';           // Import necessary components for popup
@Component({
    selector: 'example-popup-com',                          // Choose the selector name of the popup
    templateUrl: './template.html',                         // Assign HTML file as template
    styleUrls: ['./styles.less']                            // Assign LESS file as style sheet file})
export class PopupComponent {
    constructor() { }
    @Input() public msg: string;                            // Expect input from host component
}

<p>Example</p>
<button (click)="_ng_popup()"></button>       <!-- Button to open popup -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';           // Import necessary components for plugin
import { PopupComponent } from './popup/components';        // Import the popup module
@Component({
    selector: 'example',                                    // Choose the selector name of the popup
    templateUrl: './template.html',                         // Assign HTML file as template
    styleUrls: ['./styles.less']                            // Assign LESS file as style sheet file})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                      // API assignment
    @Input() public msg: string;                            // Expect input from host component
    constructor() { }
    public _ng_popup() {
        this.api.addPopup({
            caption: 'Example',
            component: {
                factory: PopupComponent,                    // Assign the popup module to factory
                inputs: {
                    msg: 'Hello World!',                    // Provide the popup with a message as input
                }
            },
            buttons: [                                      // Create a button on the popup to close it
                {
                    caption: 'close',
                    handler: () => {
                        this.api.removePopup();             // close and remove popup
                    }
                }
            ]
        });
    }
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

setSidebarTitleInjection

    /**
     * Adds sidebar title injection.
     * This method doesn't need "delete" method, because sidebar injection would be
     * removed with a component, which used as sidebar tab render.
     * In any way developer could define an argument as "undefined" to force removing
     * injection from the title of sidebar
     * @param {IComponentDesc} component - description of Angular component
     * @returns {void}
     */
    setSidebarTitleInjection: (component: IComponentDesc | undefined) => void;

Example - setSidebarTitleInjection

In this example a button will be created in the title of the sidebar which will log a message when clicked.


<!-- Create the title component of the button. -->
<span>+</span>      <!-- Create '+' as button -->

span {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'lib-add',
    templateUrl: './template.html',
    styleUrls: ['./styles.less']
})
export class ExampleTitleComponent {
    @Input() public _ng_click: () => void;      // Take method from host component and execute when clicked
}

<p>Example</p>      <!-- Show example string -->

p {
    color: #FFFFFF;
}

import { Component, AfterViewInit } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent implements AfterViewInit {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    ngAfterViewInit() {
        this.api.setSidebarTitleInjection({                     // Create button in title
            factory: ExampleTitleComponent,                     // Assign component for button
            inputs: {
                _ng_click: () => { console.log('Clicked!') },   // Provide function, which logs a string in console, as input
            }
        });
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

openSidebarApp

    /**
     * Opens sidebar app by ID
     * @param {string} appId - id of app
     * @param {boolean} silence - do not make tab active
     */
    openSidebarApp: (appId: string, silence: boolean) => void;

Example - openSidebarApp

In this example the plugin serial will be opened and set as the active plugin 2 seconds after the example plugin is opened.


<p>Wait for it...</p>       <!-- Show example string -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    constructor() {
        setTimeout(() => {                                      // Set a timeout of 2000 ms before opening the plugin
            this.api.openSidebarApp('serial', false);           // Open the serial plugin and set it as the active plugin 
        }, 2000);
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

openToolbarApp

    /**
     * Opens toolbar app by ID
     * @param {string} appId - id of app
     * @param {boolean} silence - do not make tab active
     */
    openToolbarApp: (appId: string, silence: boolean) => void;

Example - openToolbarApp

In this example the xterminal app will be opened and set as active 2 seconds after the example plugin is opened.


<p>Wait for it...</p>       <!-- Show example string -->

p {
    color: #FFFFFF;
}

import { Component, Input, AfterViewInit } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent implements AfterViewInit {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    constructor() {
        setTimeout(() => {                                      // Set a timeout of 2000 ms before opening the app
            this.api.openToolbarApp('xterminal', false);        // Open the xterminal app and set it as active
        }, 2000);
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

addNotification

    /**
     * Adds new notification
     * @param {INotification} notification - notification to be added
     */
    addNotification: (notification: INotification) => void;

Example - addNotification

In this example the xterminal app will be opened and set as active 2 seconds after the example plugin is opened.

The following example shows an example plugin with a line of text and a button which creates a notification.


<p>Example</p>                                                  <!-- Create a line of text -->
<button (click)="_ng_notify()"></button>              <!-- Create a button with a method to be called from the components.ts file -->

p {
    color: #FFFFFF;
}
button {
    height: 20px;
    width: 50px;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
import { ENotificationType } from 'chipmunk.client.toolkit';    // Import notification type
@Component({
    selector: 'example',                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                // Assign LESS file as style sheet file
})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                          // API assignment
    public _ng_notify() {
        this.api.addNotification({
            caption: 'Info',                                    // Caption of the notification
            message: 'You just got notified!',                  // Message of the notification
            options: {
                type: ENotificationType.info                    // Notification type
            }
        });
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

3. Abstract classes

chipmunk.client.toolkit provides different kinds of abstract classes from which classes can extend from.

3.1 Parsers

These abstract classes allow to create parsers that can modify the output in the rows (e.g: change text color, convert into different format).

Parser nameDescription
RowBoundParserParse only data received from the process part of the plugin
RowCommomParserParse data from any kind of source
RowTypedParserParse only specific type of source (e.g. DLT)
SelectionParser (coming soon)Parse only selected line(s), right-click to see self-chosen name as option to see the parsed result in the tab Details below
TypedRowRender (coming soon)Parser for more complex stream output
TypedRowRenderAPIColumns - show stream line as columns
TypedRowRenderAPIExternal - use custom Angular component as stream

RowBoundParser

// Typescript

/**
 * Allows creating row parser, which will bound with plugin's host.
 * It means: this row parser will be applied only to data, which was
 * received from plugin's host.
 * It also means: usage of this kind of plugin makes sense only if plugin has
 * host part (backend part), which delivery some data. A good example would be:
 * serial port plugin. Host part extracts data from serial port and sends into
 * stream; render (this kind of plugin) applies only to data, which were gotten
 * from serial port.
 * @usecases decoding stream output content; converting stream output into human-readable format
 * @requirements TypeScript or JavaScript
 * @examples Base64string parser, HEX converting into a string and so on
 * @class RowBoundParser
 */
export declare abstract class RowBoundParser {
    /**
     * This method will be called with each line in stream was gotten from plugin's host
     * @param {string} str - single line from stream (comes only from plugin's host)
     * @param {EThemeType} themeTypeRef - reference to active theme (dark, light and so on)
     * @param {IRowInfo} row - information about current row (see IRowInfo for more details)
     * @returns {string} method should return a string.
     */
    abstract parse(str: string, themeTypeRef: EThemeType, row: IRowInfo): string;
}

Example - RowBoundParser


import * as Toolkit from 'chipmunk.client.toolkit';                                                 // Import UI API to extend Parser class
class ParseMe extends Toolkit.RowBoundParser {                                                      // Extend parser class with Abstract parser class 
    public parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo): string {    // Create parser which modifies and returns parsed string
        return `--> ${str}`;                                                                        // Return string with --> in front
    }
} 
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;                      // Identification of the plugin
if (gate === undefined) {                                                                           // If binding didn't work print out error message
    console.error(`Fail to find logviewer gate.`);
} else {
    gate.setPluginExports({                                                                         // Set parser(s) to export here (Setting Multiple parsers possible)
        parser: new ParseMe()                                                                       // Create parser instance (Free to choose parser name)
    });
}

RowCommomParser

// Typescript

/**
 * Allows creating row parser, which will be applied to each new line in stream.
 * @usecases decoding stream output content; converting stream output into human-readable format
 * @requirements TypeScript or JavaScript
 * @examples Base64string parser, HEX converting into a string and so on
 * @class RowCommonParser
 */
export declare abstract class RowCommonParser {
    /**
     * This method will be called with each line in stream
     * @param {string} str - single line from stream
     * @param {EThemeType} themeTypeRef - reference to active theme (dark, light and so on)
     * @param {IRowInfo} row - information about current row (see IRowInfo for more details)
     * @returns {string} method should return a string.
     */
    abstract parse(str: string, themeTypeRef: EThemeType, row: IRowInfo): string;
}

Example - RowCommonParser


import * as Toolkit from 'chipmunk.client.toolkit';                                                 // Import UI API to extend Parser class
class ParseMe extends Toolkit.RowCommonParser {                                                     // Extend parser class with Abstract parser class 
    public parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo): string {    // Create parser which modifies and returns parsed string
        return `--> ${str}`;                                                                        // Return string with --> in front
    }
} 
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;                      // Identification of the plugin
if (gate === undefined) {                                                                           // If binding didn't work print out error message
    console.error(`Fail to find logviewer gate.`);
} else {
    gate.setPluginExports({                                                                         // Set parser(s) to export here (Setting Multiple parsers possible)
        parser: new ParseMe()                                                                       // Create parser instance (Free to choose parser name)
    });
}

RowTypedParser

// Typescript

/**
 * Allows creating row parser with checking the type of source before.
 * It means this parser could be bound with some specific type of source,
 * for example with some specific file's type (DLT, log and so on)
 * @usecases decoding stream output content; converting stream output into human-readable format
 * @requirements TypeScript or JavaScript
 * @examples Base64string parser, HEX converting into a string and so on
 * @class RowTypedParser
 */
export declare abstract class RowTypedParser {
    /**
     * This method will be called with each line in stream
     * @param {string} str - single line from stream
     * @param {EThemeType} themeTypeRef - reference to active theme (dark, light and so on)
     * @param {IRowInfo} row - information about current row (see IRowInfo for more details)
     * @returns {string} method should return a string.
     */
    abstract parse(str: string, themeTypeRef: EThemeType, row: IRowInfo): string;
    /**
     * This method will be called for each line of stream before method "parse" will be called.
     * @param {string} sourceName - name of source
     * @returns {boolean} - true - method "parse" will be called for this line; false - parser will be ignored
     */
    abstract isTypeMatch(sourceName: string): boolean;
}

Example - RowTypedParser


import * as Toolkit from 'chipmunk.client.toolkit';                                                 // Import UI API to extend Parser class
class ParseMe extends Toolkit.RowTypedParser {                                                      // Extend parser class with Abstract parser class 
    public parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo): string {    // Create parser which modifies and returns parsed string
        return `--> ${str}`;                                                                        // Return string with --> in front
    }
    public isTypeMatch(fileName: string): boolean {                                                 // Typecheck for each line of stream before parsing
        if (typeof fileName === 'string' && fileName.search(/\.txt/) > -1) {                        // Check if source is a .txt file
            return true;                                                                            // Return true in case the it is a .txt file
        }
    }
}
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;                      // Identification of the plugin
if (gate === undefined) {                                                                           // If binding didn't work print out error message
    console.error(`Fail to find logviewer gate.`);
} else {
    gate.setPluginExports({                                                                         // Set parser(s) to export here (Setting Multiple parsers possible)
        parser: new ParseMe()                                                                       // Create parser instance (Free to choose parser name)
    });
}

SelectionParser

// Typescript

/**
 * Allows creating parser of selection.
 * Name of the parser will be shown in the context menu of selection. If a user selects parser,
 * parser will be applied to selection and result will be shown on tab "Details"
 * @usecases decoding selected content; converting selected content into human-readable format
 * @requirements TypeScript or JavaScript
 * @examples encrypting of data, Base64string parser, HEX converting into a string and so on
 * @class SelectionParser
 */
export declare abstract class SelectionParser {
    /**
     * This method will be called on user selection
     * @param {string} str - selection in main view or search results view
     * @param {EThemeType} themeTypeRef - reference to active theme (dark, light and so on)
     * @returns {string} method should return a string or HTML string
     */
    abstract parse(str: string, themeTypeRef: EThemeType): string | THTMLString;
    /**
     * Should return name of parser to be shown in context menu of selection
     * @param {string} str - selection in main view or search results view
     * @returns {string} name of parser
     */
    abstract getParserName(str: string): string | undefined;
}

Example - SelectionParser


import * as Toolkit from 'chipmunk.client.toolkit';                                                 // Import UI API to extend Parser class
class ParseMe extends Toolkit.SelectionParser {                                                     // Extend parser class with Abstract parser class 
    public parse(str: string, themeTypeRef: Toolkit.EThemeType): string {                           // Create parser which modifies and returns parsed string
        return `--> ${str}`;                                                                        // Return string with --> in front
    }
    public getParserName(str: string) {                                                             // Create a parser that checks if the string only consists of digits
        if ( str.search(/^\d+$/) {                                                                  // If the string only consists of numbers
            return 'Hightlight number';                                                             // return the name of the parser and create an option upon right-clicking
        }
        return undefined;                                                                           // if it's not the case, return 'undefined' to not create an option upon right-clicking
    }
} 
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;                      // Identification of the plugin
if (gate === undefined) {                                                                           // If binding didn't work print out error message
    console.error(`Fail to find logviewer gate.`);
} else {
    gate.setPluginExports({                                                                         // Set parser(s) to export here (Setting Multiple parsers possible)
        parser: new ParseMe()                                                                       // Create parser instance (Free to choose parser name)
    });
}

TypedRowRender

// Typescript

/**
 * This class is used for more complex renders of stream output. Like:
 * - TypedRowRenderAPIColumns - to show stream line as columns
 * - TypedRowRenderAPIExternal - to use custom Angular component as stream
 * line render
 *
 * @usecases to show content in columns; to have full HTML/LESS features for rendering
 * @class TypedRowRender
 */
export declare abstract class TypedRowRender<T> {
    /**
     * This method will be called for each line of a stream before method "parse" will be called.
     * @param {string} sourceName - name of source
     * @returns {boolean} - true - method "parse" will be called for this line; false - parser will be ignored
     */
    abstract isTypeMatch(sourceName: string): boolean;
    /**
     * This method will return one of the supported types of custom renders:
     * - columns
     * - external
     * @returns {ETypedRowRenders} - type of custom render
     */
    abstract getType(): ETypedRowRenders;
    /**
     * Should return an implementation of custom render. An instance of one of the next renders:
     * - TypedRowRenderAPIColumns
     * - TypedRowRenderAPIExternal
     */
    abstract getAPI(): T;
}

Example - TypedRowRender

IMPORTANT: It's important to note, that TypedRowRender cannot be used by itself, but instead used to create TypedRowRenderAPIColumns and TypedRowRenderAPIExternal renderers. For examples and further information check out the sections TypedRowRenderAPIColumns and TypedRowRenderAPIExternal

Identification

The abstract classes listed below are necessary for the identification and registration of the plugin.

PluginNgModule

// Typescript

/**
 * Root module class for Angular plugin. Should be used by the developer of a plugin (based on Angular) to
 * let core know, which module is a root module of plugin.
 * One plugin can have only one instance of this module.
 * @usecases views, complex components, addition tabs, Angular components
 * @requirements Angular, TypeScript
 * @class PluginNgModule
 */
export declare class PluginNgModule {
    constructor(name: string, description: string) {}
}

Example - PluginNgModule

This example shows how to create a simple plugin along with the usage of PluginNgModule:


<p>Example</p>

p {
    color: #FFFFFF;
}

import { Component } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                // Choose the selector name of the plugin
    templateUrl: './template.html',                     // Assign HTML file as template
    styleUrls: ['./styles.less']                        // Assign LESS file as style sheet file})
export class ExampleComponent {
    constructor() { }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // <-- The module class of the plugin extends from Toolkit.PluginNgModule
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

PluginService

// Typescript

/**
 * Service which can be used to get access to plugin API
 * Plugin API has a collection of methods to listen to major core events and
 * communicate between render and host of plugin.
 * Into plugin's Angular components (like tabs, panels, and dialogs) API object will be
 * delivered via inputs of a component. But to have global access to API developer can
 * create an instance of this class.
 *
 * Note: an instance of this class should be exported with PluginNgModule (for Angular plugins) or
 * with PluginServiceGate.setPluginExports (for none-Angular plugins)
 *
 * @usecases Create global (in the scope of plugin) service with access to plugin's API and core's API
 * @class PluginService
 */
export declare abstract class PluginService {
    private _apiGetter;
    /**
     * @property {Subject<boolean>} onAPIReady subject will be emitted on API is ready to use
     */
    onAPIReady: Subject<boolean>;
    /**
     * Should be used to get access to API of plugin and core.
     * Note: will return undefined before onAPIReady will be emitted
     * @returns {API | undefined} returns an instance of API or undefined if API isn't ready to use
     */
    getAPI(): IAPI | undefined;
}

Example - PluginService

This example shows how to create a service class, that extends from PluginService, which allows global access to the API by import the service class:


<p>Example</p>
p {
    color: #FFFFFF;
}

import * as Toolkit from 'chipmunk.client.toolkit';
export class Service extends Toolkit.PluginService {                    // The service class has to inherit the PluginService from chipmunk.client.toolkit to get access the the API methods
    private api: Toolkit.IAPI | undefined;                              // Instance variable to assign API
    constructor() {
        super();                                                        // Call parent constructor
    }
    private _onReady() {                                                // Method to be called when the API is ready
        this.api = this.getAPI();                                       // Assign the API to instance variable
        if (this.api === undefined) {                                   // Check if the API is defined to prevent errors
            console.log('API not defined!');
            return;
        }
    }
    public printID(): string {
        console.log(`Session id: ${this.api.getActiveSessionId()}`);    // Prints session ID in the console
    }
}
export default (new Service());                                         // Export the instantiated service class

import { Component } from '@angular/core';
import Service from './service.ts'              // Import the service class to use in main component of plugin
@Component({
    selector: 'example',                        // Choose the selector name of the plugin
    templateUrl: './template.html',             // Assign HTML file as template
    styleUrls: ['./styles.less']                // Assign LESS file as style sheet file})
export class ExampleComponent {
    constructor() { 
            Service.printID();                  // Print session ID in the console
    }
}

import { NgModule } from '@angular/core';                       // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';                 // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';             // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                         // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                               // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                               // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {      // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');           // Call the constructor of the parent class
    }
}

import Service from './service';
export * from './component';
export * from './module';
export { Service };

IMPORTANT: It's important to note, that the Service HAS to be exported to be used globally (in scope of the plugin)

PluginServiceGate

// Typescript

/**
 * Used for none-Angular plugins to delivery plugin's exports into the core of chipmunk
 * Developer can create none-Angular plugin. In global namespace of the main javascript file will be
 * available implementation of PluginServiceGate.
 * For example:
 * =================================================================================================
 * const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;
 * gate.setPluginExports({
 *     parser: new MyParserOfEachRow(),
 * });
 * =================================================================================================
 * This code snippet registered a new parser for output "MyParserOfEachRow"
 * @usecases should be used for none-angular plugins to register parsers
 * @class PluginServiceGate
 */
export declare abstract class PluginServiceGate {
    /**
     * Internal usage
     */
    abstract setPluginExports(exports: IPluginExports): void;
    /**
     * Internal usage
     */
    abstract getCoreModules(): ICoreModules;
    /**
     * Internal usage
     */
    abstract getRequireFunc(): TRequire;
}

Example - PluginServiceGate

This example shows how to create a parser, that puts '-->' in front of every line in the output.


import * as Toolkit from 'chipmunk.client.toolkit';                                                 // Import front-end API to extend Parser class
class ParseMe extends Toolkit.RowCommonParser {                                                    // Extend parser class with Abstract parser class 
    public parse(str: string, themeTypeRef: Toolkit.EThemeType, row: Toolkit.IRowInfo): string {    // Create parser which modifies and returns parsed string
        return `--> ${str}`;                                                                        // Return string with --> in front
    }
} 
const gate: Toolkit.PluginServiceGate | undefined = (window as any).logviewer;                      // <-- Usage of PluginServiceGate, Identification of the plugin
if (gate === undefined) {                                                                           // If binding didn't work print out error message
    console.error(`Fail to find logviewer gate.`);
} else {
    gate.setPluginExports({                                                                         // Set parser(s) to export here (Setting Multiple parsers possible)
        parser: new ParseMe()                                                                       // Create parser instance (Free to choose parser name)
    });
}

4. Classes

4.1 ControllerSessionsEvents

// Typescript

/**
 * This class provides access to sessions events (like close, open, change of session).
 *
 * @usecases to track sessions state
 * @class ControllerSessionsEvents
 */
export class ControllerSessionsEvents {

    public static Events = {
        /**
         * Fired on user switch a tab (session)
         * @name onSessionChange
         * @event {string} sessionId - active session ID
         */
        onSessionChange: 'onSessionChange',
        /**
         * Fired on user open a new tab (session)
         * @name onSessionOpen
         * @event {string} sessionId - a new session ID
         */
        onSessionOpen: 'onSessionOpen',
        /**
         * Fired on user close a new tab (session)
         * @name onSessionClose
         * @event {string} sessionId - ID of closed session
         */
        onSessionClose: 'onSessionClose',
        /**
         * Fired on stream has been changed
         * @name onStreamUpdated
         * @event {IEventStreamUpdate} event - current state of stream
         */
        onStreamUpdated: 'onStreamUpdated',
        /**
         * Fired on search results has been changed
         * @name onSearchUpdated
         * @event {IEventSearchUpdate} event - current state of stream
         */
        onSearchUpdated: 'onSearchUpdated',
    };

    private _emitter: Emitter = new Emitter();

    public destroy() {
        this._emitter.unsubscribeAll();
    }

    public unsubscribe(event: any) {
        this._emitter.unsubscribeAll(event);
    }

    /**
     * Emits event
     * @returns {Event Emitter} - function event emitter
     */
    public emit(): {
        onSessionChange: (sessionId: string) => void,
        onSessionOpen: (sessionId: string) => void,
        onSessionClose: (sessionId: string) => void,
        onStreamUpdated: (event: IEventStreamUpdate) => void,
        onSearchUpdated: (event: IEventSearchUpdate) => void,
    } {
        return {
            onSessionChange: this._getEmit.bind(this, ControllerSessionsEvents.Events.onSessionChange),
            onSessionOpen: this._getEmit.bind(this, ControllerSessionsEvents.Events.onSessionOpen),
            onSessionClose: this._getEmit.bind(this, ControllerSessionsEvents.Events.onSessionClose),
            onStreamUpdated: this._getEmit.bind(this, ControllerSessionsEvents.Events.onStreamUpdated),
            onSearchUpdated: this._getEmit.bind(this, ControllerSessionsEvents.Events.onSearchUpdated),
        };
    }

    /**
     * Subscribes to event
     * @returns {Event Subscriber} - function-subscriber for each available event
     */
    public subscribe(): {
        onSessionChange: (handler: TSubscriptionHandler<string>) => Subscription,
        onSessionOpen: (handler: TSubscriptionHandler<string>) => Subscription,
        onSessionClose: (handler: TSubscriptionHandler<string>) => Subscription,
        onStreamUpdated: (handler: TSubscriptionHandler<IEventStreamUpdate>) => Subscription,
        onSearchUpdated: (handler: TSubscriptionHandler<IEventSearchUpdate>) => Subscription,
    } {
        return {
            onSessionChange: (handler: TSubscriptionHandler<string>) => {
                return this._getSubscription<string>(ControllerSessionsEvents.Events.onSessionChange, handler);
            },
            onSessionOpen: (handler: TSubscriptionHandler<string>) => {
                return this._getSubscription<string>(ControllerSessionsEvents.Events.onSessionOpen, handler);
            },
            onSessionClose: (handler: TSubscriptionHandler<string>) => {
                return this._getSubscription<string>(ControllerSessionsEvents.Events.onSessionClose, handler);
            },
            onStreamUpdated: (handler: TSubscriptionHandler<IEventStreamUpdate>) => {
                return this._getSubscription<IEventStreamUpdate>(ControllerSessionsEvents.Events.onStreamUpdated, handler);
            },
            onSearchUpdated: (handler: TSubscriptionHandler<IEventSearchUpdate>) => {
                return this._getSubscription<IEventSearchUpdate>(ControllerSessionsEvents.Events.onSearchUpdated, handler);
            },
        };
    }

Example - ControllerSessionEvents

This example shows how to call specific methods when a session is created/closed/changed:


<p>Example</p>      <!-- Create a line of text -->

p {
    color: #FFFFFF;
}

import { Component, Input } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                                                                                // Choose the selector name of the plugin
    templateUrl: './template.html',                                                                                     // Assign HTML file as template
    styleUrls: ['./styles.less']                                                                                        // Assign LESS file as style sheet file})
export class ExampleComponent {
    @Input() public api: Toolkit.IAPI;                                                                                  // API assignment
    @Input() public session: string;                                                                                    // Session ID assignment
    @Input() public sessions: Toolkit.ControllerSessionsEvents;                                                         // Session event listener assignment
    private _subs: { [key: string]: Toolkit.Subscription } = {};                                                        // Hashlist for session events
    constructor() {
        this._subs.onSessionChange = this.sessions.subscribe().onSessionChange(this._onSessionSessionChange.bind(this));    // Subscribe to session change event
        this._subs.onSessionOpen = this.sessions.subscribe().onSessionOpen(this._onSessionOpen.bind(this));                 // Subscribe to new session open event
        this._subs.onSessionClose = this.sessions.subscribe().onSessionClose(this._onSessionClose.bind(this));              // Subscribe to session close event
    }
    private _onSessionChange(session: string) {                                                                         // Method when session changes
        this.session = session;                                                                                         // Reassign the session to the session, to which has been changed to
    }
    private _onSessionOpen(session: string) { }                                                                         // Method when new session opens
    private _onSessionClose(session: string) { }                                                                        // Method when session closes
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
})
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

4.4 Logger

The API also offers a logger to log any kind of errors or warnings in the UI.

// Typescript

export default class Logger {
    private _signature;
    private _parameters;
    /**
     * @constructor
     * @param {string} signature        - Signature of logger instance
     * @param {LoggerParameters} params - Logger parameters
     */
    constructor(signature: string, params?: LoggerParameters) {}
    /**
     * Publish info logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    info(...args: any[]): string;
    /**
     * Publish warnings logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    warn(...args: any[]): string;
    /**
     * Publish verbose logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    verbose(...args: any[]): string;
    /**
     * Publish error logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    error(...args: any[]): string;
    /**
     * Publish debug logs
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    debug(...args: any[]): string;
    /**
     * Publish environment logs (low-level stuff, support or tools)
     * @param {any} args - Any input for logs
     * @returns {string} - Formatted log-string
     */
    env(...args: any[]): string;
    private _console;
    private _output;
    private _getMessage;
    private _getTime;
    private _log;
}

Example - Logger

In the example below a plugin is created which logs a message.


<p>Example</p>

p {
    color: #FFFFFF;
}

import { Component } from '@angular/core';
import * as Toolkit from 'chipmunk.client.toolkit';
@Component({
    selector: 'example',                                                        // Choose the selector name of the plugin
    templateUrl: './template.html',                                             // Assign HTML file as template
    styleUrls: ['./styles.less']                                                // Assign LESS file as style sheet file})
export class ExampleComponent {
    private _logger: Toolkit.Logger = new Toolkit.Logger('Plugin: example: ');  // Instantiate logger with signature   
    constructor() {
        this._logger.debug('Plugin started!');                                  // Create debug message
    }
}

import { NgModule } from '@angular/core';                   // Import the Angular component that is necessary for the setup below
import { ExampleComponent } from './component';             // Import the class of the plugin, mentioned in the components.ts file
import * as Toolkit from 'chipmunk.client.toolkit';         // Import Chipmunk Toolkit to let the module class inherit
@NgModule({
    declarations: [ ExampleComponent ],                     // Declare which components, directives and pipes belong to the module 
    imports: [ ],                                           // Imports other modules with the components, directives and pipes that components in the current module need
    exports: [ ExampleComponent ]                           // Provides services that the other application components can use
}
export class PluginModule extends Toolkit.PluginNgModule {  // Create module class which inherits from the Toolkit module
    constructor() {
        super('Example', 'Create an example plugin');       // Call the constructor of the parent class
    }
}

export * from './component';
export * from './module';

Go to top

Developing for chipmunk

create indexer operation with neon binding

Rust: implement streaming API

Since most operations triggered in chipmunk can take some time (mostly working on big files or streams), all functions should use events/messages to inform the chipmunk about progress, results, errors and warnings. Below is a description of how to achieve that.

your function should take 2 additional parameters:


#![allow(unused_variables)]
fn main() {
update_channel: mpsc::Sender<IndexingResults<T>>,
shutdown_rx: Option<mpsc::Receiver<()>>,
}

both are rust mpsc channels that can be used for communication between the client using your function and the function itself. the update_channel is the sender-end of a mpsc channel which means that your function can use it to send messages to the function-user. So what should/can your function send? There are 2 basic categories of events you should send: IndexingProgress<T> messages or Notification messages. We use the Result type to make the distinction. Kind of like the Either type in haskell. Either we send an Ok(event) or an Err(notification).


#![allow(unused_variables)]
fn main() {
pub type IndexingResults<T> = std::result::Result<IndexingProgress<T>, Notification>;
}

In case of an IndexingProgress, you can either send an actual result (GotItem), or report on the lifecycle state of the function (indicating progress or the end of the function)


#![allow(unused_variables)]
fn main() {
pub enum IndexingProgress<T> {
    GotItem { item: T },
    Progress { ticks: (usize, usize) },
    Stopped,
    Finished,
}
}

Note that for indicating that the function is finished, there are 2 events (Stopped and Finished) This is to distinguish between "we were stopped from outside" and "we really did finish"). For all errors that occure and should be communicated to chipmunk, you can send a Notification. This is a struct that contains the severity, some content and optionally a line number to indicate at which line the error has occurred.


#![allow(unused_variables)]
fn main() {
pub struct Notification {
    pub severity: Severity,
    pub content: String,
    pub line: Option<usize>,
}
}