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 search requests can be saved and restored as filters.
Search
To search within the opened file, type into the area with the "Type a Search Request" placeholder and press Enter
. The search results will be listed as they are with their whole line.
Note: The bottom left shows: Total lines of search results/Total lines of file
Flags
The three buttons on the right side of the search request input are flags to modify the search. Active flags will have a blue background color.
Starting from the left, the first flag defines a Case Sensitive
-search. As the name suggests, this flag will make the search case-sesitive when active.
The second flag defines a Match Whole Word
-search. With this flag active, the search only lists search results, where the search request matches with just special characters or spaces surrounding the word.
The third flag defines a Use Regular Expression
-search. With this flag active it is possible to run searches with Regex (Regular expression).
Note: It is possible to de-/activate search flags while and after starting the search.
Create filter
Note: Saved search requests will be called filter from this point on
Type a search request at the bottom and click the floppy disc icon next to the search input.
Keyboard shortcut:
Enter + CTRL (Windows) / CMD (Mac/Linux)
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.
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.
NOTE: Commenting only works for the visible area at the start of marking lines. Scrolling out of the visible area while marking lines to comment is not supported.
Reply to comments
In addition to creating comment, you can also reply to comments with no limit.
Colors
Comments can be grouped by color which can help when the view of comments is filtered by a specific color. The ordering of the comments is ascending by line numbers and can be changed to ordered by color (The order of the colors is predefined and cannot be edited).
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:
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.
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 selectSwitch 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.
NOTE: Referred Fibex files will be restored for each file it was referred to
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]
NOTE: Opening with multiple files will result in one tab for each file
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 fromToolkit.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
<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 {
@Input() public msg: string; // Expect input from host component
constructor() { }
}
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 outChapter 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 outChapter 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 outChapter 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.
NOTE: This API will soon be deprecated
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, theAPI
is bound to the life cycle of the component and gets destroyed together with the component. It is advised to bind theAPI
to the component if it is going to be used locally only by the component itself and nothing else. If theAPI
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, theAPI
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 name | Description |
---|---|
RowBoundParser | Parse only data received from the process part of the plugin |
RowCommomParser | Parse data from any kind of source |
RowTypedParser | Parse 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 createTypedRowRenderAPIColumns
andTypedRowRenderAPIExternal
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';
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)] 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)] 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)] 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)] fn main() { pub struct Notification { pub severity: Severity, pub content: String, pub line: Option<usize>, } }