├── src
├── assets
│ ├── .gitkeep
│ └── fonts
│ │ └── monaco.ttf
├── app
│ ├── app.component.scss
│ ├── components
│ │ ├── terminal
│ │ │ ├── terminal.component.sass
│ │ │ ├── terminal.component.html
│ │ │ ├── terminal.component.spec.ts
│ │ │ └── terminal.component.ts
│ │ └── title-bar
│ │ │ ├── title-bar.component.sass
│ │ │ ├── title-bar.component.spec.ts
│ │ │ ├── title-bar.component.ts
│ │ │ └── title-bar.component.html
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── providers
│ │ ├── window.service.ts
│ │ ├── electron.service.ts
│ │ ├── config.service.ts
│ │ └── terminal.service.ts
│ ├── app.module.ts
│ └── app.component.ts
├── favicon.icns
├── favicon.ico
├── favicon.png
├── favicon.256x256.png
├── favicon.512x512.png
├── favicon.1024x1024.png
├── environments
│ ├── environment.ts
│ ├── environment.prod.ts
│ └── environment.dev.ts
├── typings.d.ts
├── styles
│ ├── fonts.sass
│ ├── terminal.sass
│ ├── app.sass
│ ├── reset.sass
│ └── title-bar.sass
├── index.html
├── tsconfig.app.json
├── main.ts
├── tsconfig.spec.json
├── test.ts
└── polyfills.ts
├── postcss.config.js
├── e2e
├── tsconfig.e2e.json
└── app.e2e-spec.ts
├── .editorconfig
├── .abstruse.yml
├── tsconfig.json
├── postinstall-web.js
├── postinstall.js
├── electron-builder.json
├── .gitignore
├── LICENSE
├── keyboard-shortcuts.ts
├── README.md
├── menu.ts
├── tslint.json
├── package.json
├── main.ts
└── angular.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/src/app/components/terminal/terminal.component.sass:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/title-bar/title-bar.component.sass:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.icns
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.png
--------------------------------------------------------------------------------
/src/app/components/terminal/terminal.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/favicon.256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.256x256.png
--------------------------------------------------------------------------------
/src/favicon.512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.512x512.png
--------------------------------------------------------------------------------
/src/assets/fonts/monaco.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/assets/fonts/monaco.ttf
--------------------------------------------------------------------------------
/src/favicon.1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bleenco/bterm/HEAD/src/favicon.1024x1024.png
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'LOCAL'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: true,
3 | environment: 'PROD'
4 | };
5 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var nodeModule: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
7 | declare var window: Window;
8 | interface Window {
9 | process: any;
10 | require: any;
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out",
5 | "module": "commonjs",
6 | "target": "es2015",
7 | "types":[
8 | "jasmine",
9 | "node"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles/fonts.sass:
--------------------------------------------------------------------------------
1 | =font-face($family, $path, $weight: normal, $style: normal)
2 | @font-face
3 | font-family: $family
4 | src: url('#{$path}.ttf') format('truetype')
5 | font-weight: $weight
6 | font-style: $style
7 |
8 | +font-face('Monaco', '../assets/fonts/monaco', 400, 'normal')
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bterm
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "module": "es2015",
6 | "baseUrl": "",
7 | "types": []
8 | },
9 | "exclude": [
10 | "src/test.ts",
11 | "**/*.spec.ts",
12 | "dist",
13 | "app-builds",
14 | "node_modules"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/terminal.sass:
--------------------------------------------------------------------------------
1 | .terminal
2 | width: 100%
3 | height: calc(100% - 36px)
4 | padding: 5px 2.5px 0 2.5px
5 | display: block
6 | position: relative
7 |
8 | .terminal-instance
9 | width: 100%
10 | height: 100%
11 | display: block
12 | position: absolute
13 | top: 0
14 | left: 0
15 | right: 0
16 | bottom: 0
17 |
--------------------------------------------------------------------------------
/.abstruse.yml:
--------------------------------------------------------------------------------
1 | image: abstruse
2 |
3 | matrix:
4 | - env: SCRIPT=test NODE_VERSION=9
5 |
6 | before_install:
7 | - nvm install $NODE_VERSION
8 | - npm config set spin false
9 | - npm config set progress false
10 |
11 | install:
12 | - npm install
13 |
14 | script:
15 | - if [[ "$SCRIPT" ]]; then npm run-script $SCRIPT; fi
16 |
17 | cache:
18 | - node_modules
19 |
--------------------------------------------------------------------------------
/src/styles/app.sass:
--------------------------------------------------------------------------------
1 | @charset 'utf8'
2 |
3 | @import 'fonts'
4 | @import 'reset'
5 | @import 'title-bar'
6 | @import 'terminal'
7 | @import '../../node_modules/xterm/dist/xterm.css'
8 |
9 | html, body
10 | height: 100%
11 | width: 100%
12 | font-family: Monaco, Menlo, 'DejaVu Sans Mono', 'Ubuntu Mono', monospace
13 | font-size: 12px
14 | border: 1px solid transparent
15 |
--------------------------------------------------------------------------------
/src/environments/environment.dev.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `index.ts`, but if you do
3 | // `ng build --env=prod` then `index.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const AppConfig = {
7 | production: false,
8 | environment: 'DEV'
9 | };
10 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { AppConfig } from './environments/environment';
6 |
7 | if (AppConfig.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule, {
13 | preserveWhitespaces: false
14 | })
15 | .catch(err => console.error(err));
16 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills-test.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ],
19 | "exclude": [
20 | "dist",
21 | "app-builds",
22 | "node_modules"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "importHelpers": true,
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es5",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "es2016",
18 | "es2015",
19 | "dom"
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/postinstall-web.js:
--------------------------------------------------------------------------------
1 | // Allow angular using electron module (native node modules)
2 | const fs = require('fs');
3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
4 |
5 | fs.readFile(f_angular, 'utf8', function (err, data) {
6 | if (err) {
7 | return console.log(err);
8 | }
9 | var result = data.replace(/target: "electron-renderer",/g, '');
10 | var result = result.replace(/target: "web",/g, '');
11 | var result = result.replace(/return \{/g, 'return {target: "web",');
12 |
13 | fs.writeFile(f_angular, result, 'utf8', function (err) {
14 | if (err) return console.log(err);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/postinstall.js:
--------------------------------------------------------------------------------
1 | // Allow angular using electron module (native node modules)
2 | const fs = require('fs');
3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
4 |
5 | fs.readFile(f_angular, 'utf8', function (err, data) {
6 | if (err) {
7 | return console.log(err);
8 | }
9 | var result = data.replace(/target: "electron-renderer",/g, '');
10 | var result = result.replace(/target: "web",/g, '');
11 | var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');
12 |
13 | fs.writeFile(f_angular, result, 'utf8', function (err) {
14 | if (err) return console.log(err);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/components/terminal/terminal.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TerminalComponent } from './terminal.component';
4 |
5 | describe('TerminalComponent', () => {
6 | let component: TerminalComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ TerminalComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(TerminalComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/components/title-bar/title-bar.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TitleBarComponent } from './title-bar.component';
4 |
5 | describe('TitleBarComponent', () => {
6 | let component: TitleBarComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ TitleBarComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(TitleBarComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "productName": "bterm",
3 | "directories": {
4 | "output": "app-builds"
5 | },
6 | "files": [
7 | "**/*",
8 | "!*.ts",
9 | "!*.code-workspace",
10 | "!LICENSE.md",
11 | "!package.json",
12 | "!package-lock.json",
13 | "!src/",
14 | "!e2e/",
15 | "!hooks/",
16 | "!.angular-cli.json",
17 | "!_config.yml",
18 | "!karma.conf.js",
19 | "!tsconfig.json",
20 | "!tslint.json"
21 | ],
22 | "win": {
23 | "icon": "dist",
24 | "target": [
25 | "portable"
26 | ]
27 | },
28 | "mac": {
29 | "icon": "dist",
30 | "target": [
31 | "dmg"
32 | ],
33 | "identity": null
34 | },
35 | "linux": {
36 | "icon": "dist",
37 | "target": [
38 | "deb"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /app-builds
8 | /e2e/out
9 | main.js
10 | menu.js
11 | keyboard-shortcuts.js
12 |
13 | # dependencies
14 | /node_modules
15 |
16 | # IDEs and editors
17 | /.idea
18 | .project
19 | .classpath
20 | .c9/
21 | *.launch
22 | .settings/
23 | *.sublime-workspace
24 |
25 | # IDE - VSCode
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 |
32 | # misc
33 | /.sass-cache
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | npm-debug.log
38 | testem.log
39 | /typings
40 | package-lock.json
41 |
42 | # e2e
43 | /e2e/*.js
44 | /e2e/*.map
45 |
46 | # System Files
47 | .DS_Store
48 | Thumbs.db
49 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 | import { ElectronService } from './providers/electron.service';
5 |
6 | describe('AppComponent', () => {
7 | beforeEach(async(() => {
8 | TestBed.configureTestingModule({
9 | declarations: [
10 | AppComponent
11 | ],
12 | providers : [
13 | ElectronService
14 | ],
15 | imports: [RouterTestingModule]
16 | }).compileComponents();
17 | }));
18 |
19 | it('should create the app', async(() => {
20 | const fixture = TestBed.createComponent(AppComponent);
21 | const app = fixture.debugElement.componentInstance;
22 | expect(app).toBeTruthy();
23 | }));
24 | });
25 |
--------------------------------------------------------------------------------
/src/app/providers/window.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject, fromEvent } from 'rxjs';
3 | import { map, distinctUntilChanged } from 'rxjs/operators';
4 |
5 | export interface WindowSize {
6 | width: number;
7 | height: number;
8 | }
9 |
10 | @Injectable()
11 | export class WindowService {
12 | size: BehaviorSubject;
13 |
14 | constructor() {
15 | this.size = new BehaviorSubject(this.getWindowSize());
16 | fromEvent(window, 'resize')
17 | .pipe(
18 | map((): WindowSize => this.getWindowSize()),
19 | distinctUntilChanged()
20 | )
21 | .subscribe(this.size);
22 | }
23 |
24 | getWindowSize(): WindowSize {
25 | return {
26 | width: window.innerWidth,
27 | height: window.innerHeight
28 | };
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/app/providers/electron.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | // If you import a module but never use any of the imported values other than as TypeScript types,
4 | // the resulting javascript file will look as if you never imported the module at all.
5 | import { ipcRenderer } from 'electron';
6 | import * as childProcess from 'child_process';
7 |
8 | @Injectable()
9 | export class ElectronService {
10 |
11 | ipcRenderer: typeof ipcRenderer;
12 | childProcess: typeof childProcess;
13 |
14 | constructor() {
15 | // Conditional imports
16 | if (this.isElectron()) {
17 | this.ipcRenderer = window.require('electron').ipcRenderer;
18 | this.childProcess = window.require('child_process');
19 | }
20 | }
21 |
22 | isElectron = () => {
23 | return window && window.process && window.process.type;
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { ElectronService } from './providers/electron.service';
5 | import { TerminalService } from './providers/terminal.service';
6 | import { WindowService } from './providers/window.service';
7 | import { ConfigService } from './providers/config.service';
8 |
9 | import { AppComponent } from './app.component';
10 | import { TerminalComponent } from './components/terminal/terminal.component';
11 | import { TitleBarComponent } from './components/title-bar/title-bar.component';
12 |
13 | @NgModule({
14 | declarations: [
15 | AppComponent,
16 | TerminalComponent,
17 | TitleBarComponent
18 | ],
19 | imports: [
20 | BrowserModule
21 | ],
22 | providers: [ElectronService, TerminalService, WindowService, ConfigService],
23 | bootstrap: [AppComponent]
24 | })
25 | export class AppModule { }
26 |
--------------------------------------------------------------------------------
/src/styles/reset.sass:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | p,
4 | ol,
5 | ul,
6 | li,
7 | dl,
8 | dt,
9 | dd,
10 | blockquote,
11 | figure,
12 | fieldset,
13 | legend,
14 | textarea,
15 | pre,
16 | iframe,
17 | hr,
18 | h1,
19 | h2,
20 | h3,
21 | h4,
22 | h5,
23 | h6
24 | margin: 0
25 | padding: 0
26 |
27 | // Headings
28 | h1,
29 | h2,
30 | h3,
31 | h4,
32 | h5,
33 | h6
34 | font-size: 100%
35 | font-weight: normal
36 |
37 | // List
38 | ul
39 | list-style: none
40 |
41 | // Form
42 | button,
43 | input,
44 | select,
45 | textarea
46 | margin: 0
47 |
48 | // Box sizing
49 | html
50 | box-sizing: border-box
51 |
52 | *
53 | box-sizing: inherit
54 | &:before,
55 | &:after
56 | box-sizing: inherit
57 |
58 | // Media
59 | img,
60 | embed,
61 | object,
62 | audio,
63 | video
64 | max-width: 100%
65 |
66 | // Iframe
67 | iframe
68 | border: 0
69 |
70 | // Table
71 | table
72 | border-collapse: collapse
73 | border-spacing: 0
74 |
75 | td,
76 | th
77 | padding: 0
78 | text-align: left
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 Bleenco GmbH https://bleenco.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare const __karma__: any;
17 | declare const require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/src/app/components/title-bar/title-bar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, Renderer2, OnInit } from '@angular/core';
2 | import { TerminalService } from '../../providers/terminal.service';
3 | import { ipcRenderer } from 'electron';
4 | import { platform } from 'os';
5 |
6 | @Component({
7 | selector: 'app-title-bar',
8 | templateUrl: './title-bar.component.html'
9 | })
10 | export class TitleBarComponent implements OnInit {
11 | constructor(public terminalService: TerminalService, private elementRef: ElementRef, private renderer: Renderer2) { }
12 |
13 | ngOnInit() {
14 | const header = this.elementRef.nativeElement.querySelector('.header');
15 | const buttons = this.elementRef.nativeElement.querySelector('.window-buttons');
16 | if (platform() === 'darwin') {
17 | this.renderer.addClass(header, 'is-draggable');
18 | this.renderer.addClass(buttons, 'is-enabled');
19 | }
20 | }
21 |
22 | closeTab(index: number): void {
23 | this.terminalService.destroy(index);
24 | }
25 |
26 | closeWindow(): void {
27 | this.terminalService.destroyAll();
28 | }
29 |
30 | minimizeWindow(): void {
31 | ipcRenderer.send('minimize');
32 | }
33 |
34 | maximizeWindow(): void {
35 | ipcRenderer.send('maximize');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/components/title-bar/title-bar.component.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject, Renderer2, OnInit } from '@angular/core';
2 | import { DOCUMENT } from '@angular/platform-browser';
3 | import { ConfigService } from './providers/config.service';
4 | import { TerminalService } from './providers/terminal.service';
5 | import { filter } from 'rxjs/operators';
6 |
7 | @Component({
8 | selector: 'app-root',
9 | templateUrl: './app.component.html',
10 | styleUrls: ['./app.component.scss']
11 | })
12 | export class AppComponent implements OnInit {
13 | constructor(
14 | @Inject(DOCUMENT) private document: any,
15 | private renderer: Renderer2,
16 | private config: ConfigService,
17 | private terminal: TerminalService
18 | ) { }
19 |
20 | ngOnInit() {
21 | this.config.body = this.document.querySelector('body');
22 |
23 | this.config.configEvents.subscribe(config => {
24 | this.renderer.setStyle(this.config.body, 'color', config.colors.foreground);
25 | this.renderer.setStyle(this.config.body, 'background', config.colors.background);
26 | this.renderer.setStyle(this.config.body, 'border-color', config.borderColor);
27 |
28 | this.terminal.terminals.forEach(terminal => {
29 | terminal.term.setOption('fontFamily', config.fontFamily);
30 | terminal.term.setOption('fontSize', config.fontSize);
31 | terminal.term.setOption('theme', config.colors);
32 | });
33 | });
34 |
35 | this.terminal.events
36 | .pipe(filter(x => x.type === 'create'))
37 | .subscribe(() => this.config.getConfig());
38 |
39 | this.config.initWatcher();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/keyboard-shortcuts.ts:
--------------------------------------------------------------------------------
1 | export const keyboardShortcuts = [
2 | {keypress: 'CommandOrControl+Shift+Left', sctype: 'send', sckey: 'tabLeft', scvalue: true },
3 | {keypress: 'CommandOrControl+Shift+Right', sctype: 'send', sckey: 'tabRight', scvalue: true },
4 | {keypress: 'CommandOrControl+T', sctype: 'send', sckey: 'newTab', scvalue: true },
5 | {keypress: 'CommandOrControl+W', sctype: 'send', sckey: 'closeTab', scvalue: true },
6 | {keypress: 'CommandOrControl+K', sctype: 'send', sckey: 'clearTab', scvalue: true },
7 | {keypress: 'CommandOrControl+1', sctype: 'send', sckey: 'switchTab', scvalue: 0 },
8 | {keypress: 'CommandOrControl+2', sctype: 'send', sckey: 'switchTab', scvalue: 1 },
9 | {keypress: 'CommandOrControl+3', sctype: 'send', sckey: 'switchTab', scvalue: 2 },
10 | {keypress: 'CommandOrControl+4', sctype: 'send', sckey: 'switchTab', scvalue: 3 },
11 | {keypress: 'CommandOrControl+5', sctype: 'send', sckey: 'switchTab', scvalue: 4 },
12 | {keypress: 'CommandOrControl+6', sctype: 'send', sckey: 'switchTab', scvalue: 5 },
13 | {keypress: 'CommandOrControl+7', sctype: 'send', sckey: 'switchTab', scvalue: 6 },
14 | {keypress: 'CommandOrControl+8', sctype: 'send', sckey: 'switchTab', scvalue: 7 },
15 | {keypress: 'CommandOrControl+9', sctype: 'send', sckey: 'switchTab', scvalue: 8 },
16 | {keypress: 'CommandOrControl+0', sctype: 'send', sckey: 'switchTab', scvalue: 9 },
17 | {keypress: 'CommandOrControl+V', sctype: 'send', sckey: 'paste', scvalue: null },
18 | {keypress: 'CommandOrControl+N', sctype: 'createWindow', sckey: 'createWindow', scvalue: null },
19 | {keypress: 'CommandOrControl+Shift+I', sctype: 'toggleDevTools', sckey: 'toggleDevTools', scvalue: null }
20 | ];
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bterm — :warning: This project is no longer maintained. :warning:
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://ci.bleenco.io/repo/2)
9 |
10 | ## Overview
11 | Fully customisable cross-platform terminal that works and feels the same way everywhere: MacOS, Linux and Windows.
12 |
13 | ## Download
14 | To download visit
15 | [http://bterm.bleenco.io](http://bterm.bleenco.io) and hit the right button to get your favourite OS installer.
16 |
17 | On the first run `~/.bterm2.json` (on Linux and MacOS) or `C:\Users\user\.bterm.json` (on Windows) configuration file is created. It contains the attributes in json format that define the layout of the terminal, i.e. changing the attribute `settings > fonts` will immediately update the type of fonts.
18 |
19 | ## Settings and customization
20 |
21 | The default theme is a combination of black and white visuals.
22 | Settings are stored in `.bterm2.json` configuration file where you can hack your terminals' visual settings.
23 |
24 |
25 | ## Hacking on bterm
26 | In order to run bterm locally in a development mode please use the following commands:
27 |
28 | ```sh
29 | git clone https://github.com/bleenco/bterm.git
30 | npm install
31 | npm start
32 | ```
33 |
34 | ### Production build
35 | To generate bterm production build and installation package, run the following commands:
36 |
37 | ```sh
38 | npm run electron:[mac | linux | windows]
39 | ```
40 |
41 | The executable installation package can be found in `app-builds` folder.
42 |
43 | ### LICENCE
44 |
45 | MIT
46 |
--------------------------------------------------------------------------------
/menu.ts:
--------------------------------------------------------------------------------
1 | import { Menu, app } from 'electron';
2 |
3 | export function getMenu(): Menu {
4 | const menu = Menu.buildFromTemplate(template as any);
5 | Menu.setApplicationMenu(menu);
6 |
7 | return menu;
8 | }
9 |
10 | const template = [
11 | {
12 | label: app.getName(),
13 | submenu: [
14 | {role: 'about'},
15 | {type: 'separator'},
16 | {role: 'services', submenu: []},
17 | {type: 'separator'},
18 | {role: 'hide'},
19 | {role: 'hideothers'},
20 | {role: 'unhide'},
21 | {type: 'separator'},
22 | {role: 'quit'}
23 | ]
24 | },
25 | {
26 | label: 'Edit',
27 | submenu: [
28 | {role: 'undo'},
29 | {role: 'redo'},
30 | {type: 'separator'},
31 | {role: 'cut'},
32 | {role: 'copy'},
33 | {role: 'paste'},
34 | {role: 'pasteandmatchstyle'},
35 | {role: 'delete'},
36 | {role: 'selectall'}
37 | ]
38 | },
39 | {
40 | label: 'View',
41 | submenu: [
42 | {role: 'togglefullscreen'},
43 | {type: 'separator'},
44 | {role: 'toggledevtools'},
45 | {type: 'separator'},
46 | {role: 'resetzoom'},
47 | {role: 'zoomin'},
48 | {role: 'zoomout'},
49 | {type: 'separator'},
50 | { label: 'Tab Left', accelerator: 'CommandOrControl+Shift+Left', selector: 'Left' },
51 | { label: 'Tab Right', accelerator: 'CommandOrControl+Shift+Right', selector: 'Right' }
52 | ]
53 | },
54 | {
55 | role: 'window',
56 | submenu: [
57 | {role: 'minimize'},
58 | {role: 'close'}
59 | ]
60 | },
61 | {
62 | role: 'help',
63 | submenu: [
64 | {
65 | label: 'Learn More',
66 | click () { require('electron').shell.openExternal('http://bterm.bleenco.io') }
67 | }
68 | ]
69 | }
70 | ];
71 |
--------------------------------------------------------------------------------
/src/styles/title-bar.sass:
--------------------------------------------------------------------------------
1 | .header
2 | height: 36px
3 | width: 100%
4 | position: relative
5 | border-top-left-radius: 4px
6 | border-top-right-radius: 4px
7 |
8 | &.is-draggable
9 | -webkit-app-region: drag
10 | -webkit-user-select: none
11 |
12 | .tabs
13 | display: flex
14 |
15 | .tab
16 | width: 100%
17 | min-width: 0
18 | margin-left: -1px
19 | position: relative
20 | height: 36px
21 | border: 1px solid #333
22 | border-top: none
23 | transition: border 50ms ease-out
24 |
25 | &:first-child
26 | border-left: none
27 |
28 | &:first-child:not(:last-child)
29 | .title
30 | padding-left: 65px
31 |
32 | &:last-child
33 | border-right: none
34 |
35 | &.is-active
36 | border-color: transparent
37 |
38 | svg
39 | width: 12px
40 | height: 12px
41 | position: absolute
42 | top: 12px
43 | right: 10px
44 | cursor: pointer
45 | opacity: 0
46 | transition: opacity 150ms ease-in 300ms
47 |
48 | .title
49 | display: block
50 | text-align: center
51 | width: calc(100%)
52 | height: 100%
53 | overflow: hidden
54 | text-overflow: ellipsis
55 | white-space: nowrap
56 | padding: 10px 25px
57 |
58 | &:hover
59 |
60 | svg
61 | opacity: 1
62 |
63 | .window-buttons
64 | position: absolute
65 | left: 10px
66 | top: 12px
67 | z-index: 10
68 | display: none
69 |
70 | &.is-enabled
71 | display: inline
72 |
73 | .circle-button
74 | width: 12px
75 | height: 12px
76 | display: block
77 | float: left
78 | border-radius: 50%
79 | margin-right: 7px
80 | cursor: pointer
81 |
82 | &.close
83 | background: #FF6157
84 | border: 0.5px solid #E24640
85 |
86 | &.minimize
87 | background: #FFC12F
88 | border: 0.5px solid #DFA023
89 |
90 | &.maximize
91 | background: #2ACB42
92 | border: 0.5px solid #1BAC2C
93 |
94 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/set';
35 |
36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
37 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
38 |
39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */
40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
41 |
42 |
43 | /** Evergreen browsers require these. **/
44 | import 'core-js/es6/reflect';
45 |
46 |
47 |
48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/
49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * Zone JS is required by Angular itself.
55 | */
56 | import 'zone.js/dist/zone-mix'; // Included with Angular CLI.
57 |
58 |
59 | /***************************************************************************************************
60 | * APPLICATION IMPORTS
61 | */
62 |
63 | /**
64 | * Date, currency, decimal and percent pipes.
65 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
66 | */
67 | // import 'intl'; // Run `npm install --save intl`.
68 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-use-before-declare": true,
76 | "no-var-keyword": true,
77 | "object-literal-sort-keys": false,
78 | "one-line": [
79 | true,
80 | "check-open-brace",
81 | "check-catch",
82 | "check-else",
83 | "check-whitespace"
84 | ],
85 | "prefer-const": true,
86 | "quotemark": [
87 | true,
88 | "single"
89 | ],
90 | "radix": true,
91 | "semicolon": [
92 | true,
93 | "always"
94 | ],
95 | "triple-equals": [
96 | true,
97 | "allow-null-check"
98 | ],
99 | "typedef-whitespace": [
100 | true,
101 | {
102 | "call-signature": "nospace",
103 | "index-signature": "nospace",
104 | "parameter": "nospace",
105 | "property-declaration": "nospace",
106 | "variable-declaration": "nospace"
107 | }
108 | ],
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "no-output-on-prefix": true,
120 | "use-input-property-decorator": true,
121 | "use-output-property-decorator": true,
122 | "use-host-property-decorator": true,
123 | "no-input-rename": true,
124 | "no-output-rename": true,
125 | "use-life-cycle-interface": true,
126 | "use-pipe-transform-interface": true,
127 | "component-class-suffix": true,
128 | "directive-class-suffix": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bterm",
3 | "version": "2.0.0",
4 | "description": "bterm",
5 | "author": "Bleenco OSS Team ",
6 | "main": "main.js",
7 | "private": true,
8 | "scripts": {
9 | "postinstall": "npm run postinstall:electron && npx electron-builder install-app-deps",
10 | "postinstall:web": "node postinstall-web",
11 | "postinstall:electron": "node postinstall",
12 | "ng": "ng",
13 | "start": "npm run postinstall:electron && npm-run-all -p ng:serve electron:serve",
14 | "build": "npm run postinstall:electron && npm run electron:tsc && ng build",
15 | "build:dev": "npm run build -- -c dev",
16 | "build:prod": "npm run build -- -c production",
17 | "ng:serve": "ng serve",
18 | "ng:serve:web": "npm run postinstall:web && ng serve -o",
19 | "electron:tsc": "tsc main.ts",
20 | "electron:serve": "wait-on http-get://localhost:4200/ && npm run electron:tsc && electron . --serve",
21 | "electron:local": "npm run build:prod && electron .",
22 | "electron:linux": "npm run build:prod && npx electron-builder build --linux",
23 | "electron:windows": "npm run build:prod && npx electron-builder build --windows",
24 | "electron:mac": "npm run build:prod && npx electron-builder build --mac",
25 | "test": "npm run postinstall:web && ng test",
26 | "e2e": "npm run postinstall:web && ng e2e"
27 | },
28 | "dependencies": {
29 | "node-pty": "^0.8.0",
30 | "shelljs": "^0.8.3",
31 | "tslib": "^1.9.3",
32 | "xterm": "^3.9.1"
33 | },
34 | "devDependencies": {
35 | "@angular-devkit/build-angular": "~0.11.4",
36 | "@angular/cli": "7.1.4",
37 | "@angular/common": "7.1.4",
38 | "@angular/compiler": "7.1.4",
39 | "@angular/compiler-cli": "7.1.4",
40 | "@angular/core": "7.1.4",
41 | "@angular/forms": "7.1.4",
42 | "@angular/http": "7.1.4",
43 | "@angular/language-service": "7.1.4",
44 | "@angular/platform-browser": "7.1.4",
45 | "@angular/platform-browser-dynamic": "7.1.4",
46 | "@angular/router": "7.1.4",
47 | "@types/jasmine": "3.3.5",
48 | "@types/node": "10.12.18",
49 | "@types/shelljs": "^0.8.1",
50 | "bulma": "^0.7.2",
51 | "circular-dependency-plugin": "5.0.2",
52 | "codelyzer": "4.5.0",
53 | "copy-webpack-plugin": "4.6.0",
54 | "copyfiles": "2.1.0",
55 | "core-js": "2.6.1",
56 | "cross-env": "5.2.0",
57 | "css-loader": "2.1.0",
58 | "cssnano": "4.1.8",
59 | "electron": "4.0.0",
60 | "electron-builder": "20.38.4",
61 | "electron-rebuild": "^1.8.2",
62 | "electron-reload": "1.4.0",
63 | "enhanced-resolve": "4.1.0",
64 | "exports-loader": "0.7.0",
65 | "file-loader": "3.0.1",
66 | "html-loader": "0.5.5",
67 | "html-webpack-plugin": "3.2.0",
68 | "jasmine": "^3.3.1",
69 | "jasmine-core": "3.3.0",
70 | "jasmine-spec-reporter": "4.2.1",
71 | "json-loader": "0.5.7",
72 | "less-loader": "4.1.0",
73 | "minimist": "1.2.0",
74 | "mkdirp": "0.5.1",
75 | "npm-run-all": "4.1.5",
76 | "npx": "10.2.0",
77 | "raw-loader": "1.0.0",
78 | "rxjs": "6.3.3",
79 | "sass-loader": "7.1.0",
80 | "script-loader": "0.7.2",
81 | "source-map-loader": "0.2.4",
82 | "spectron": "^5.0.0",
83 | "style-loader": "0.23.1",
84 | "stylus-loader": "3.0.2",
85 | "ts-node": "7.0.1",
86 | "tslint": "5.12.0",
87 | "typescript": "~3.1.6",
88 | "uglifyjs-webpack-plugin": "2.1.0",
89 | "url-loader": "1.1.2",
90 | "wait-on": "^3.2.0",
91 | "webdriver-manager": "12.1.1",
92 | "webpack": "4.28.2",
93 | "webpack-concat-plugin": "3.0.0",
94 | "webpack-dev-server": "3.1.14",
95 | "zone.js": "0.8.26"
96 | },
97 | "license": "MIT"
98 | }
99 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, globalShortcut } from 'electron';
2 | import { getMenu } from './menu';
3 | import { keyboardShortcuts } from './keyboard-shortcuts';
4 | import * as path from 'path';
5 | import * as url from 'url';
6 | import { platform } from 'os';
7 |
8 | const args = process.argv.slice(1);
9 | const serve = args.some(val => val === '--serve');
10 | const os = platform();
11 | const showFrame = os === 'darwin' ? false : true;
12 |
13 | let currentWindow: BrowserWindow = null;
14 | let windows: BrowserWindow[] = [];
15 |
16 | if (serve) {
17 | require('electron-reload')(__dirname, {
18 | });
19 | }
20 |
21 | app.on('ready', () => {
22 | createWindow();
23 | });
24 |
25 | app.on('window-all-closed', () => {
26 | if (process.platform !== 'darwin') {
27 | app.quit();
28 | }
29 | });
30 |
31 | app.on('activate', () => {
32 | if (!currentWindow) {
33 | createWindow();
34 | }
35 | });
36 |
37 | ipcMain.on('minimize', () => currentWindow.minimize());
38 | ipcMain.on('tabMaximize', () => currentWindow.isMaximized() ? currentWindow.unmaximize() : currentWindow.maximize());
39 | ipcMain.on('maximize', () => {
40 | const isMac = process.platform === 'darwin';
41 | if (isMac) {
42 | currentWindow.setFullScreen(!currentWindow.isFullScreen());
43 | } else {
44 | currentWindow.isMaximized() ? currentWindow.unmaximize() : currentWindow.maximize();
45 | }
46 | });
47 | ipcMain.on('close', (ev, id) => {
48 | if (!currentWindow) {
49 | return;
50 | }
51 |
52 | currentWindow.close();
53 | });
54 |
55 | function createWindow(): void {
56 | const width = 640;
57 | const height = 480;
58 |
59 | const win = new BrowserWindow({
60 | width,
61 | height,
62 | center: true,
63 | frame: showFrame,
64 | show: false,
65 | transparent: true
66 | });
67 |
68 | win.setMenu(getMenu());
69 |
70 | win.loadURL(url.format({
71 | protocol: 'file:',
72 | pathname: path.join(__dirname, '/index.html'),
73 | slashes: true
74 | }));
75 |
76 | if (serve) {
77 | require('electron-reload')(__dirname, {
78 | electron: require(`${__dirname}/node_modules/electron`)
79 | });
80 | win.loadURL('http://localhost:4200');
81 | } else {
82 | win.loadURL(url.format({
83 | pathname: path.join(__dirname, 'dist/index.html'),
84 | protocol: 'file:',
85 | slashes: true
86 | }));
87 | }
88 |
89 | registerShortcuts(win);
90 |
91 | win.once('ready-to-show', () => win.show());
92 | win.on('blur', () => {
93 | currentWindow = null;
94 | unregisterShortcuts();
95 | });
96 | win.on('focus', () => {
97 | currentWindow = win;
98 | registerShortcuts(win);
99 | });
100 | win.on('move', event => win.webContents.send('move', event));
101 | win.on('close', () => {
102 | windows = windows.filter(w => w.id !== currentWindow.id);
103 | currentWindow = null;
104 | unregisterShortcuts();
105 | });
106 | win.on('resize', event => win.webContents.send('resize', event));
107 | win.on('restore', event => win.webContents.send('restore', event));
108 | win.on('enter-full-screen', event => win.webContents.send('enter-full-screen', event));
109 | win.on('leave-full-screen', event => win.webContents.send('leave-full-screen', event));
110 |
111 | windows.push(win);
112 | }
113 |
114 | function registerShortcuts(window: any): void {
115 | const keypressFunctions = {
116 | send: (key, value) => window.webContents.send(key, value),
117 | toggleDevTools: () => window.webContents.toggleDevTools(),
118 | createWindow: () => createWindow()
119 | };
120 |
121 | keyboardShortcuts.forEach(shortcut => {
122 | globalShortcut.register(shortcut.keypress, () => {
123 | keypressFunctions[shortcut.sctype](shortcut.sckey, shortcut.scvalue);
124 | });
125 | });
126 | }
127 |
128 | function unregisterShortcuts(): void {
129 | keyboardShortcuts.forEach(shortcut => {
130 | globalShortcut.unregister(shortcut.keypress);
131 | });
132 | }
133 |
--------------------------------------------------------------------------------
/src/app/providers/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, EventEmitter } from '@angular/core';
2 | import { homedir } from 'os';
3 | import { existsSync, writeFileSync, readFileSync, watchFile } from 'fs';
4 | import { ITheme } from 'xterm';
5 |
6 | export interface BtermConfig {
7 | fontFamily: string;
8 | fontSize: number;
9 | baseTheme: 'light' | 'dark';
10 | themeOverrides: ITheme;
11 | borderColor: string;
12 | }
13 |
14 | export interface ParsedConfig {
15 | fontFamily: string;
16 | fontSize: number;
17 | borderColor: string;
18 | colors: {
19 | foreground?: string;
20 | background?: string;
21 | cursor?: string;
22 | cursorAccent?: string;
23 | selection?: string;
24 | black?: string;
25 | red?: string;
26 | green?: string;
27 | yellow?: string;
28 | blue?: string;
29 | magenta?: string;
30 | cyan?: string;
31 | white?: string;
32 | brightRed?: string;
33 | brightGreen?: string;
34 | brightYellow?: string;
35 | brightBlue?: string;
36 | brightMagenta?: string;
37 | brightCyan?: string;
38 | brightWhite?: string;
39 | };
40 | }
41 |
42 | @Injectable()
43 | export class ConfigService {
44 | home: string;
45 | configPath: string;
46 | lightTheme: ITheme;
47 | darkTheme: ITheme;
48 | configEvents: EventEmitter;
49 |
50 | body: HTMLBodyElement;
51 |
52 | constructor() {
53 | this.home = homedir();
54 | this.configPath = this.home + '/.bterm2.json';
55 | this.configEvents = new EventEmitter();
56 |
57 | this.lightTheme = {
58 | foreground: '#000000',
59 | background: '#ffffff',
60 | cursor: '#000000',
61 | cursorAccent: '#000000',
62 | selection: 'rgba(0, 0, 0, 0.1)',
63 | black: '#000000',
64 | red: '#de3e35',
65 | green: '#3f953a',
66 | yellow: '#d2b67c',
67 | blue: '#2f5af3',
68 | magenta: '#950095',
69 | cyan: '#3f953a',
70 | white: '#bbbbbb',
71 | brightBlack: '#000000',
72 | brightRed: '#de3e35',
73 | brightGreen: '#3f953a',
74 | brightYellow: '#d2b67c',
75 | brightBlue: '#2f5af3',
76 | brightMagenta: '#a00095',
77 | brightCyan: '#3f953a',
78 | brightWhite: '#ffffff'
79 | };
80 |
81 | this.darkTheme = {
82 | foreground: '#F8F8F2',
83 | background: '#090E15',
84 | cursor: '#bd93f9',
85 | cursorAccent: '#bd93f9',
86 | selection: 'rgba(241, 250, 140, 0.3)',
87 | black: '#090E15',
88 | red: '#ff5555',
89 | green: '#50fa7b',
90 | yellow: '#f1fa8c',
91 | blue: '#96ECFD',
92 | magenta: '#bd93f9',
93 | cyan: '#8be9fd',
94 | white: '#ffffff',
95 | brightBlack: '#666666',
96 | brightRed: '#ff5555',
97 | brightGreen: '#50fa7b',
98 | brightYellow: '#f1fa8c',
99 | brightBlue: '#96ECFD',
100 | brightMagenta: '#bd93f9',
101 | brightCyan: '#8be9fd',
102 | brightWhite: '#ffffff'
103 | };
104 |
105 | if (!existsSync(this.configPath)) {
106 | this.writeConfig(this.defaultConfig());
107 | }
108 | }
109 |
110 | initWatcher(): void {
111 | watchFile(this.configPath, () => {
112 | if (!existsSync(this.configPath)) {
113 | this.writeConfig(this.defaultConfig());
114 | }
115 |
116 | this.getConfig();
117 | });
118 | }
119 |
120 | getConfig(): void {
121 | const btermConfig = this.readConfig();
122 | const parsedConfig: ParsedConfig = {
123 | fontFamily: btermConfig.fontFamily,
124 | fontSize: btermConfig.fontSize,
125 | borderColor: btermConfig.borderColor,
126 | colors: Object.assign({}, btermConfig.baseTheme === 'light' ? this.lightTheme : this.darkTheme, btermConfig.themeOverrides)
127 | };
128 |
129 | this.configEvents.emit(parsedConfig);
130 | }
131 |
132 | defaultConfig(): BtermConfig {
133 | return {
134 | fontFamily: 'Monaco, Menlo, \'DejaVu Sans Mono\', \'Ubuntu Mono\', monospace',
135 | fontSize: 12,
136 | baseTheme: 'dark',
137 | themeOverrides: {},
138 | borderColor: '#222222'
139 | };
140 | }
141 |
142 | readConfig(): BtermConfig {
143 | return JSON.parse(readFileSync(this.configPath, { encoding: 'utf8' }));
144 | }
145 |
146 | writeConfig(config: BtermConfig): void {
147 | writeFileSync(this.configPath, JSON.stringify(config, null, 2), { encoding: 'utf8' });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "bterm": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:browser",
13 | "options": {
14 | "outputPath": "dist",
15 | "index": "src/index.html",
16 | "main": "src/main.ts",
17 | "tsConfig": "src/tsconfig.app.json",
18 | "polyfills": "src/polyfills.ts",
19 | "assets": [
20 | "src/assets",
21 | "src/favicon.ico",
22 | "src/favicon.ico",
23 | "src/favicon.png",
24 | "src/favicon.icns",
25 | "src/favicon.256x256.png",
26 | "src/favicon.512x512.png",
27 | "src/favicon.1024x1024.png"
28 | ],
29 | "styles": [
30 | "src/styles/app.sass"
31 | ],
32 | "scripts": []
33 | },
34 | "configurations": {
35 | "dev": {
36 | "optimization": false,
37 | "outputHashing": "all",
38 | "sourceMap": true,
39 | "extractCss": true,
40 | "namedChunks": false,
41 | "aot": false,
42 | "extractLicenses": true,
43 | "vendorChunk": false,
44 | "buildOptimizer": false,
45 | "fileReplacements": [
46 | {
47 | "replace": "src/environments/environment.ts",
48 | "with": "src/environments/environment.dev.ts"
49 | }
50 | ]
51 | },
52 | "production": {
53 | "optimization": true,
54 | "outputHashing": "all",
55 | "sourceMap": false,
56 | "extractCss": true,
57 | "namedChunks": false,
58 | "aot": true,
59 | "extractLicenses": true,
60 | "vendorChunk": false,
61 | "buildOptimizer": true,
62 | "fileReplacements": [
63 | {
64 | "replace": "src/environments/environment.ts",
65 | "with": "src/environments/environment.prod.ts"
66 | }
67 | ]
68 | }
69 | }
70 | },
71 | "serve": {
72 | "builder": "@angular-devkit/build-angular:dev-server",
73 | "options": {
74 | "browserTarget": "bterm:build"
75 | },
76 | "configurations": {
77 | "prod": {
78 | "browserTarget": "bterm:build:prod"
79 | }
80 | }
81 | },
82 | "extract-i18n": {
83 | "builder": "@angular-devkit/build-angular:extract-i18n",
84 | "options": {
85 | "browserTarget": "bterm:build"
86 | }
87 | },
88 | "test": {
89 | "builder": "@angular-devkit/build-angular:karma",
90 | "options": {
91 | "main": "src/test.ts",
92 | "karmaConfig": "./karma.conf.js",
93 | "polyfills": "src/polyfills.ts",
94 | "tsConfig": "src/tsconfig.spec.json",
95 | "scripts": [],
96 | "styles": [
97 | "src/styles/app.sass"
98 | ],
99 | "assets": [
100 | "src/assets",
101 | "src/favicon.ico"
102 | ]
103 | }
104 | },
105 | "lint": {
106 | "builder": "@angular-devkit/build-angular:tslint",
107 | "options": {
108 | "tsConfig": [
109 | "src/tsconfig.app.json",
110 | "src/tsconfig.spec.json"
111 | ],
112 | "exclude": []
113 | }
114 | }
115 | }
116 | },
117 | "bterm-e2e": {
118 | "root": "",
119 | "sourceRoot": "",
120 | "projectType": "application",
121 | "architect": {
122 | "e2e": {
123 | "builder": "@angular-devkit/build-angular:protractor",
124 | "options": {
125 | "protractorConfig": "./protractor.conf.js",
126 | "devServerTarget": "bterm:serve"
127 | }
128 | },
129 | "lint": {
130 | "builder": "@angular-devkit/build-angular:tslint",
131 | "options": {
132 | "tsConfig": [
133 | "e2e/tsconfig.e2e.json"
134 | ],
135 | "exclude": []
136 | }
137 | }
138 | }
139 | }
140 | },
141 | "defaultProject": "bterm",
142 | "schematics": {
143 | "@schematics/angular:component": {
144 | "prefix": "app",
145 | "styleext": "sass"
146 | },
147 | "@schematics/angular:directive": {
148 | "prefix": "app"
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Application } from 'spectron';
2 | import { resolve } from 'path';
3 |
4 | const getElectronPath = () => {
5 | let electronPath = resolve(__dirname, '../../node_modules/.bin/electron');
6 | if (process.platform === 'win32') {
7 | electronPath += '.cmd';
8 | }
9 | return electronPath;
10 | }
11 |
12 | const startApplication = () => {
13 | return new Application({
14 | path: getElectronPath(),
15 | args: [resolve(__dirname, '../../dist')],
16 | env: { SPECTRON: true }
17 | }).start();
18 | }
19 |
20 | describe('bterm', () => {
21 | let app: Application;
22 |
23 | beforeAll(() => startApplication().then(startedApp => app = startedApp));
24 |
25 | afterAll(() => {
26 | if (app && app.isRunning()) {
27 | return app.stop();
28 | }
29 | })
30 |
31 | it('should show an initial window', () => {
32 | return app.client.waitUntilWindowLoaded()
33 | .then(() => app.client.getWindowCount())
34 | .then(count => expect(count).toEqual(1));
35 | });
36 |
37 | it('should be visible', () => {
38 | return app.client.waitUntilWindowLoaded()
39 | .then(() => app.browserWindow.isVisible())
40 | .then(res => expect(res).toBeTruthy());
41 | });
42 |
43 | it('should not be minimized', () => {
44 | return app.client.waitUntilWindowLoaded()
45 | .then(() => app.browserWindow.isMinimized())
46 | .then(res => expect(res).toBeFalsy());
47 | });
48 |
49 | it('should check if globalShortcuts are registered', () => {
50 | return app.client.waitUntilWindowLoaded()
51 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+Shift+O'))
52 | .then((result) => expect(result).toBeFalsy()) // First is not existing that shpuld be false
53 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+Shift+Left'))
54 | .then((result) => expect(result).toBeTruthy())
55 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+Shift+Right'))
56 | .then((result) => expect(result).toBeTruthy())
57 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+T'))
58 | .then((result) => expect(result).toBeTruthy())
59 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+W'))
60 | .then((result) => expect(result).toBeTruthy())
61 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+K'))
62 | .then((result) => expect(result).toBeTruthy())
63 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+1'))
64 | .then((result) => expect(result).toBeTruthy())
65 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+2'))
66 | .then((result) => expect(result).toBeTruthy())
67 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+3'))
68 | .then((result) => expect(result).toBeTruthy())
69 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+4'))
70 | .then((result) => expect(result).toBeTruthy())
71 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+5'))
72 | .then((result) => expect(result).toBeTruthy())
73 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+6'))
74 | .then((result) => expect(result).toBeTruthy())
75 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+7'))
76 | .then((result) => expect(result).toBeTruthy())
77 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+8'))
78 | .then((result) => expect(result).toBeTruthy())
79 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+9'))
80 | .then((result) => expect(result).toBeTruthy())
81 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+0'))
82 | .then((result) => expect(result).toBeTruthy())
83 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+N'))
84 | .then((result) => expect(result).toBeTruthy())
85 | .then(() => app.electron.remote.globalShortcut.isRegistered('CommandOrControl+Shift+I'))
86 | .then((result) => expect(result).toBeTruthy())
87 | });
88 |
89 | it('should have a width', () => {
90 | return app.client.waitUntilWindowLoaded()
91 | .then(() => app.browserWindow.getBounds())
92 | .then(result => {
93 | expect(result.width).toBeDefined();
94 | expect(result.width).toBeGreaterThan(0);
95 | expect(result.height).toBeDefined();
96 | expect(result.height).toBeGreaterThan(0);
97 | });
98 | });
99 |
100 | it('should have the app title', () => {
101 | return app.client.waitUntilWindowLoaded()
102 | .then(() => app.browserWindow.getTitle())
103 | .then(title => expect(title).toEqual('bterm'));
104 | });
105 |
106 | });
107 |
--------------------------------------------------------------------------------
/src/app/components/terminal/terminal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy, ElementRef, Renderer2 } from '@angular/core';
2 | import { TerminalService } from '../../providers/terminal.service';
3 | import { ipcRenderer, remote, clipboard } from 'electron';
4 | import { Observable, Subscription, fromEvent, timer } from 'rxjs';
5 | import { debounce } from 'rxjs/operators';
6 |
7 | @Component({
8 | selector: 'app-terminal',
9 | templateUrl: './terminal.component.html',
10 | styleUrls: ['./terminal.component.sass']
11 | })
12 | export class TerminalComponent implements OnInit, OnDestroy {
13 | el: HTMLMainElement;
14 | subs: Subscription[] = [];
15 |
16 | constructor(
17 | public elementRef: ElementRef,
18 | public terminalService: TerminalService,
19 | public renderer: Renderer2
20 | ) { }
21 |
22 | ngOnInit() {
23 | this.el = this.elementRef.nativeElement.querySelector('.terminal');
24 | this.initIpcListeners();
25 | this.initTerminalEvents();
26 | this.terminalService.create(this.el);
27 | }
28 |
29 | ngOnDestroy() {
30 | this.subs.forEach(sub => sub.unsubscribe());
31 | }
32 |
33 | initIpcListeners(): void {
34 | this.subs.push(
35 | fromEvent(ipcRenderer, 'move')
36 | .pipe(debounce(() => timer(100)))
37 | .subscribe(event => this.terminalService.focusCurrentTab())
38 | );
39 |
40 | this.subs.push(
41 | fromEvent(ipcRenderer, 'newTab')
42 | .subscribe(() => this.terminalService.create(this.el))
43 | );
44 |
45 | this.subs.push(
46 | fromEvent(ipcRenderer, 'resize')
47 | .pipe(debounce(() => timer(100)))
48 | .subscribe(() => {
49 | this.terminalService.terminals.forEach(terminal => {
50 | (terminal.term).fit();
51 | terminal.term.focus();
52 | });
53 | })
54 | );
55 |
56 | this.subs.push(
57 | fromEvent(ipcRenderer, 'restore')
58 | .subscribe(() => this.terminalService.focusCurrentTab())
59 | );
60 |
61 | this.subs.push(
62 | fromEvent(ipcRenderer, 'enter-full-screen')
63 | .subscribe(() => this.terminalService.focusCurrentTab())
64 | );
65 |
66 | this.subs.push(
67 | fromEvent(ipcRenderer, 'leave-full-screen')
68 | .subscribe(() => this.terminalService.focusCurrentTab())
69 | );
70 |
71 | this.subs.push(
72 | fromEvent(ipcRenderer, 'tabLeft')
73 | .subscribe(() => this.previousTab())
74 | );
75 |
76 | this.subs.push(
77 | fromEvent(ipcRenderer, 'tabRight')
78 | .subscribe(() => this.nextTab())
79 | );
80 |
81 | this.subs.push(
82 | fromEvent(ipcRenderer, 'paste')
83 | .subscribe(() => this.paste())
84 | );
85 |
86 | this.subs.push(
87 | fromEvent(ipcRenderer, 'copy')
88 | .subscribe(() => this.copy())
89 | );
90 | }
91 |
92 | initTerminalEvents(): void {
93 | this.terminalService.events.subscribe(event => {
94 | const elements = this.el.querySelectorAll('.terminal-instance');
95 | if (event.type === 'focusTab') {
96 | this.setActiveTab(event.index);
97 | } else if (event.type === 'destroy') {
98 | const element = elements[event.index];
99 | this.renderer.removeChild(element.parentElement, element);
100 | this.terminalService.terminals = this.terminalService.terminals.filter(t => t.el !== element);
101 | const index = this.el.querySelectorAll('.terminal-instance').length - 1;
102 | if (index > -1) {
103 | this.setActiveTab(index);
104 | this.terminalService.focusTab(index);
105 | } else {
106 | ipcRenderer.send('close', remote.getCurrentWindow().id);
107 | }
108 | }
109 | });
110 | }
111 |
112 | setActiveTab(i: number): void {
113 | const elements = this.el.querySelectorAll('.terminal-instance');
114 | [].forEach.call(elements, el => this.renderer.setStyle(el, 'z-index', 0));
115 | this.renderer.setStyle(elements[i], 'z-index', 10);
116 | (this.terminalService.terminals[i].term).focus();
117 | (this.terminalService.terminals[i].term).fit();
118 | }
119 |
120 | previousTab(): void {
121 | if (this.terminalService.terminals.length === 1) {
122 | return;
123 | }
124 |
125 | const currentIndex = this.terminalService.currentIndex;
126 | let index = null;
127 | if (currentIndex - 1 < 0) {
128 | index = this.terminalService.terminals.length - 1;
129 | } else {
130 | index = currentIndex - 1;
131 | }
132 |
133 | this.terminalService.currentIndex = index;
134 | this.setActiveTab(index);
135 | this.terminalService.focusTab(index);
136 | }
137 |
138 | nextTab(): void {
139 | if (this.terminalService.terminals.length === 1) {
140 | return;
141 | }
142 |
143 | const currentIndex = this.terminalService.currentIndex;
144 | let index = null;
145 | if (currentIndex + 1 > this.terminalService.terminals.length - 1) {
146 | index = 0;
147 | } else {
148 | index = currentIndex + 1;
149 | }
150 |
151 | this.terminalService.currentIndex = index;
152 | this.setActiveTab(index);
153 | this.terminalService.focusTab(index);
154 | }
155 |
156 | paste(): void {
157 | this.terminalService.terminals[this.terminalService.currentIndex].ptyProcess.write.next(clipboard.readText());
158 | }
159 |
160 | copy(): void {
161 | const term = this.terminalService.terminals[this.terminalService.currentIndex].term;
162 | if (term.hasSelection()) {
163 | const selection = term.getSelection();
164 | clipboard.writeText(selection);
165 | term.clearSelection();
166 | }
167 | }
168 |
169 | }
170 |
--------------------------------------------------------------------------------
/src/app/providers/terminal.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, EventEmitter } from '@angular/core';
2 | import { WindowService } from './window.service';
3 | import { Observable, Subject, Subscription, fromEvent } from 'rxjs';
4 | import { share, filter, map } from 'rxjs/operators';
5 | import * as os from 'os';
6 | import { Terminal } from 'xterm';
7 | import * as fit from 'xterm/lib/addons/fit/fit';
8 | import { execSync } from 'child_process';
9 | import { which } from 'shelljs';
10 | import { StringDecoder } from 'string_decoder';
11 | const spawn = window.require('node-pty').spawn;
12 |
13 | export interface PtyProcessType {
14 | shell: { shell: string, args: string[] };
15 | process: any;
16 | onData: Observable;
17 | write: Subject;
18 | writeSub: Subscription;
19 | }
20 |
21 | export interface TerminalType {
22 | el: HTMLElement;
23 | ptyProcess: PtyProcess;
24 | term: Terminal;
25 | title: string;
26 | subscriptions: Subscription[];
27 | }
28 |
29 | class PtyProcess implements PtyProcessType {
30 | shell: { shell: string, args: string[] };
31 | process: any;
32 | onData: Observable;
33 | onError: Observable;
34 | onExit: Observable;
35 | write: Subject;
36 | writeSub: Subscription;
37 |
38 | constructor() {
39 | this.shell = this.getDefaultShell();
40 | this.process = spawn(this.shell.shell, this.shell.args, {
41 | cols: 80,
42 | rows: 30,
43 | cwd: os.homedir()
44 | });
45 |
46 | const decoder = new StringDecoder('utf8');
47 | this.onData = fromEvent(this.process, 'data').pipe(map((x: Buffer) => decoder.write(x)), share());
48 | this.onError = fromEvent(this.process, 'error').pipe(map(x => x.toString()), share());
49 | this.onExit = fromEvent(this.process, 'exit').pipe(share());
50 | this.write = new Subject();
51 | this.writeSub = this.write.pipe(map(input => this.process.write(input))).subscribe();
52 | }
53 |
54 | getDefaultShell(): { shell: string, args: string[] } {
55 | let shell = null;
56 |
57 | const exec = execSync('echo $SHELL', { encoding: 'utf8' }).toString();
58 | if (exec && exec.includes('bin')) {
59 | shell = exec.trim();
60 | } else {
61 | const platform = os.platform();
62 | if (platform === 'darwin') {
63 | shell = process.env.SHELL || '/bin/bash';
64 | } else if (platform === 'win32') {
65 | const bashPath: any = which('bash');
66 | if (bashPath.code === 0 && bashPath.stdout) {
67 | shell = bashPath.stdout;
68 | } else {
69 | shell = process.env.SHELL || process.env.COMSPEC || 'cmd.exe';
70 | }
71 | } else {
72 | shell = process.env.SHELL || '/bin/sh';
73 | }
74 | }
75 |
76 | const args = process.env.SHELL_EXECUTE_FLAGS || '--login';
77 | return { shell: shell, args: args.split(' ').filter(Boolean) };
78 | }
79 | }
80 |
81 | @Injectable()
82 | export class TerminalService {
83 | terminals: TerminalType[];
84 | currentIndex: number;
85 | events: EventEmitter<{ type: string, index: number }>;
86 |
87 | constructor(public windowService: WindowService) {
88 | this.terminals = [];
89 | Terminal.applyAddon(fit);
90 | this.events = new EventEmitter<{ type: string, index: number }>();
91 | }
92 |
93 | create(el: HTMLMainElement): void {
94 | const doc: HTMLDocument = document;
95 | const element = doc.createElement('div');
96 | element.classList.add('terminal-instance');
97 | el.appendChild(element);
98 |
99 | const terminal: TerminalType = {
100 | el: element,
101 | ptyProcess: new PtyProcess(),
102 | term: new Terminal(),
103 | title: 'Shell',
104 | subscriptions: []
105 | };
106 |
107 | this.terminals.push(terminal);
108 | this.currentIndex = this.terminals.length - 1;
109 |
110 | terminal.term.open(element);
111 | this.events.next({ type: 'create', index: null });
112 | this.focusCurrentTab();
113 |
114 | terminal.subscriptions.push(terminal.ptyProcess.onData.subscribe(data => {
115 | terminal.term.write(data);
116 | (terminal.term).fit();
117 | }));
118 | terminal.subscriptions.push(terminal.ptyProcess.onError.subscribe(data => {
119 | this.destroy();
120 | }));
121 | terminal.subscriptions.push(terminal.ptyProcess.onExit.subscribe((exitCode) => {
122 | this.destroy();
123 | }));
124 | terminal.subscriptions.push(
125 | fromEvent(terminal.term, 'title')
126 | .subscribe((title: string) => {
127 | terminal.title = title;
128 | })
129 | );
130 | terminal.subscriptions.push(
131 | fromEvent(terminal.term, 'data').subscribe((key: string) => {
132 | terminal.ptyProcess.write.next(key);
133 | (terminal.term).fit();
134 | })
135 | );
136 | terminal.subscriptions.push(
137 | fromEvent(terminal.term, 'resize').subscribe((sizeData: any) => {
138 | terminal.ptyProcess.process.resize(sizeData.cols, sizeData.rows);
139 | })
140 | );
141 | terminal.subscriptions.push(
142 | this.windowService.size.subscribe(size => {
143 | terminal.ptyProcess.process.resize(terminal.term.cols, terminal.term.rows);
144 | (terminal.term).fit();
145 | })
146 | );
147 | }
148 |
149 | focusTab(i: number): void {
150 | const terminal = this.terminals[i];
151 | this.currentIndex = i;
152 | this.events.emit({ type: 'focusTab', index: i });
153 | terminal.term.focus();
154 | (terminal.term).fit();
155 | terminal.term.scrollToBottom();
156 | }
157 |
158 | focusCurrentTab(): void {
159 | const terminal = this.terminals[this.currentIndex];
160 | this.events.emit({ type: 'focusTab', index: this.currentIndex });
161 | terminal.term.focus();
162 | (terminal.term).fit();
163 | terminal.term.scrollToBottom();
164 | }
165 |
166 | destroy(i?: number): void {
167 | const index = typeof i === 'undefined' ? this.currentIndex : i;
168 | const terminal = this.terminals[index];
169 | terminal.subscriptions.forEach(sub => sub.unsubscribe());
170 | terminal.ptyProcess.process.kill();
171 | this.events.emit({ type: 'destroy', index: index });
172 | }
173 |
174 | destroyAll(): void {
175 | this.terminals.forEach((term, index) => {
176 | const terminal = this.terminals[index];
177 | terminal.subscriptions.forEach(sub => sub.unsubscribe());
178 | terminal.ptyProcess.process.kill();
179 | this.events.emit({ type: 'destroy', index: index });
180 | });
181 | }
182 | }
183 |
--------------------------------------------------------------------------------