├── .prettierignore ├── .dtpl ├── templates │ ├── module │ │ └── index.ts.dtpl │ └── react │ │ ├── component-module │ │ └── index.ts.dtpl │ │ └── component.tsx.dtpl ├── tslint.json ├── tsconfig.json └── dtpl.ts ├── extensions └── vscode │ ├── CHANGELOG.md │ ├── .vscodeignore │ ├── README.md │ └── package.json ├── src ├── client │ ├── components │ │ ├── menu │ │ │ ├── index.ts │ │ │ ├── @group │ │ │ │ ├── index.ts │ │ │ │ ├── @list │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── @list-item-buttons │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-item-close-button.tsx │ │ │ │ │ │ ├── list-item-start-button.tsx │ │ │ │ │ │ ├── list-item-stop-button.tsx │ │ │ │ │ │ ├── list-item-restart-button.tsx │ │ │ │ │ │ └── @list-item-button.tsx │ │ │ │ │ ├── list.tsx │ │ │ │ │ └── @list-item.tsx │ │ │ │ ├── @group-nav │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── group-nav.tsx │ │ │ │ │ └── @group-nav-link.tsx │ │ │ │ └── group.tsx │ │ │ ├── @logo.tsx │ │ │ ├── @pin-button.tsx │ │ │ └── menu.tsx │ │ ├── window-manager │ │ │ ├── index.ts │ │ │ ├── @window │ │ │ │ ├── index.ts │ │ │ │ ├── @window-tools │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── window-remove-button.tsx │ │ │ │ │ ├── window-stop-button.tsx │ │ │ │ │ ├── window-start-button.tsx │ │ │ │ │ ├── window-restart-window.tsx │ │ │ │ │ └── window-status-dot.tsx │ │ │ │ ├── @block.tsx │ │ │ │ └── window.tsx │ │ │ ├── @zero-state.tsx │ │ │ ├── @tool-bar-button.tsx │ │ │ ├── manager.tsx │ │ │ └── @manager-style.tsx │ │ ├── disconnected │ │ │ ├── index.ts │ │ │ └── disconnected-view.tsx │ │ └── app.tsx │ ├── types │ │ ├── images.d.ts │ │ └── third-parties │ │ │ ├── react-scroll-horizontal.d.ts │ │ │ └── rc-dropdown.d.ts │ ├── services.ts │ ├── tslint.json │ ├── tsconfig.json │ ├── utils │ │ ├── storage.ts │ │ ├── dom.ts │ │ ├── lang.ts │ │ └── output.ts │ ├── index.html │ ├── main.tsx │ ├── services │ │ ├── socket-io-service.ts │ │ └── task-service.ts │ ├── global.css │ ├── theme.ts │ └── assets │ │ ├── logo.svg │ │ └── zero-state.svg └── cli │ ├── types │ ├── lang.d.ts │ ├── strip-color.d.ts │ ├── open.d.ts │ ├── shell-escape.d.ts │ ├── npm-which.d.ts │ └── ansi-to-html.d.ts │ ├── tslint.json │ ├── problem-matchers │ ├── index.ts │ └── @typescript.ts │ ├── tsconfig.json │ ├── main.ts │ ├── commands │ └── default.ts │ └── core │ ├── config.ts │ ├── task.ts │ ├── problem-matcher.ts │ └── server.ts ├── prettier.config.js ├── .gitignore ├── .vscode └── settings.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.dtpl/templates/module/index.ts.dtpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log -------------------------------------------------------------------------------- /.dtpl/templates/react/component-module/index.ts.dtpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './menu'; 2 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/index.ts: -------------------------------------------------------------------------------- 1 | export * from './group'; 2 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list'; 2 | -------------------------------------------------------------------------------- /src/client/components/window-manager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manager'; 2 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@magicspace/configs/prettier'); 2 | -------------------------------------------------------------------------------- /src/cli/types/lang.d.ts: -------------------------------------------------------------------------------- 1 | interface Dictionary { 2 | [key: string]: T; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/index.ts: -------------------------------------------------------------------------------- 1 | export * from './window'; 2 | -------------------------------------------------------------------------------- /src/cli/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@magicspace/configs/tslint-prettier" 3 | } 4 | -------------------------------------------------------------------------------- /src/client/components/disconnected/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disconnected-view'; 2 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@group-nav/index.ts: -------------------------------------------------------------------------------- 1 | export * from './group-nav'; 2 | -------------------------------------------------------------------------------- /src/client/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | 3 | declare module '*.png'; 4 | 5 | declare module '*.jpg'; 6 | -------------------------------------------------------------------------------- /extensions/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore -------------------------------------------------------------------------------- /src/cli/types/strip-color.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'strip-color' { 2 | function stripColor(text:string): string; 3 | export = stripColor; 4 | } 5 | -------------------------------------------------------------------------------- /.dtpl/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@magicspace/configs/tslint-prettier", 4 | "@magicspace/configs/tslint-override-dev" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/cli/types/open.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'open' { 2 | function open(target: string, app?: string): void; 3 | namespace open { 4 | 5 | } 6 | export = open; 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/types/shell-escape.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'shell-escape' { 2 | function escape(args: string[]): string; 3 | namespace escape { 4 | 5 | } 6 | export = escape; 7 | } 8 | -------------------------------------------------------------------------------- /.dtpl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../node_modules/@magicspace/configs/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "files": ["dtpl.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/types/npm-which.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'npm-which' { 2 | const builder: ( 3 | dir: string, 4 | ) => { 5 | sync(name: string): string; 6 | }; 7 | 8 | export = builder; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list-item-start-button'; 2 | export * from './list-item-stop-button'; 3 | export * from './list-item-close-button'; 4 | export * from './list-item-restart-button'; 5 | -------------------------------------------------------------------------------- /src/client/services.ts: -------------------------------------------------------------------------------- 1 | import {SocketIOService} from './services/socket-io-service'; 2 | import {TaskService} from './services/task-service'; 3 | 4 | export const socketIOService = new SocketIOService(); 5 | 6 | export const taskService = new TaskService(socketIOService); 7 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './window-remove-button'; 2 | export * from './window-status-dot'; 3 | export * from './window-restart-window'; 4 | export * from './window-start-button'; 5 | export * from './window-stop-button'; 6 | -------------------------------------------------------------------------------- /src/cli/problem-matchers/index.ts: -------------------------------------------------------------------------------- 1 | import {ProblemMatcherConfig} from '../core/config'; 2 | 3 | import {tscWatch, tslint} from './@typescript'; 4 | 5 | export const builtInProblemMatcherDict: Dictionary = { 6 | '$typescript:tsc-watch': tscWatch, 7 | '$typescript:tslint': tslint, 8 | }; 9 | -------------------------------------------------------------------------------- /src/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../node_modules/@magicspace/configs/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | 8 | "rootDir": ".", 9 | "outDir": "../../bld/cli" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Installable packages 2 | 3 | node_modules/ 4 | 5 | # Configuration 6 | 7 | /.vscode/launch.json 8 | /.idea/ 9 | .config/ 10 | /nodemon.json 11 | /.biu.json 12 | 13 | # Building artifacts 14 | 15 | .cache/ 16 | bld/ 17 | *.tgz 18 | 19 | # Debugging outputs 20 | 21 | *.log 22 | 23 | # Testing files 24 | 25 | /test.* 26 | -------------------------------------------------------------------------------- /src/client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@magicspace/configs/tslint-prettier", 3 | "rules": { 4 | "no-implicit-dependencies": [ 5 | true, 6 | ["socket.io-client", "theme", "assets", "services", "utils"] 7 | ], 8 | "explicit-return-type": [true, {"complexTypeFixer": true}], 9 | "no-null-keyword": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../node_modules/@magicspace/configs/tsconfig-browser.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "jsx": "react", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "skipLibCheck": true, 9 | 10 | "baseUrl": ".", 11 | "rootDir": ".", 12 | "outDir": "../../bld/client" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "files.eol": "\n", 6 | "files.insertFinalNewline": true, 7 | "files.trimTrailingWhitespace": true, 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "tslint.autoFixOnSave": true, 10 | "tslint.configFile": "node_modules/@magicspace/configs/tslint-vscode.js" 11 | } 12 | -------------------------------------------------------------------------------- /extensions/vscode/README.md: -------------------------------------------------------------------------------- 1 | # Problem Matchers for Biu! 2 | 3 | ## Usage 4 | 5 | ```json 6 | { 7 | "version": "2.0.0", 8 | "tasks": [ 9 | { 10 | "label": "biu", 11 | "type": "shell", 12 | "command": "yarn", 13 | "args": ["biu"], 14 | "isBackground": true, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | "problemMatcher": "$biu-typescript" 20 | } 21 | ] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /src/client/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export function getStorageObject(key: string): T | undefined { 2 | let json = localStorage.getItem(key); 3 | 4 | if (json) { 5 | let object = JSON.parse(json); 6 | 7 | if (object) { 8 | return object as T; 9 | } 10 | } 11 | 12 | return undefined; 13 | } 14 | 15 | export function setStorageObject(key: string, object: T): void { 16 | let json = JSON.stringify(object); 17 | localStorage.setItem(key, json); 18 | } 19 | -------------------------------------------------------------------------------- /src/client/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export const fadeInUpAnimation = { 2 | from: { 3 | transform: 'translateY(+50px)', 4 | opacity: '0', 5 | }, 6 | to: { 7 | transform: 'none', 8 | opacity: '1', 9 | }, 10 | }; 11 | 12 | export const fadeInAnimation = { 13 | from: { 14 | opacity: '0', 15 | }, 16 | to: { 17 | opacity: '1', 18 | }, 19 | }; 20 | 21 | export const fadeOutAnimation = { 22 | from: { 23 | opacity: '1', 24 | }, 25 | to: { 26 | opacity: '0', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/cli/types/ansi-to-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ansi-to-html' { 2 | interface AnsiConverterOptions { 3 | fg?: string; 4 | bg?: string; 5 | newline?: boolean; 6 | escapeXML?: boolean; 7 | stream?: boolean; 8 | colors?: {[key: string]: string} | string[]; 9 | } 10 | 11 | class AnsiConverter { 12 | constructor(options?: AnsiConverterOptions); 13 | toHtml(ansi: string): string; 14 | } 15 | 16 | namespace AnsiConverter { 17 | 18 | } 19 | 20 | export = AnsiConverter; 21 | } 22 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Biu! 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/client/main.tsx: -------------------------------------------------------------------------------- 1 | import {Provider} from 'mobx-react'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import {ThemeProvider} from 'styled-components'; 5 | 6 | import {App} from 'components/app'; 7 | 8 | import './global.css'; 9 | import * as services from './services'; 10 | import {theme} from './theme'; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') as HTMLElement, 19 | ); 20 | -------------------------------------------------------------------------------- /src/client/types/third-parties/react-scroll-horizontal.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-scroll-horizontal' { 2 | import {Component, CSSProperties} from 'react'; 3 | 4 | export interface ScrollHorizontalConfig { 5 | stiffness?: number; 6 | damping?: number; 7 | } 8 | 9 | export interface ScrollHorizontalProps { 10 | reverseScroll?: boolean; 11 | pageLock?: boolean; 12 | config?: ScrollHorizontalConfig; 13 | style?: CSSProperties; 14 | className?: string; 15 | } 16 | 17 | export default class ScrollHorizontal extends Component< 18 | ScrollHorizontalProps 19 | > {} 20 | } 21 | -------------------------------------------------------------------------------- /src/cli/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as Path from 'path'; 4 | import * as Util from 'util'; 5 | 6 | import 'source-map-support/register'; 7 | import 'villa/platform/node'; 8 | 9 | import {CLI, Shim} from 'clime'; 10 | 11 | process.on('uncaughtException', exitWithError); 12 | process.on('unhandledRejection', exitWithError); 13 | 14 | let cli = new CLI('biu', Path.join(__dirname, 'commands')); 15 | 16 | let shim = new Shim(cli); 17 | shim.execute(process.argv).catch(exitWithError); 18 | 19 | function exitWithError(error: any): void { 20 | process.stderr.write(`${Util.inspect(error)}\n`); 21 | process.exit(1); 22 | } 23 | -------------------------------------------------------------------------------- /.dtpl/templates/react/component.tsx.dtpl: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {styled} from 'theme'; 6 | 7 | const Wrapper = styled.div``; 8 | 9 | export interface ${ModuleName}Props { 10 | className?: string; 11 | } 12 | 13 | @observer 14 | export class ${ModuleName} extends Component<${ModuleName}Props> { 15 | render(): ReactNode { 16 | let {className} = this.props; 17 | 18 | return ( 19 | 20 | ${htmlClassName} 21 | 22 | ); 23 | } 24 | 25 | static Wrapper = Wrapper; 26 | } 27 | -------------------------------------------------------------------------------- /src/client/utils/lang.ts: -------------------------------------------------------------------------------- 1 | import {ReactNode, ReactNodeArray} from 'react'; 2 | 3 | export type ObjectMapCallback = ( 4 | value: T[K], 5 | key: K, 6 | object: T, 7 | ) => ReactNode; 8 | 9 | export function mapObject( 10 | object: T, 11 | callback: ObjectMapCallback, 12 | ): ReactNodeArray { 13 | let keys = Object.keys(object); 14 | 15 | let nodes: ReactNodeArray = []; 16 | 17 | for (let key of keys) { 18 | if (callback && key in object) { 19 | let node = callback((object as any)[key], key as keyof object, object); 20 | 21 | nodes.push(node); 22 | } 23 | } 24 | 25 | return nodes; 26 | } 27 | 28 | export function deepCopy(object: T): T { 29 | return JSON.parse(JSON.stringify(object)); 30 | } 31 | -------------------------------------------------------------------------------- /src/client/components/menu/@logo.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import LogoImgSrc from 'assets/logo.svg'; 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div` 9 | width: 78px; 10 | height: 36px; 11 | `; 12 | 13 | const LogoImg = styled.img` 14 | width: 100%; 15 | height: 100%; 16 | `; 17 | 18 | export interface LogoProps { 19 | className?: string; 20 | } 21 | 22 | @observer 23 | export class Logo extends Component { 24 | render(): ReactNode { 25 | let {className} = this.props; 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | static Wrapper = Wrapper; 35 | } 36 | -------------------------------------------------------------------------------- /src/client/types/third-parties/rc-dropdown.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rc-dropdown' { 2 | import {Component, CSSProperties} from 'react'; 3 | 4 | type Func = (...args: any[]) => any; 5 | 6 | export interface DropdownProps { 7 | minOverlayWidthMatchTrigger?: boolean; 8 | onVisibleChange?: Func; 9 | onOverlayClick?: Func; 10 | prefixCls?: string; 11 | transitionName?: string; 12 | overlayClassName?: string; 13 | animation?: any; 14 | align?: object; 15 | overlayStyle?: CSSProperties; 16 | placement?: string; 17 | overlay?: React.ReactNode; 18 | trigger?: any[]; 19 | alignPoint?: boolean; 20 | showAction?: any[]; 21 | hideAction?: any[]; 22 | getPopupContainer?: Func; 23 | visible?: boolean; 24 | defaultVisible?: boolean; 25 | } 26 | 27 | export default class Dropdown extends Component {} 28 | } 29 | -------------------------------------------------------------------------------- /.dtpl/dtpl.ts: -------------------------------------------------------------------------------- 1 | import {IDtplConfig, IUserTemplate, Source} from 'dot-template-types'; 2 | 3 | export default function(source: Source): IDtplConfig { 4 | const basename = source.filePath.match(/([^\\/]+?)(?:\.\w+){0,2}$/)![1]; 5 | const url = basename.replace(/^@/, '').replace(/\./, '-'); 6 | 7 | const localData = { 8 | htmlClassName: basename.replace(/^@/, ''), 9 | defaultUrl: url, 10 | }; 11 | 12 | const templates: IUserTemplate[] = [ 13 | { 14 | name: 'templates/module', 15 | matches: '*/src/**', 16 | }, 17 | { 18 | name: 'templates/react/component.tsx.dtpl', 19 | matches: 'src/client/components/**/*.tsx', 20 | }, 21 | { 22 | name: 'templates/react/component-module', 23 | matches: 'src/client/components/**', 24 | }, 25 | ].map(template => { 26 | return {localData, ...template}; 27 | }); 28 | 29 | return {templates}; 30 | } 31 | -------------------------------------------------------------------------------- /src/client/services/socket-io-service.ts: -------------------------------------------------------------------------------- 1 | import Client from 'socket.io-client'; 2 | 3 | export type EventListener = (...args: any[]) => void; 4 | 5 | export interface EventListenerId { 6 | event: string; 7 | listener: EventListener; 8 | } 9 | 10 | export class SocketIOService { 11 | private client = Client(); 12 | 13 | constructor() { 14 | this.client.connect(); 15 | } 16 | 17 | on(event: string, listener: EventListener): EventListenerId { 18 | this.client.on(event, listener); 19 | 20 | return {event, listener}; 21 | } 22 | 23 | once(event: string, listener: EventListener): EventListenerId { 24 | this.client.once(event, listener); 25 | 26 | return {event, listener}; 27 | } 28 | 29 | emit(event: string, ...args: any[]): void { 30 | this.client.emit(event, ...args); 31 | } 32 | 33 | removeListener(listenerId: EventListenerId): void { 34 | let {event, listener} = listenerId; 35 | 36 | this.client.removeListener(event, listener); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/list-item-close-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {styled} from 'theme'; 6 | 7 | import {ListItemButton} from './@list-item-button'; 8 | 9 | const Wrapper = styled.div` 10 | margin-left: 5px; 11 | display: inline; 12 | `; 13 | 14 | export interface ListItemCloseButtonProps { 15 | className?: string; 16 | onClick?(): void; 17 | } 18 | 19 | @observer 20 | export class ListItemCloseButton extends Component { 21 | render(): ReactNode { 22 | let {className, onClick} = this.props; 23 | 24 | return ( 25 | 26 | 32 | 33 | ); 34 | } 35 | 36 | static Wrapper = Wrapper; 37 | } 38 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/list-item-start-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {styled} from 'theme'; 6 | 7 | import {ListItemButton} from './@list-item-button'; 8 | 9 | const Wrapper = styled.div` 10 | margin-left: 5px; 11 | display: inline; 12 | `; 13 | 14 | export interface ListItemStartButtonProps { 15 | className?: string; 16 | onClick?(): void; 17 | } 18 | 19 | @observer 20 | export class ListItemStartButton extends Component { 21 | render(): ReactNode { 22 | let {className, onClick} = this.props; 23 | 24 | return ( 25 | 26 | 32 | 33 | ); 34 | } 35 | 36 | static Wrapper = Wrapper; 37 | } 38 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/list-item-stop-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {styled} from 'theme'; 6 | 7 | import {ListItemButton} from './@list-item-button'; 8 | 9 | const Wrapper = styled.div` 10 | margin-left: 5px; 11 | display: inline; 12 | `; 13 | 14 | export interface ListItemStopButtonProps { 15 | className?: string; 16 | onClick?(): void; 17 | } 18 | 19 | @observer 20 | export class ListItemStopButton extends Component { 21 | render(): ReactNode { 22 | let {className, onClick} = this.props; 23 | 24 | return ( 25 | 26 | 32 | 33 | ); 34 | } 35 | 36 | static Wrapper = Wrapper; 37 | } 38 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/list-item-restart-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {styled} from 'theme'; 6 | 7 | import {ListItemButton} from './@list-item-button'; 8 | 9 | const Wrapper = styled.div` 10 | margin-left: 5px; 11 | display: inline; 12 | `; 13 | 14 | export interface ListItemRestartButtonProps { 15 | className?: string; 16 | onClick?(): void; 17 | } 18 | 19 | @observer 20 | export class ListItemRestartButton extends Component< 21 | ListItemRestartButtonProps 22 | > { 23 | render(): ReactNode { 24 | let {className, onClick} = this.props; 25 | 26 | return ( 27 | 28 | 34 | 35 | ); 36 | } 37 | 38 | static Wrapper = Wrapper; 39 | } 40 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/window-remove-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {createDefaultToolbarButton} from 'react-mosaic-component'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div``; 9 | 10 | export interface WindowRemoveButtonProps { 11 | className?: string; 12 | onClick?(): void; 13 | } 14 | 15 | @observer 16 | export class WindowRemoveButton extends Component { 17 | render(): ReactNode { 18 | let {className} = this.props; 19 | 20 | return ( 21 | 22 | {createDefaultToolbarButton( 23 | 'Close Window', 24 | 'pt-icon-cross', 25 | this.remove, 26 | )} 27 | 28 | ); 29 | } 30 | 31 | remove = (): void => { 32 | let {onClick} = this.props; 33 | 34 | if (onClick) { 35 | onClick(); 36 | } 37 | }; 38 | 39 | static Wrapper = Wrapper; 40 | } 41 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/window-stop-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {Icon} from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | color: ${props => props.theme.bar.gray}; 10 | transition: all 0.3s; 11 | font-size: 10px; 12 | padding: 2px; 13 | margin-top: 9px; 14 | margin-left: 3px; 15 | 16 | &:hover { 17 | color: ${props => props.theme.dangerAccent()}; 18 | transition: all 0.3s; 19 | } 20 | `; 21 | 22 | export interface WindowStopButtonProps { 23 | className?: string; 24 | onClick?(): void; 25 | } 26 | 27 | @observer 28 | export class WindowStopButton extends Component { 29 | render(): ReactNode { 30 | let {className, onClick} = this.props; 31 | 32 | return ( 33 | 38 | 39 | 40 | ); 41 | } 42 | 43 | static Wrapper = Wrapper; 44 | } 45 | -------------------------------------------------------------------------------- /src/cli/problem-matchers/@typescript.ts: -------------------------------------------------------------------------------- 1 | import {ProblemMatcherConfig} from '../core/config'; 2 | 3 | export const tscWatch: ProblemMatcherConfig = { 4 | owner: 'typescript', 5 | pattern: { 6 | regexp: 7 | '^([^\\s].*)[\\(:](\\d+[,:]\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$', 8 | file: 1, 9 | location: 2, 10 | severity: 3, 11 | code: 4, 12 | message: 5, 13 | }, 14 | background: { 15 | activeOnStart: true, 16 | beginsPattern: 17 | '^\\s*(?:message TS6032:|\\[?\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM| a\\.m\\.| p\\.m\\.)?(?:\\]| -)) File change detected\\. Starting incremental compilation\\.\\.\\.', 18 | endsPattern: 19 | '^\\s*(?:message TS6042:|\\[?\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM| a\\.m\\.| p\\.m\\.)?(?:\\]| -)) (?:Compilation complete\\.|Found \\d+ errors?\\.) Watching for file changes\\.', 20 | }, 21 | }; 22 | 23 | export const tslint: ProblemMatcherConfig = { 24 | owner: 'typescript', 25 | pattern: { 26 | regexp: 27 | '^(WARNING|ERROR):(?:\\s+\\(\\S*\\))?\\s+(\\S.*)\\[(\\d+), (\\d+)\\]:\\s+(.*)$', 28 | severity: 1, 29 | file: 2, 30 | location: '$3,$4', 31 | message: 5, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/window-start-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {Icon} from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | color: ${props => props.theme.bar.gray}; 10 | transition: all 0.3s; 11 | cursor: pointer; 12 | font-size: 10px; 13 | padding: 2px; 14 | margin-top: 9px; 15 | margin-left: 3px; 16 | 17 | &:hover { 18 | color: ${props => props.theme.safeAccent()}; 19 | transition: all 0.3s; 20 | } 21 | `; 22 | 23 | export interface WindowStartButtonProps { 24 | className?: string; 25 | onClick?(): void; 26 | } 27 | 28 | @observer 29 | export class WindowStartButton extends Component { 30 | render(): ReactNode { 31 | let {className, onClick} = this.props; 32 | 33 | return ( 34 | 39 | 40 | 41 | ); 42 | } 43 | 44 | static Wrapper = Wrapper; 45 | } 46 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/window-restart-window.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {Icon} from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | color: ${props => props.theme.bar.gray}; 10 | transition: all 0.3s; 11 | cursor: pointer; 12 | font-size: 10px; 13 | padding: 2px; 14 | margin-top: 9px; 15 | margin-left: 3px; 16 | 17 | &:hover { 18 | color: ${props => props.theme.alertAccent(0.6)}; 19 | transition: all 0.3s; 20 | } 21 | `; 22 | 23 | export interface WindowRestartButtonProps { 24 | className?: string; 25 | onClick?(): void; 26 | } 27 | 28 | @observer 29 | export class WindowRestartButton extends Component { 30 | render(): ReactNode { 31 | let {className, onClick} = this.props; 32 | 33 | return ( 34 | 39 | 40 | 41 | ); 42 | } 43 | 44 | static Wrapper = Wrapper; 45 | } 46 | -------------------------------------------------------------------------------- /src/client/components/app.tsx: -------------------------------------------------------------------------------- 1 | import {inject, observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {TaskService} from 'services/task-service'; 6 | import {styled} from 'theme'; 7 | 8 | import {DisconnectedView} from './disconnected'; 9 | import {Menu} from './menu'; 10 | import {Manager} from './window-manager'; 11 | 12 | const Wrapper = styled.div` 13 | display: flex; 14 | justify-content: space-between; 15 | width: 100%; 16 | height: 100vh; 17 | position: relative; 18 | 19 | ${Menu.Wrapper} { 20 | width: 320px; 21 | height: 100vh; 22 | } 23 | 24 | ${Manager.Wrapper} { 25 | flex: 1; 26 | } 27 | `; 28 | 29 | export interface AppProps { 30 | className?: string; 31 | } 32 | 33 | @observer 34 | export class App extends Component { 35 | @inject 36 | taskService!: TaskService; 37 | 38 | render(): ReactNode { 39 | let {className} = this.props; 40 | 41 | let connect = this.taskService.connected; 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/client/components/menu/@pin-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import Icon from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | font-size: 11px; 10 | color: ${props => props.theme.bar.gray}; 11 | opacity: 0.7; 12 | padding: 5px; 13 | 14 | &:hover { 15 | opacity: 1; 16 | } 17 | 18 | &.active { 19 | color: ${props => props.theme.alertAccent(0.3)}; 20 | } 21 | `; 22 | 23 | export interface PinButtonProps { 24 | className?: string; 25 | pinned?: boolean; 26 | hide?: boolean; 27 | onClick?(): void; 28 | } 29 | 30 | @observer 31 | export class PinButton extends Component { 32 | render(): ReactNode { 33 | let {className, onClick, pinned, hide} = this.props; 34 | 35 | return ( 36 | 44 | 48 | 49 | ); 50 | } 51 | 52 | static Wrapper = Wrapper; 53 | } 54 | -------------------------------------------------------------------------------- /extensions/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "biu-problem-matchers", 3 | "displayName": "Biu Problem Matchers", 4 | "description": "Problem Matchers for Biu!", 5 | "version": "0.1.0", 6 | "publisher": "mf", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/makeflow/biu.git" 10 | }, 11 | "engines": { 12 | "vscode": "^1.13.0" 13 | }, 14 | "categories": ["Other"], 15 | "contributes": { 16 | "problemMatchers": [ 17 | { 18 | "name": "biu-typescript", 19 | "owner": "typescript", 20 | "applyTo": "closedDocuments", 21 | "fileLocation": "absolute", 22 | "pattern": { 23 | "regexp": 24 | "^\\[biu-problem:typescript:([^;]*);([^;]*);([^;]*);([^;]*);(.*?)\\]?$", 25 | "severity": 1, 26 | "file": 2, 27 | "location": 3, 28 | "code": 4, 29 | "message": 5 30 | }, 31 | "background": { 32 | "activeOnStart": false, 33 | "beginsPattern": "^\\[biu-problems:typescript:begin\\]$", 34 | "endsPattern": "^\\[biu-problems:typescript:end\\]$" 35 | } 36 | } 37 | ] 38 | }, 39 | "scripts": { 40 | "vscode:prepublish": "", 41 | "compile": "", 42 | "postinstall": "node ./node_modules/vscode/bin/install", 43 | "test": "exit 0" 44 | }, 45 | "devDependencies": { 46 | "vscode": "^1.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/utils/output.ts: -------------------------------------------------------------------------------- 1 | export const MAX_LINE_LIMIT = 3000; 2 | 3 | export function cutOutOneOutputLine(output: string): string { 4 | let closedDivTagPos = output.indexOf(''); 5 | 6 | if (closedDivTagPos !== -1) { 7 | return output.slice(closedDivTagPos + 6); 8 | } else { 9 | let firstLineEndPos = output.indexOf('\n'); 10 | 11 | return output.slice(firstLineEndPos + 1); 12 | } 13 | } 14 | 15 | export function appendOutput( 16 | output: string | undefined, 17 | suffix: string, 18 | dataType?: string, 19 | ): string { 20 | if (dataType) { 21 | suffix = `
${suffix}
`; 22 | } 23 | 24 | if (output) { 25 | let lineCount = output.split('').length; 26 | 27 | if (lineCount > MAX_LINE_LIMIT) { 28 | output = cutOutOneOutputLine(output); 29 | } 30 | 31 | return output + suffix; 32 | } else { 33 | return suffix; 34 | } 35 | } 36 | 37 | export function outputWarning(text: string): string { 38 | return `${text}`; 39 | } 40 | 41 | export function outputError(text: string): string { 42 | return `${text}`; 43 | } 44 | 45 | export function outputSuccess(text: string): string { 46 | return `${text}`; 47 | } 48 | 49 | export function outputInfo(text: string): string { 50 | return `${text}`; 51 | } 52 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/list.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import FlipMove from 'react-flip-move'; 5 | 6 | import {Task} from 'services/task-service'; 7 | import {styled} from 'theme'; 8 | import {fadeInUpAnimation} from 'utils/dom'; 9 | 10 | import {ListItem} from './@list-item'; 11 | 12 | const Wrapper = styled.div` 13 | padding-right: 16px; 14 | padding-left: 10px; 15 | 16 | ${ListItem.Wrapper} { 17 | margin-bottom: 9px; 18 | } 19 | `; 20 | 21 | const ListItemWrapper = styled.div``; 22 | 23 | export interface ListProps { 24 | className?: string; 25 | group: string | undefined; 26 | tasks: Task[]; 27 | } 28 | 29 | @observer 30 | export class List extends Component { 31 | render(): ReactNode { 32 | let {className, tasks, group} = this.props; 33 | 34 | return ( 35 | 36 | 42 | {tasks.map(task => ( 43 | 44 | 45 | 46 | ))} 47 | 48 | 49 | ); 50 | } 51 | 52 | static Wrapper = Wrapper; 53 | } 54 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@zero-state.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import ZeroStateImage from 'assets/zero-state.svg'; 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div` 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | const ZeroStateBox = styled.div` 17 | padding-bottom: 30px; 18 | text-align: center; 19 | animation: fadeUpIn 0.5s; 20 | `; 21 | 22 | const ZeroStateImg = styled.img` 23 | width: 200px; 24 | margin-bottom: 40px; 25 | `; 26 | 27 | const ZeroStateTitle = styled.div` 28 | text-align: center; 29 | color: #414b55; 30 | margin-bottom: 10px; 31 | `; 32 | 33 | const ZeroStateSubtitle = styled.div` 34 | font-size: 13px; 35 | color: #96a1aa; 36 | `; 37 | 38 | export interface ZeroStateProps { 39 | className?: string; 40 | } 41 | 42 | @observer 43 | export class ZeroState extends Component { 44 | render(): ReactNode { 45 | let {className} = this.props; 46 | 47 | return ( 48 | 49 | 50 | 51 | No Tasks 52 | 53 | Click on items on the left to run a few tasks! 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | static Wrapper = Wrapper; 61 | } 62 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@window-tools/window-status-dot.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | 5 | import {TaskStatus} from 'services/task-service'; 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div` 9 | border-radius: 2.5px; 10 | width: 5px; 11 | height: 5px; 12 | display: block; 13 | transition: all 0.2s; 14 | 15 | &.running { 16 | background-color: ${props => props.theme.bar.green}; 17 | } 18 | 19 | &.waiting { 20 | background-color: ${props => props.theme.bar.yellow}; 21 | } 22 | 23 | &.stopped { 24 | background-color: ${props => props.theme.bar.gray}; 25 | } 26 | `; 27 | 28 | export interface WindowStatusDotProps { 29 | className?: string; 30 | status: TaskStatus; 31 | } 32 | 33 | @observer 34 | export class WindowStatusDot extends Component { 35 | render(): ReactNode { 36 | let {className, status} = this.props; 37 | 38 | let statusClassName = getStatusClassName(status); 39 | 40 | return ( 41 | 44 | ); 45 | } 46 | 47 | static Wrapper = Wrapper; 48 | } 49 | 50 | function getStatusClassName(status: TaskStatus): string | undefined { 51 | switch (status) { 52 | case TaskStatus.running: 53 | return 'running'; 54 | case TaskStatus.stopped: 55 | return 'stopped'; 56 | case TaskStatus.stopping: 57 | case TaskStatus.restarting: 58 | return 'waiting'; 59 | } 60 | 61 | return undefined; 62 | } 63 | -------------------------------------------------------------------------------- /src/client/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | } 5 | 6 | html { 7 | background-color: #eff3f5; 8 | font-family: Helvetica, 'Hiragino Sans GB', 'Microsoft Yahei UI', '微软雅黑', 9 | Arial, sans-serif; 10 | overflow: hidden; 11 | } 12 | 13 | a { 14 | transition: all 0.3s; 15 | cursor: pointer; 16 | user-select: none; 17 | } 18 | 19 | ::-webkit-scrollbar { 20 | width: 20px; 21 | height: 20px; 22 | background-color: transparent; 23 | border-radius: 10px; 24 | } 25 | ::-webkit-scrollbar:hover { 26 | } 27 | 28 | ::-webkit-scrollbar-track:hover { 29 | background: transparent; 30 | } 31 | 32 | ::-webkit-scrollbar-thumb { 33 | border-radius: 15px; 34 | background-clip: content-box; 35 | border: 7px solid transparent; 36 | background-color: #e2e2e2; 37 | background-clip: content-box; 38 | } 39 | ::-webkit-scrollbar-thumb:hover { 40 | background-color: #c2c2c2; 41 | } 42 | 43 | ::-webkit-scrollbar-thumb:vertical:active { 44 | background-color: #9da1a5; 45 | } 46 | 47 | @keyframes pulse { 48 | from { 49 | transform: scale3d(1, 1, 1); 50 | opacity: 1; 51 | } 52 | 53 | 35% { 54 | transform: scale3d(1.03, 1.03, 1.03); 55 | opacity: 0.95; 56 | } 57 | 58 | to { 59 | transform: scale3d(1, 1, 1); 60 | opacity: 1; 61 | } 62 | } 63 | 64 | @keyframes zoomIn { 65 | from { 66 | opacity: 0; 67 | transform: scale3d(0.5, 0.5, 0.5); 68 | } 69 | 70 | 50% { 71 | opacity: 1; 72 | } 73 | } 74 | 75 | @keyframes fadeIn { 76 | from { 77 | opacity: 0; 78 | } 79 | 80 | to { 81 | opacity: 1; 82 | } 83 | } 84 | 85 | @keyframes fadeUpIn { 86 | from { 87 | opacity: 0; 88 | transform: translateY(20px); 89 | } 90 | 91 | to { 92 | opacity: 1; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item-buttons/@list-item-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {Icon} from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | color: ${props => props.theme.border.light}; 10 | cursor: pointer; 11 | 12 | &.red { 13 | color: ${props => props.theme.dangerAccent()}; 14 | 15 | &:hover { 16 | color: ${props => props.theme.dangerAccent(-0.2)}; 17 | } 18 | } 19 | 20 | &.hover-red:hover { 21 | color: ${props => props.theme.dangerAccent()}; 22 | } 23 | 24 | &.hover-yellow:hover { 25 | color: ${props => props.theme.alertAccent(0.6)}; 26 | } 27 | 28 | &.hover-blue:hover { 29 | color: ${props => props.theme.accent()}; 30 | } 31 | 32 | &.hover-green:hover { 33 | color: ${props => props.theme.safeAccent()}; 34 | } 35 | `; 36 | 37 | export interface ListItemButtonProps { 38 | className?: string; 39 | icon: string; 40 | title?: string; 41 | onClick?(): void; 42 | } 43 | 44 | @observer 45 | export class ListItemButton extends Component { 46 | render(): ReactNode { 47 | let {className, icon, title} = this.props; 48 | 49 | return ( 50 | 55 | 56 | 57 | ); 58 | } 59 | 60 | onInnerClick = (event: React.MouseEvent): void => { 61 | event.preventDefault(); 62 | event.stopPropagation(); 63 | 64 | let {onClick} = this.props; 65 | 66 | if (onClick) { 67 | onClick(); 68 | } 69 | }; 70 | 71 | static Wrapper = Wrapper; 72 | } 73 | -------------------------------------------------------------------------------- /src/cli/commands/default.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import { 4 | Castable, 5 | Command, 6 | Context, 7 | ExpectedError, 8 | Options, 9 | command, 10 | option, 11 | param, 12 | } from 'clime'; 13 | import open from 'open'; 14 | 15 | import {Config, readConfigFromPackageFile} from '../core/config'; 16 | import {Server} from '../core/server'; 17 | 18 | export class BiuOptions extends Options { 19 | @option({ 20 | flag: 'p', 21 | description: 'Port to listen, defaults to `8088`.', 22 | default: 8088, 23 | }) 24 | port!: number; 25 | 26 | @option({ 27 | flag: 'o', 28 | description: 'Open browser.', 29 | toggle: true, 30 | }) 31 | open!: boolean; 32 | } 33 | 34 | @command({ 35 | description: 'Biu! Biu! Biu!', 36 | }) 37 | export default class extends Command { 38 | async execute( 39 | @param({ 40 | description: 'Task configuration file to require, defaults to `.biu`.', 41 | default: '.biu', 42 | }) 43 | configFile: Castable.File, 44 | options: BiuOptions, 45 | context: Context, 46 | ): Promise { 47 | let config: Config; 48 | 49 | let configFilePath = await configFile.exists(['', '.js', '.json']); 50 | 51 | if (configFilePath) { 52 | config = configFile.require(); 53 | } else if (configFile.default) { 54 | config = readConfigFromPackageFile(context.cwd); 55 | console.info('Configuration loaded from "package.json".'); 56 | } else { 57 | throw new ExpectedError( 58 | `Config file "${configFile.source}" (.js, .json) does not exist`, 59 | ); 60 | } 61 | 62 | let server = new Server(config, Path.dirname(configFile.fullName)); 63 | 64 | await server.listen(options.port); 65 | 66 | let url = `http://localhost:${options.port}/`; 67 | 68 | console.info(`Open ${url} to start tasks.`); 69 | 70 | if (options.open) { 71 | open(url); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@tool-bar-button.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import Icon from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.a` 9 | background-color: rgba(0, 0, 0, 0.1); 10 | color: ${props => props.theme.background}; 11 | padding: 3px 5px; 12 | border-radius: 3px; 13 | margin-left: 5px; 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | 18 | &:hover { 19 | background-color: rgba(0, 0, 0, 0.2); 20 | } 21 | 22 | &:active { 23 | background-color: rgba(0, 0, 0, 0.3); 24 | } 25 | 26 | &.green { 27 | &:hover { 28 | background-color: ${props => props.theme.greenAccent()}; 29 | } 30 | 31 | &:active { 32 | background-color: ${props => props.theme.greenAccent(-0.2)}; 33 | } 34 | } 35 | 36 | &.red { 37 | &:hover { 38 | background-color: ${props => props.theme.dangerAccent()}; 39 | } 40 | 41 | &:active { 42 | background-color: ${props => props.theme.dangerAccent(-0.2)}; 43 | } 44 | } 45 | 46 | &.yellow { 47 | &:hover { 48 | background-color: ${props => props.theme.alertAccent(0.4)}; 49 | } 50 | 51 | &:active { 52 | background-color: ${props => props.theme.alertAccent(0)}; 53 | } 54 | } 55 | `; 56 | 57 | export type ToolBarButtonAccent = 'green' | 'red' | 'yellow'; 58 | 59 | export interface ToolBarButtonProps { 60 | className?: string; 61 | icon: string; 62 | title: string; 63 | accent?: ToolBarButtonAccent; 64 | onClick?(): void; 65 | } 66 | 67 | @observer 68 | export class ToolBarButton extends Component { 69 | render(): ReactNode { 70 | let {className, icon, title, accent, onClick} = this.props; 71 | 72 | return ( 73 | 77 | {title} 78 | 79 | ); 80 | } 81 | 82 | static Wrapper = Wrapper; 83 | } 84 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@group-nav/group-nav.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import ScrollHorizontal from 'react-scroll-horizontal'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | import {GroupNavLink} from './@group-nav-link'; 9 | 10 | const Wrapper = styled.div` 11 | height: 20px; 12 | 13 | &.hidden { 14 | display: none; 15 | } 16 | 17 | .scroll-horizontal-restricted { 18 | & > div { 19 | min-width: 100%; 20 | } 21 | } 22 | `; 23 | 24 | export interface GroupNavProps { 25 | className?: string; 26 | groupNames: string[]; 27 | nowGroupName: string | undefined; 28 | onGroupNavLinkClick?(name: string): void; 29 | onGroupNavStartGroupClick?(name: string): void; 30 | onGroupNavRestartGroupClick?(name: string): void; 31 | onGroupNavStopGroupClick?(name: string): void; 32 | onGroupNavCloseGroupClick?(name: string): void; 33 | } 34 | 35 | @observer 36 | export class GroupNav extends Component { 37 | render(): ReactNode { 38 | let { 39 | className, 40 | groupNames, 41 | nowGroupName, 42 | onGroupNavLinkClick, 43 | onGroupNavStartGroupClick, 44 | onGroupNavRestartGroupClick, 45 | onGroupNavStopGroupClick, 46 | onGroupNavCloseGroupClick, 47 | } = this.props; 48 | 49 | return ( 50 | 57 | 61 | {groupNames.map(name => ( 62 | 72 | ))} 73 | 74 | 75 | ); 76 | } 77 | 78 | static Wrapper = Wrapper; 79 | } 80 | -------------------------------------------------------------------------------- /src/cli/core/config.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as Path from 'path'; 3 | 4 | import {ExpectedError} from 'clime'; 5 | 6 | import {ProblemMatcherPatternBase} from './problem-matcher'; 7 | 8 | export interface ProblemMatcherPatternConfig extends ProblemMatcherPatternBase { 9 | regexp: string; 10 | } 11 | 12 | export interface ProblemMatcherBackgroundConfig { 13 | activeOnStart: boolean; 14 | beginsPattern: string; 15 | endsPattern: string; 16 | } 17 | 18 | export interface ProblemMatcherConfig { 19 | owner: string; 20 | pattern: ProblemMatcherPatternConfig | ProblemMatcherPatternConfig[]; 21 | background?: ProblemMatcherBackgroundConfig; 22 | } 23 | 24 | export interface TaskConfig { 25 | executable: string; 26 | args?: string[]; 27 | cwd?: string; 28 | stdout?: boolean; 29 | stderr?: boolean; 30 | watch?: string | string[]; 31 | problemMatcher?: string | ProblemMatcherConfig; 32 | autoClose?: boolean; 33 | } 34 | 35 | export interface Config { 36 | tasks: Dictionary; 37 | groups: Dictionary | undefined; 38 | problemMatchers: Dictionary | undefined; 39 | } 40 | 41 | interface PackageData { 42 | scripts: Dictionary; 43 | biu?: { 44 | groups?: Dictionary; 45 | }; 46 | biuGroups?: Dictionary; 47 | 'biu-groups'?: Dictionary; 48 | } 49 | 50 | export function readConfigFromPackageFile(cwd = process.cwd()): Config { 51 | let packageData: PackageData; 52 | let useYarn = FS.existsSync(Path.join(cwd, 'yarn.lock')); 53 | 54 | let executable = useYarn ? 'yarn' : 'npm'; 55 | 56 | try { 57 | packageData = require(Path.join(cwd, 'package.json')); 58 | } catch (error) { 59 | throw new ExpectedError('Error requiring "package.json"'); 60 | } 61 | 62 | let scriptDict = packageData.scripts; 63 | 64 | if (!scriptDict) { 65 | throw new ExpectedError('No `scripts` defined in "package.json"'); 66 | } 67 | 68 | let taskDict: Dictionary = {}; 69 | 70 | for (let name of Object.keys(scriptDict)) { 71 | taskDict[name] = { 72 | executable, 73 | args: ['run', name], 74 | }; 75 | } 76 | 77 | let groupDict = 78 | (packageData.biu && packageData.biu.groups) || 79 | packageData.biuGroups || 80 | packageData['biu-groups']; 81 | 82 | return { 83 | tasks: taskDict, 84 | groups: groupDict, 85 | problemMatchers: undefined, 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/client/components/menu/menu.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import {action, observable} from 'mobx'; 4 | import React, {Component, ReactNode} from 'react'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | import {Group} from './@group'; 9 | import {Logo} from './@logo'; 10 | import {PinButton} from './@pin-button'; 11 | 12 | const Wrapper = styled.div` 13 | background-color: #fff; 14 | display: flex; 15 | flex-direction: column; 16 | padding-top: 6px; 17 | position: fixed; 18 | z-index: 999; 19 | transition: all 0.5s; 20 | 21 | &.pinned { 22 | position: static; 23 | } 24 | 25 | &.hide { 26 | transform: translateX(-285px); 27 | transition: all 0.5s; 28 | cursor: pointer; 29 | 30 | ${Group.Wrapper} { 31 | opacity: 0; 32 | transition: all 0.5s; 33 | } 34 | } 35 | 36 | ${Group.Wrapper} { 37 | padding: 5px 10px 5px 16px; 38 | flex: 1; 39 | } 40 | `; 41 | 42 | const TopTool = styled.div` 43 | text-align: right; 44 | padding-right: 10px; 45 | `; 46 | 47 | const Header = styled.div` 48 | height: 36px; 49 | padding: 10px 20px 20px 20px; 50 | flex: none; 51 | 52 | ${Logo.Wrapper} { 53 | margin: auto; 54 | } 55 | `; 56 | 57 | export interface MenuProps { 58 | className?: string; 59 | } 60 | 61 | @observer 62 | export class Menu extends Component { 63 | @observable 64 | pinned = true; 65 | 66 | @observable 67 | hide = false; 68 | 69 | render(): ReactNode { 70 | let {className} = this.props; 71 | 72 | return ( 73 | 83 | 84 | 89 | 90 |
91 | 92 |
93 | 94 |
95 | ); 96 | } 97 | 98 | @action 99 | onPinButtonClick = (): void => { 100 | this.pinned = !this.pinned; 101 | }; 102 | 103 | @action 104 | onMouseEnter = (): void => { 105 | this.hide = false; 106 | }; 107 | 108 | @action 109 | onMouseLeave = (): void => { 110 | this.hide = true; 111 | }; 112 | 113 | static Wrapper = Wrapper; 114 | } 115 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/@block.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode, createRef} from 'react'; 4 | import Scrollbars from 'react-custom-scrollbars'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div` 9 | background-color: ${props => props.theme.washedOutBlack}; 10 | width: 100%; 11 | height: 100%; 12 | color: ${props => props.theme.text.placeholder}; 13 | font-size: 12px; 14 | 15 | .custom-scroll-bar-track { 16 | position: absolute; 17 | width: 6px; 18 | transition: opacity 200ms ease 0s; 19 | opacity: 0; 20 | right: 2px; 21 | bottom: 6px; 22 | top: 6px; 23 | border-radius: 3px; 24 | } 25 | 26 | .custom-scroll-bar-thumb { 27 | position: relative; 28 | display: block; 29 | width: 100%; 30 | cursor: pointer; 31 | border-radius: inherit; 32 | background-color: rgba(255, 255, 255, 0.2); 33 | right: 2px; 34 | } 35 | `; 36 | 37 | const OutputWrapper = styled.div` 38 | padding: 7px; 39 | font-family: monospace; 40 | word-break: break-all; 41 | `; 42 | 43 | export interface BlockProps { 44 | className?: string; 45 | html: string; 46 | } 47 | 48 | @observer 49 | export class Block extends Component { 50 | scrollbars: React.RefObject = createRef(); 51 | 52 | render(): ReactNode { 53 | let {className, html} = this.props; 54 | 55 | this.autoScroll(); 56 | 57 | return ( 58 | 59 | ( 64 |
69 | )} 70 | renderThumbVertical={(props, styles) => ( 71 |
76 | )} 77 | > 78 | 79 | 80 | 81 | ); 82 | } 83 | 84 | autoScroll = (): void => { 85 | if (this.scrollbars.current) { 86 | let scrollbars = this.scrollbars.current; 87 | 88 | let height = scrollbars.getClientHeight(); 89 | 90 | let scrollTop = scrollbars.getScrollTop(); 91 | 92 | let scrollHeight = scrollbars.getScrollHeight(); 93 | 94 | let offset = scrollHeight - height - scrollTop; 95 | 96 | if (offset < 5) { 97 | setTimeout(() => { 98 | scrollbars.scrollToBottom(); 99 | }, 100); 100 | } 101 | } 102 | }; 103 | 104 | static Wrapper = Wrapper; 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "biu", 3 | "version": "0.3.0-alpha.2", 4 | "description": "The command-line task hub.", 5 | "bin": { 6 | "biu": "bld/cli/main.js" 7 | }, 8 | "scripts": { 9 | "lint-cli": "tslint -c src/cli/tslint.json -p src/cli/tsconfig.json", 10 | "lint-client": "tslint -c src/client/tslint.json -p src/client/tsconfig.json", 11 | "lint": "yarn lint-cli && yarn lint-client", 12 | "build-cli": "yarn lint-cli && rimraf bld/cli && tsc -p src/cli/tsconfig.json", 13 | "build-client": "yarn lint-client && rimraf bld/client && parcel build src/client/index.html --out-dir bld/client", 14 | "build": "yarn build-cli && yarn build-client", 15 | "start-cli": "node bld/cli/main.js", 16 | "start-client": "parcel serve src/client/index.html --port 8089 --out-dir bld/client" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/makeflow/biu.git" 21 | }, 22 | "keywords": [ 23 | "command-line", 24 | "command", 25 | "cli", 26 | "multiple", 27 | "hub" 28 | ], 29 | "author": "vilicvane", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/makeflow/biu/issues" 33 | }, 34 | "homepage": "https://github.com/makeflow/biu#readme", 35 | "files": [ 36 | "bld/cli/**/*.js", 37 | "bld/client/**" 38 | ], 39 | "dependencies": { 40 | "@makeflow/mobx-utils": "^0.1.0", 41 | "ansi-to-html": "^0.6.6", 42 | "chalk": "^2.4.1", 43 | "chokidar": "^2.0.4", 44 | "classnames": "^2.2.6", 45 | "clime": "^0.5.9", 46 | "color": "^3.0.0", 47 | "components": "^0.1.0", 48 | "express": "^4.16.3", 49 | "lodash": "^4.17.11", 50 | "mobx": "^5.1.0", 51 | "mobx-react": "^5.2.8", 52 | "npm-which": "^3.0.1", 53 | "open": "0.0.5", 54 | "prettier": "^1.14.2", 55 | "rc-dropdown": "^2.2.0", 56 | "react": "^15.3.0 || 16", 57 | "react-custom-scrollbars": "^4.2.1", 58 | "react-dom": "^15.3.0 || 16", 59 | "react-fa": "^5.0.0", 60 | "react-flip-move": "^3.0.2", 61 | "react-mosaic": "^0.0.20", 62 | "react-mosaic-component": "^1.1.1", 63 | "react-scroll-horizontal": "^1.5.1", 64 | "shell-escape": "^0.2.0", 65 | "socket.io": "^2.1.1", 66 | "source-map-support": "^0.5.6", 67 | "strip-color": "^0.1.0", 68 | "styled-components": "^3.4.6", 69 | "tslib": "^1.9.3", 70 | "villa": "^0.3.1" 71 | }, 72 | "devDependencies": { 73 | "@magicspace/configs": "^0.1.29", 74 | "@types/chalk": "^2.2.0", 75 | "@types/chokidar": "^1.7.5", 76 | "@types/classnames": "^2.2.6", 77 | "@types/color": "^3.0.0", 78 | "@types/express": "^4.16.0", 79 | "@types/lodash": "^4.14.116", 80 | "@types/node": "^10.5.7", 81 | "@types/react": "^16.4.14", 82 | "@types/react-custom-scrollbars": "^4.0.5", 83 | "@types/react-dom": "^16.0.7", 84 | "@types/react-fa": "^4.1.5", 85 | "@types/socket.io": "^1.4.36", 86 | "@types/socket.io-client": "^1.4.32", 87 | "babel-preset-react": "^6.24.1", 88 | "dot-template-types": "^0.0.7", 89 | "parcel-bundler": "^1.9.7", 90 | "parcel-plugin-typescript": "^1.0.0", 91 | "rimraf": "^2.6.2", 92 | "ts-node": "^7.0.1", 93 | "tslint": "^5.11.0", 94 | "tslint-language-service": "^0.9.9", 95 | "typescript": "^3.0.1" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/client/components/disconnected/disconnected-view.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {Icon} from 'react-fa'; 5 | 6 | import {styled} from 'theme'; 7 | 8 | const Wrapper = styled.div` 9 | position: absolute; 10 | z-index: 9999; 11 | left: 0; 12 | top: 0; 13 | bottom: 0; 14 | right: 0; 15 | background-color: rgba(239, 243, 245, 0.85); 16 | transition: all 0.5s; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | 21 | &.hidden { 22 | opacity: 0; 23 | visibility: hidden; 24 | pointer-events: none; 25 | transition: all 1s; 26 | transition-delay: 1s; 27 | } 28 | `; 29 | 30 | const HintBoxWrapper = styled.div` 31 | &.animated { 32 | animation: fadeUpIn 0.3s; 33 | } 34 | `; 35 | 36 | const HintBox = styled.div` 37 | position: relative; 38 | text-align: center; 39 | margin-bottom: 20px; 40 | background-color: #fff; 41 | padding: 60px 50px 40px 50px; 42 | transition: all 0.3s; 43 | 44 | &:before, 45 | &:after { 46 | position: absolute; 47 | content: ''; 48 | top: 100px; 49 | bottom: 5px; 50 | left: 30px; 51 | right: 30px; 52 | z-index: -1; 53 | box-shadow: 0 0 40px 13px rgba(0, 0, 0, 0.1); 54 | border-radius: 100px/20px; 55 | } 56 | `; 57 | 58 | const HintIcon = styled.div` 59 | font-size: 60px; 60 | margin-bottom: 40px; 61 | `; 62 | 63 | const HintTitle = styled.div` 64 | text-align: center; 65 | color: ${props => props.theme.text.regular}; 66 | margin-bottom: 15px; 67 | `; 68 | 69 | const HintSubtitle = styled.div` 70 | width: 270px; 71 | font-size: 13px; 72 | color: ${props => props.theme.text.placeholder}; 73 | `; 74 | 75 | export interface DisconnectedViewProps { 76 | className?: string; 77 | connect?: boolean; 78 | } 79 | 80 | @observer 81 | export class DisconnectedView extends Component { 82 | render(): ReactNode { 83 | let {className, connect} = this.props; 84 | 85 | return ( 86 | 93 | 94 | {connect ? ( 95 | 96 | 97 | 98 | 99 | Connected! 100 | Welcome to Biu. Start playing around! 101 | 102 | ) : ( 103 | 104 | 105 | 106 | 107 | Oops, disconnected. 108 | 109 | Have you restarted Biu after editor restarting / reloading? 110 | 111 | 112 | )} 113 | 114 | 115 | ); 116 | } 117 | 118 | static Wrapper = Wrapper; 119 | } 120 | -------------------------------------------------------------------------------- /src/client/theme.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color'; 2 | import {ClassAttributes, DetailedHTMLProps, HTMLAttributes} from 'react'; 3 | import _styled, { 4 | InterpolationFunction, 5 | ThemedBaseStyledInterface, 6 | ThemedCssFunction, 7 | ThemedStyledFunction as _ThemedStyledFunction, 8 | ThemedStyledProps, 9 | css as _css, 10 | } from 'styled-components'; 11 | 12 | const light = new Color('#fff'); 13 | const dark = new Color('#000'); 14 | 15 | const accent = new Color('#409eff'); 16 | const darkerAccent = new Color('#298df4'); 17 | const alertAccent = new Color('#e6a23c'); 18 | const dangerAccent = new Color('#f56c6c'); 19 | const greenAccent = new Color('#67c23a'); 20 | const safeAccent = new Color('#5fc300'); 21 | 22 | const gray = new Color('#909399'); 23 | 24 | type LightnessModifier = (lightness?: number) => string; 25 | 26 | function createLightnessModifier(color: Color): LightnessModifier { 27 | return (lightness = 0): string => { 28 | let modified = 29 | lightness >= 0 30 | ? color.mix(light, lightness) 31 | : color.mix(dark, -lightness); 32 | 33 | return modified.hex(); 34 | }; 35 | } 36 | 37 | const accentLightnessModifier = createLightnessModifier(accent); 38 | const darkerAccentLightnessModifier = createLightnessModifier(darkerAccent); 39 | const alertAccentLightnessModifier = createLightnessModifier(alertAccent); 40 | const dangerAccentLightnessModifier = createLightnessModifier(dangerAccent); 41 | const greenAccentLightnessModifier = createLightnessModifier(greenAccent); 42 | const safeAccentLightnessModifier = createLightnessModifier(safeAccent); 43 | const grayLightnessModifier = createLightnessModifier(gray); 44 | 45 | export const theme = { 46 | accent: accentLightnessModifier, 47 | darkerAccent: darkerAccentLightnessModifier, 48 | alertAccent: alertAccentLightnessModifier, 49 | dangerAccent: dangerAccentLightnessModifier, 50 | greenAccent: greenAccentLightnessModifier, 51 | safeAccent: safeAccentLightnessModifier, 52 | gray: grayLightnessModifier, 53 | light: light.hex(), 54 | washedOutBlack: '#31363D', 55 | background: '#eff3f5', 56 | text: { 57 | navPrimary: '#333', 58 | navRegular: '#444', 59 | navSecondary: '#999', 60 | navPlaceholder: '#888', 61 | primary: '#303133', 62 | regular: '#5e6d82', 63 | secondary: '#909399', 64 | placeholder: '#C0C4CC', 65 | hint: '#c8c8c8', 66 | }, 67 | border: { 68 | base: '#dcdfe6', 69 | light: '#e4e7ed', 70 | lighter: '#ebfef5', 71 | extraLight: '#f2f6fc', 72 | }, 73 | bar: { 74 | green: '#B8EB9F', 75 | yellow: '#FCEAAF', 76 | gray: '#E5E5E5', 77 | }, 78 | }; 79 | 80 | export type StyledWrapperProps = DetailedHTMLProps< 81 | HTMLAttributes, 82 | T 83 | >; 84 | 85 | export type Theme = typeof theme; 86 | 87 | export type ThemedStyledFunction = _ThemedStyledFunction< 88 | ClassAttributes & HTMLAttributes & P, 89 | Theme, 90 | ClassAttributes & HTMLAttributes & P 91 | >; 92 | 93 | export type ThemedInterpolationFunction

= InterpolationFunction< 94 | ThemedStyledProps 95 | >; 96 | 97 | export const styled = _styled as ThemedBaseStyledInterface; 98 | export const css = _css as ThemedCssFunction; 99 | 100 | export const lightLinkStyle = css` 101 | color: ${props => props.theme.light} !important; 102 | text-decoration: none !important; 103 | opacity: 0.9; 104 | 105 | &:hover { 106 | opacity: 1; 107 | } 108 | `; 109 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/group.tsx: -------------------------------------------------------------------------------- 1 | import {inject, observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import {action, entries, observable, values} from 'mobx'; 4 | import React, {Component, ReactNode} from 'react'; 5 | import Scrollbars from 'react-custom-scrollbars'; 6 | 7 | import { 8 | Task, 9 | TaskDict, 10 | TaskGroupDict, 11 | TaskService, 12 | } from 'services/task-service'; 13 | import {styled} from 'theme'; 14 | 15 | import {GroupNav} from './@group-nav'; 16 | import {List} from './@list'; 17 | 18 | const Wrapper = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | position: relative; 22 | 23 | ${GroupNav.Wrapper} { 24 | padding: 0 10px; 25 | margin-bottom: 13px; 26 | margin-left: 10px; 27 | margin-right: 15px; 28 | flex: none; 29 | } 30 | `; 31 | 32 | const ScrollbarsWrapper = styled(Scrollbars)` 33 | flex: 1; 34 | margin-bottom: 20px; 35 | `; 36 | 37 | export interface GroupProps { 38 | className?: string; 39 | } 40 | 41 | @observer 42 | export class Group extends Component { 43 | @inject 44 | taskService!: TaskService; 45 | 46 | @observable 47 | nowGroupName: string | undefined; 48 | 49 | render(): ReactNode { 50 | let {className} = this.props; 51 | 52 | let groups = this.taskService.taskGroups; 53 | 54 | let keys = Object.keys(groups); 55 | 56 | let nowGroupTasks = this.getNowGroupTasks(groups, this.taskService.tasks); 57 | 58 | return ( 59 | 60 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | 76 | getNowGroupTasks = (groupDict: TaskGroupDict, taskDict: TaskDict): Task[] => { 77 | let keys = Object.keys(groupDict); 78 | 79 | let tasks: Task[] = []; 80 | 81 | let nowGroupName = this.nowGroupName; 82 | 83 | if (keys.length && nowGroupName) { 84 | let nowGroupTaskNames = groupDict[nowGroupName]; 85 | 86 | for (let [key, task] of entries(taskDict)) { 87 | if (nowGroupTaskNames.includes(key)) { 88 | tasks.push(task); 89 | } 90 | } 91 | } else { 92 | for (let task of values(this.taskService.tasks)) { 93 | tasks.push(task); 94 | } 95 | } 96 | 97 | return tasks; 98 | }; 99 | 100 | @action 101 | onGroupNavLinkClick = (name: string): void => { 102 | if (this.nowGroupName === name) { 103 | this.nowGroupName = undefined; 104 | } else { 105 | this.nowGroupName = name; 106 | } 107 | }; 108 | 109 | onGroupNavStartGroupClick = (name: string): void => { 110 | this.taskService.startGroup(name); 111 | }; 112 | 113 | onGroupNavRestartGroupClick = (name: string): void => { 114 | this.taskService.restartGroup(name); 115 | }; 116 | 117 | onGroupNavStopGroupClick = (name: string): void => { 118 | this.taskService.stopGroup(name); 119 | }; 120 | 121 | onGroupNavCloseGroupClick = (name: string): void => { 122 | this.taskService.closeGroup(name); 123 | }; 124 | 125 | static Wrapper = Wrapper; 126 | } 127 | -------------------------------------------------------------------------------- /src/client/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/client/components/window-manager/manager.tsx: -------------------------------------------------------------------------------- 1 | import {inject, observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import {action} from 'mobx'; 4 | import React, {Component, ReactNode} from 'react'; 5 | import {Mosaic, MosaicNode} from 'react-mosaic-component'; 6 | 7 | import {TaskId, TaskService} from 'services/task-service'; 8 | import {styled} from 'theme'; 9 | 10 | import {ManagerStyle} from './@manager-style'; 11 | import {ToolBarButton} from './@tool-bar-button'; 12 | import {Window} from './@window'; 13 | import {ZeroState} from './@zero-state'; 14 | 15 | const WindowManager = Mosaic.ofType(); 16 | 17 | const Wrapper = styled(ManagerStyle)` 18 | padding: 30px 40px 20px 40px; 19 | transition: all 0.5s; 20 | transform: translate3d(0, 0, 0); 21 | 22 | &.single-windowed { 23 | padding-top: 20px; 24 | transition: all 0.5s; 25 | transform: translate3d(0, 0, 0); 26 | } 27 | `; 28 | 29 | const ToolBar = styled.div` 30 | position: absolute; 31 | font-size: 11px; 32 | right: 55px; 33 | top: 15px; 34 | transition: all 0.5s; 35 | opacity: 0.5; 36 | left: 50px; 37 | display: flex; 38 | justify-content: flex-end; 39 | 40 | &.hidden { 41 | opacity: 0; 42 | transition: all 0.3s; 43 | pointer-events: none; 44 | 45 | &:hover { 46 | opacity: 0; 47 | } 48 | } 49 | 50 | &:hover { 51 | opacity: 1; 52 | transition: all 0.3s; 53 | } 54 | `; 55 | 56 | export interface ManagerProps { 57 | className?: string; 58 | } 59 | 60 | @observer 61 | export class Manager extends Component { 62 | @inject 63 | taskService!: TaskService; 64 | 65 | windowKey: number = 0; 66 | 67 | render(): ReactNode { 68 | let {className} = this.props; 69 | 70 | let hideToolBar = this.taskService.createdTaskMap.size < 2; 71 | 72 | return ( 73 | 80 | 81 | 87 | 93 | 99 | 105 | 110 | 115 | 116 | { 119 | return ; 120 | }} 121 | onChange={this.onWindowChange} 122 | value={this.taskService.currentNode} 123 | zeroStateView={} 124 | /> 125 | 126 | ); 127 | } 128 | 129 | @action 130 | onWindowChange = (newNode: MosaicNode | null): void => { 131 | let taskService = this.taskService; 132 | 133 | taskService.saveNodeLayout(newNode); 134 | 135 | taskService.currentNode = newNode; 136 | }; 137 | 138 | onStartAllClick = (): void => { 139 | this.taskService.startAll(); 140 | }; 141 | 142 | onRestartAllClick = (): void => { 143 | this.taskService.restartAll(); 144 | }; 145 | 146 | onStopAllClick = (): void => { 147 | this.taskService.stopAll(); 148 | }; 149 | 150 | onCloseAllClick = (): void => { 151 | this.taskService.closeAll(); 152 | }; 153 | 154 | onAutoArrangeClick = (): void => { 155 | this.taskService.autoArrangeWindows(); 156 | }; 157 | 158 | @action 159 | onRestoreLayoutClick = (): void => { 160 | let taskService = this.taskService; 161 | 162 | taskService.currentNode = taskService.restoreNode(taskService.currentNode); 163 | }; 164 | 165 | static Wrapper = Wrapper; 166 | } 167 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@window/window.tsx: -------------------------------------------------------------------------------- 1 | import {inject, observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import {ExpandButton, MosaicBranch, MosaicWindow} from 'react-mosaic-component'; 5 | 6 | import { 7 | Task, 8 | TaskId, 9 | TaskService, 10 | TaskStatus, 11 | getTaskStatus, 12 | } from 'services/task-service'; 13 | import {styled} from 'theme'; 14 | 15 | import {Block} from './@block'; 16 | import { 17 | WindowRemoveButton, 18 | WindowRestartButton, 19 | WindowStatusDot, 20 | WindowStopButton, 21 | } from './@window-tools'; 22 | import {WindowStartButton} from './@window-tools/window-start-button'; 23 | 24 | const Wrapper = styled.div` 25 | flex: 1; 26 | display: flex; 27 | `; 28 | 29 | const MosaicWindowWithType = MosaicWindow.ofType(); 30 | 31 | const WindowWrapper = styled(MosaicWindowWithType)` 32 | flex: 1; 33 | box-sizing: border-box; 34 | 35 | &.hover { 36 | animation: pulse 0.75s; 37 | } 38 | 39 | ${WindowStatusDot.Wrapper} { 40 | position: absolute; 41 | top: 15px; 42 | left: 5px; 43 | } 44 | 45 | ${Block.Wrapper} { 46 | transition: opacity 0.3s; 47 | 48 | &.stopped { 49 | opacity: 0.9; 50 | transition: opacity 0.3s; 51 | } 52 | } 53 | `; 54 | 55 | const WindowSubTitle = styled.div` 56 | position: absolute; 57 | font-size: 12px; 58 | top: 10px; 59 | left: 85px; 60 | right: 85px; 61 | height: 18px; 62 | white-space: nowrap; 63 | overflow: hidden; 64 | flex: none; 65 | text-align: center; 66 | pointer-events: none; 67 | text-overflow: ellipsis; 68 | user-select: none; 69 | color: ${props => props.theme.text.hint}; 70 | `; 71 | 72 | export interface WindowProps { 73 | className?: string; 74 | id: TaskId; 75 | path: MosaicBranch[]; 76 | } 77 | 78 | @observer 79 | export class Window extends Component { 80 | @inject 81 | taskService!: TaskService; 82 | 83 | render(): ReactNode { 84 | let {className, id, path} = this.props; 85 | 86 | let task = this.taskService.createdTaskMap.get(id)!; 87 | 88 | let {name, output, status} = task; 89 | 90 | let hover = id === this.taskService.currentHoverTaskId; 91 | 92 | return ( 93 | 94 | 100 | {status === TaskStatus.running ? ( 101 | { 103 | this.onRestartButtonClick(task); 104 | }} 105 | /> 106 | ) : ( 107 | undefined 108 | )} 109 | {status === TaskStatus.running || 110 | status === TaskStatus.restarting ? ( 111 | { 113 | this.onStopButtonClick(task); 114 | }} 115 | /> 116 | ) : ( 117 | undefined 118 | )} 119 | {status === TaskStatus.stopped || 120 | status === TaskStatus.stopping ? ( 121 | { 123 | this.onStartButtonClick(task); 124 | }} 125 | /> 126 | ) : ( 127 | undefined 128 | )} 129 | 130 | { 132 | this.onRemoveButtonClick(task); 133 | }} 134 | /> 135 | 136 | } 137 | > 138 | 139 | {getTaskStatus(task)} 140 | 144 | 145 | 146 | ); 147 | } 148 | 149 | onRemoveButtonClick = (task: Task): void => { 150 | this.taskService.close(task); 151 | }; 152 | 153 | onRestartButtonClick = (task: Task): void => { 154 | this.taskService.restart(task); 155 | }; 156 | 157 | onStopButtonClick = (task: Task): void => { 158 | this.taskService.stop(task); 159 | }; 160 | 161 | onStartButtonClick = (task: Task): void => { 162 | this.taskService.start(task); 163 | }; 164 | 165 | static Wrapper = Wrapper; 166 | } 167 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@group-nav/@group-nav-link.tsx: -------------------------------------------------------------------------------- 1 | import 'rc-dropdown/assets/index.css'; 2 | 3 | import {observer} from '@makeflow/mobx-utils'; 4 | import classNames from 'classnames'; 5 | import {action, observable} from 'mobx'; 6 | import Dropdown from 'rc-dropdown'; 7 | import React, {Component, ReactNode} from 'react'; 8 | import {Icon} from 'react-fa'; 9 | 10 | import {styled} from 'theme'; 11 | 12 | type OnGroupClickFunc = (name: string) => void; 13 | type OnClickFunc = () => void; 14 | 15 | const Wrapper = styled.a` 16 | margin-top: 3px; 17 | color: ${props => props.theme.text.placeholder}; 18 | margin-right: 7px; 19 | font-size: 12px; 20 | animation: fadeIn 0.5s; 21 | 22 | &:hover { 23 | color: ${props => props.theme.text.navPlaceholder}; 24 | } 25 | 26 | &.active { 27 | color: ${props => props.theme.text.navPlaceholder}; 28 | } 29 | `; 30 | 31 | const GroupMenuLayer = styled.div` 32 | background-color: ${props => props.theme.light}; 33 | border-radius: 4px; 34 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); 35 | margin-left: -15px; 36 | `; 37 | 38 | const GroupMenuButton = styled.a` 39 | display: block; 40 | padding: 6px 8px; 41 | font-size: 10px; 42 | color: ${props => props.theme.text.navPlaceholder}; 43 | 44 | span { 45 | position: relative; 46 | top: -0.5px; 47 | font-size: 7px; 48 | margin-right: 3px; 49 | 50 | &.red { 51 | color: ${props => props.theme.dangerAccent()}; 52 | } 53 | 54 | &.green { 55 | color: ${props => props.theme.greenAccent()}; 56 | } 57 | 58 | &.yellow { 59 | color: ${props => props.theme.alertAccent()}; 60 | } 61 | } 62 | 63 | &:hover { 64 | background-color: rgba(0, 0, 0, 0.025); 65 | } 66 | 67 | &:active { 68 | background-color: rgba(0, 0, 0, 0.06); 69 | } 70 | `; 71 | 72 | const createGroupMenu = ( 73 | onStartGroupClick?: OnClickFunc, 74 | onRestartGroupClick?: OnClickFunc, 75 | onStopGroupClick?: OnClickFunc, 76 | onCloseGroupClick?: OnClickFunc, 77 | ): ReactNode => ( 78 | 79 | 80 | {' '} 85 | Start group 86 | 87 | 88 | Restart group 89 | 90 | 91 | Stop group 92 | 93 | 94 | Close group 95 | 96 | 97 | ); 98 | 99 | export interface GroupNavLinkProps { 100 | className?: string; 101 | name: string; 102 | active?: boolean; 103 | onClick?: OnGroupClickFunc; 104 | onStartGroupClick?: OnGroupClickFunc; 105 | onRestartGroupClick?: OnGroupClickFunc; 106 | onStopGroupClick?: OnGroupClickFunc; 107 | onCloseGroupClick?: OnGroupClickFunc; 108 | } 109 | 110 | @observer 111 | export class GroupNavLink extends Component { 112 | @observable 113 | hover = false; 114 | 115 | render(): ReactNode { 116 | let {className, name, active} = this.props; 117 | 118 | return ( 119 | 129 | 140 | {name.toUpperCase()} 141 | 142 | 143 | ); 144 | } 145 | 146 | onInnerClick = (): void => { 147 | let {name, onClick} = this.props; 148 | 149 | if (onClick) { 150 | onClick(name); 151 | } 152 | }; 153 | 154 | onInnerStartGroupClick = (): void => { 155 | let {name, onStartGroupClick} = this.props; 156 | 157 | if (onStartGroupClick) { 158 | onStartGroupClick(name); 159 | } 160 | }; 161 | 162 | onInnerRestartGroupClick = (): void => { 163 | let {name, onRestartGroupClick} = this.props; 164 | 165 | if (onRestartGroupClick) { 166 | onRestartGroupClick(name); 167 | } 168 | }; 169 | 170 | onInnerStopGroupClick = (): void => { 171 | let {name, onStopGroupClick} = this.props; 172 | 173 | if (onStopGroupClick) { 174 | onStopGroupClick(name); 175 | } 176 | }; 177 | 178 | onInnerCloseGroupClick = (): void => { 179 | let {name, onCloseGroupClick} = this.props; 180 | 181 | if (onCloseGroupClick) { 182 | onCloseGroupClick(name); 183 | } 184 | }; 185 | 186 | @action 187 | onMouseEnter = (): void => { 188 | this.hover = true; 189 | }; 190 | 191 | @action 192 | onMouseLeave = (): void => { 193 | this.hover = false; 194 | }; 195 | 196 | static Wrapper = Wrapper; 197 | } 198 | -------------------------------------------------------------------------------- /src/cli/core/task.ts: -------------------------------------------------------------------------------- 1 | import {ChildProcess, spawn} from 'child_process'; 2 | import {EventEmitter} from 'events'; 3 | 4 | import * as Chokidar from 'chokidar'; 5 | import which from 'npm-which'; 6 | import shellEscape from 'shell-escape'; 7 | import * as v from 'villa'; 8 | 9 | import {ProblemMatcherConfig} from './config'; 10 | import {ProblemMatcher} from './problem-matcher'; 11 | 12 | export interface TaskExitEventData { 13 | code: number; 14 | close: boolean; 15 | } 16 | 17 | export interface TaskProblemsUpdateEventData { 18 | owner: string; 19 | } 20 | 21 | export interface TaskOptions { 22 | cwd: string; 23 | stdout: boolean; 24 | stderr: boolean; 25 | problemMatcher: ProblemMatcherConfig | ProblemMatcherConfig[] | undefined; 26 | watch: string | string[] | undefined; 27 | autoClose: boolean; 28 | } 29 | 30 | export class Task extends EventEmitter { 31 | path: string; 32 | running = false; 33 | problemMatcherMap: Map | undefined; 34 | 35 | private process: ChildProcess | undefined; 36 | private restartScheduleTimer: NodeJS.Timer | undefined; 37 | 38 | constructor( 39 | public name: string, 40 | public executable: string, 41 | public args: string[], 42 | public options: TaskOptions, 43 | ) { 44 | super(); 45 | 46 | try { 47 | this.path = which(options.cwd).sync(executable); 48 | } catch (error) { 49 | this.path = executable; 50 | } 51 | 52 | if (options.problemMatcher) { 53 | let configs = Array.isArray(options.problemMatcher) 54 | ? options.problemMatcher 55 | : [options.problemMatcher]; 56 | 57 | this.problemMatcherMap = new Map( 58 | configs.map<[string, ProblemMatcher]>(config => { 59 | let matcher = new ProblemMatcher(config, options.cwd); 60 | let owner = matcher.owner; 61 | 62 | matcher.on('problems-update', () => 63 | this.emit('problems-update', {owner}), 64 | ); 65 | 66 | return [owner, matcher]; 67 | }), 68 | ); 69 | } 70 | 71 | if (options.watch) { 72 | Chokidar.watch(options.watch, { 73 | cwd: options.cwd, 74 | ignoreInitial: true, 75 | }).on('all', () => this.scheduleRestart()); 76 | } 77 | } 78 | 79 | get line(): string { 80 | return shellEscape([this.executable, ...this.args]); 81 | } 82 | 83 | start(): boolean { 84 | if (this.running) { 85 | return false; 86 | } 87 | 88 | if (this.problemMatcherMap) { 89 | for (let [, matcher] of this.problemMatcherMap) { 90 | matcher.reset(); 91 | } 92 | } 93 | 94 | this.emit('start'); 95 | this.running = true; 96 | 97 | try { 98 | this.process = spawn(this.path, this.args, { 99 | cwd: this.options.cwd, 100 | }); 101 | } catch (error) { 102 | this.handleStop(error); 103 | return true; 104 | } 105 | 106 | this.process.once('error', error => this.handleStop(error)); 107 | this.process.once('exit', code => this.handleStop(undefined, code)); 108 | 109 | this.process.stdout.on('data', (data: Buffer) => { 110 | let map = this.problemMatcherMap; 111 | 112 | if (map) { 113 | for (let [, matcher] of map) { 114 | matcher.push(data); 115 | } 116 | } 117 | 118 | this.emit('stdout', data); 119 | }); 120 | 121 | this.process.stderr.on('data', (data: Buffer) => { 122 | let map = this.problemMatcherMap; 123 | 124 | if (map) { 125 | for (let [, matcher] of map) { 126 | matcher.push(data); 127 | } 128 | } 129 | 130 | this.emit('stderr', data); 131 | }); 132 | 133 | if (this.options.stdout) { 134 | this.process.stdout.pipe(process.stdout); 135 | } 136 | 137 | if (this.options.stderr) { 138 | this.process.stderr.pipe(process.stderr); 139 | } 140 | 141 | return true; 142 | } 143 | 144 | stop(): boolean { 145 | if (!this.running) { 146 | return false; 147 | } 148 | 149 | if (process.platform === 'win32') { 150 | spawn('taskkill', ['/f', '/t', '/pid', this.process!.pid.toString()]); 151 | } else { 152 | this.process!.kill(); 153 | } 154 | 155 | return true; 156 | } 157 | 158 | async stopWait(): Promise { 159 | if (this.stop()) { 160 | await v.awaitable(this, 'stop'); 161 | } 162 | } 163 | 164 | async restart(): Promise { 165 | await this.stopWait(); 166 | this.start(); 167 | } 168 | 169 | private handleStop(error: any, code?: number): void { 170 | if (this.problemMatcherMap) { 171 | for (let [, matcher] of this.problemMatcherMap) { 172 | if (!matcher.background) { 173 | this.emit('problems-update', {owner: matcher.owner}); 174 | } 175 | } 176 | } 177 | 178 | if (error) { 179 | this.emit('error', error); 180 | } else { 181 | this.emit('exit', { 182 | code, 183 | close: this.options.autoClose && code === 0, 184 | }); 185 | } 186 | 187 | if (this.running) { 188 | this.emit('stop'); 189 | this.running = false; 190 | } 191 | } 192 | 193 | private scheduleRestart(): void { 194 | clearTimeout(this.restartScheduleTimer!); 195 | 196 | this.restartScheduleTimer = setTimeout(async () => { 197 | if (this.running) { 198 | this.emit('restarting-on-change'); 199 | await this.restart(); 200 | } 201 | }, 1000); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Biu: The Command-line Task Hub 2 | 3 | Biu is a simple command-line tool for running multiple command-line tasks at the same time. It provides a simple GUI interface and aggregates stdout/stderr streams produced by tasks on demand. 4 | 5 | ![image](https://cloud.githubusercontent.com/assets/970430/26506654/fcafead6-427f-11e7-946c-4090bf8117d9.png) 6 | 7 | ## Features 8 | 9 | - Start tasks in a group with one click. 10 | - Selectively pipe stdout and stderr of specific tasks. 11 | - Aggregate problems from several tasks with different problem matchers. 12 | 13 | ## Installation 14 | 15 | ```sh 16 | # global 17 | yarn global add biu 18 | 19 | # local 20 | yarn add biu --dev 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```sh 26 | biu --help 27 | ``` 28 | 29 | ## Configuration 30 | 31 | Biu loads configuration from a Node.js module, it could either be a `.js` or `.json` file. By default, it tries to require `.biu`, or read `scripts` section of `package.json` from the current working directory if no configuration file is specified and the default `.biu` (`.js`, `.json`) does not exist. 32 | 33 | The configuration contains three fields: `tasks` (required), `groups` and `problemMatchers`. 34 | 35 | ### Using built-in problem matchers 36 | 37 | Currently Biu has the following built-in problem matchers: 38 | 39 | - `$typescript:tsc-watch` 40 | - `$typescript:tslint` 41 | 42 | ```json 43 | { 44 | "tasks": { 45 | "build-app": { 46 | "executable": "tsc", 47 | "args": ["-p", "src/app", "-w"], 48 | "problemMatcher": "$typescript:tsc-watch" 49 | }, 50 | "build-server": { 51 | "executable": "tsc", 52 | "args": ["-p", "src/server", "-w"], 53 | "problemMatcher": "$typescript:tsc-watch" 54 | } 55 | }, 56 | "groups": { 57 | "all": ["build-app", "build-server"] 58 | } 59 | } 60 | ``` 61 | 62 | ### Using custom problem matchers 63 | 64 | To use custom problem matchers, add it to the `problemMatchers` field: 65 | 66 | ```json 67 | { 68 | "tasks": { 69 | "build-app": { 70 | "executable": "tsc", 71 | "args": ["-p", "src/app", "-w"], 72 | "problemMatcher": "$typescript:tsc-watch" 73 | }, 74 | "build-server": { 75 | "executable": "tsc", 76 | "args": ["-p", "src/server", "-w"], 77 | "problemMatcher": "$typescript:tsc-watch" 78 | }, 79 | "build-website": { 80 | "executable": "webpack", 81 | "problemMatcher": "$typescript:at-loader" 82 | } 83 | }, 84 | "groups": { 85 | "all": ["build-app", "build-server", "build-website"] 86 | }, 87 | "problemMatchers": { 88 | "$typescript:at-loader": { 89 | "owner": "typescript", 90 | "pattern": [ 91 | { 92 | "regexp": "^(ERROR) in \\[at-loader\\] (.+\\.ts):(\\d+|\\d+:\\d+)\\s*$", 93 | "severity": 1, 94 | "file": 2, 95 | "location": 3 96 | }, 97 | { 98 | "regexp": "^\\s+(TS\\d+): (.+)$", 99 | "code": 1, 100 | "message": 2 101 | } 102 | ], 103 | "background": { 104 | "beginsPattern": "^\\[at-loader\\] Checking started in a separate process\\.\\.\\.$" 105 | } 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | Checkout [config.ts](src/core/config.ts) for more options. 112 | 113 | ### VSCode Problem Matcher Support 114 | 115 | To make the aggregated problem matcher output work in VSCode, you'll need to define Biu as a task and configure proper problem matcher options in `tasks.json`: 116 | 117 | ```json 118 | { 119 | "version": "2.0.0", 120 | "tasks": [ 121 | { 122 | "label": "biu", 123 | "type": "shell", 124 | "command": "yarn", 125 | "args": ["biu"], 126 | "isBackground": true, 127 | "group": { 128 | "kind": "build", 129 | "isDefault": true 130 | }, 131 | "problemMatcher": { 132 | "name": "biu-typescript", 133 | "owner": "typescript", 134 | "applyTo": "closedDocuments", 135 | "fileLocation": "absolute", 136 | "pattern": { 137 | "regexp": "^\\[biu-problem:typescript:([^;]*);([^;]*);([^;]*);([^;]*);(.*?)\\]?$", 138 | "severity": 1, 139 | "file": 2, 140 | "location": 3, 141 | "code": 4, 142 | "message": 5 143 | }, 144 | "background": { 145 | "activeOnStart": false, 146 | "beginsPattern": "^\\[biu-problems:typescript:begin\\]$", 147 | "endsPattern": "^\\[biu-problems:typescript:end\\]$" 148 | } 149 | } 150 | } 151 | ] 152 | } 153 | ``` 154 | 155 | You can also install [Biu Problem Matchers] extension which contributes the following problem matchers: 156 | 157 | - `$biu-typescript` 158 | 159 | Thus you will be able to simplify your task configuration. 160 | 161 | ```json 162 | { 163 | "version": "2.0.0", 164 | "tasks": [ 165 | { 166 | "label": "biu", 167 | "type": "shell", 168 | "command": "yarn", 169 | "args": ["biu"], 170 | "isBackground": true, 171 | "group": { 172 | "kind": "build", 173 | "isDefault": true 174 | }, 175 | "problemMatcher": "$biu-typescript" 176 | } 177 | ] 178 | } 179 | ``` 180 | 181 | ### Support for package.json `scripts` 182 | 183 | If configuration is loaded from `package.json`, Biu will convert all keys in `scripts` section into `tasks`. And if you add `biuGroups`, `biu-groups` or `groups` under `biu` section into your `package.json`, Biu will load it as `groups`. 184 | 185 | ## License 186 | 187 | MIT License. 188 | -------------------------------------------------------------------------------- /src/client/components/menu/@group/@list/@list-item.tsx: -------------------------------------------------------------------------------- 1 | import {inject, observer} from '@makeflow/mobx-utils'; 2 | import classNames from 'classnames'; 3 | import {action} from 'mobx'; 4 | import React, {Component, ReactNode} from 'react'; 5 | 6 | import { 7 | Task, 8 | TaskService, 9 | TaskStatus, 10 | getTaskStatus, 11 | } from 'services/task-service'; 12 | import {styled} from 'theme'; 13 | 14 | import { 15 | ListItemCloseButton, 16 | ListItemRestartButton, 17 | ListItemStartButton, 18 | ListItemStopButton, 19 | } from './@list-item-buttons'; 20 | 21 | const Wrapper = styled.div` 22 | border-radius: 4px; 23 | border: 1px solid ${props => props.theme.border.light}; 24 | padding: 8px 10px 8px 14px; 25 | position: relative; 26 | display: flex; 27 | justify-content: space-between; 28 | transition: all 0.3s; 29 | cursor: pointer; 30 | background-color: ${props => props.theme.light}; 31 | 32 | &:hover { 33 | box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6), 34 | 0 2px 4px 0 rgba(232, 237, 250, 0.5); 35 | } 36 | 37 | &::before { 38 | position: absolute; 39 | content: ''; 40 | width: 3px; 41 | left: 0; 42 | top: 0; 43 | bottom: 0; 44 | border-top-left-radius: 4px; 45 | border-bottom-left-radius: 4px; 46 | transition: all 0.3s; 47 | } 48 | 49 | &.status-ready { 50 | opacity: 0.5; 51 | 52 | &:hover { 53 | opacity: 0.8; 54 | } 55 | } 56 | 57 | &.status-running::before { 58 | background-color: ${props => props.theme.bar.green}; 59 | } 60 | 61 | &.status-waiting::before { 62 | background-color: ${props => props.theme.bar.yellow}; 63 | } 64 | 65 | &.status-stopped::before { 66 | background-color: ${props => props.theme.bar.gray}; 67 | } 68 | `; 69 | 70 | const ItemTitleArea = styled.div``; 71 | 72 | const Title = styled.div` 73 | color: ${props => props.theme.text.primary}; 74 | font-size: 14px; 75 | margin-bottom: 4px; 76 | `; 77 | 78 | const SubTitle = styled.div` 79 | color: ${props => props.theme.text.secondary}; 80 | font-size: 12px; 81 | white-space: nowrap; 82 | overflow: hidden; 83 | text-overflow: ellipsis; 84 | max-width: 200px; 85 | `; 86 | 87 | const ItemOperationArea = styled.div` 88 | font-size: 11px; 89 | `; 90 | 91 | export interface ListItemProps { 92 | className?: string; 93 | task: Task; 94 | } 95 | 96 | @observer 97 | export class ListItem extends Component { 98 | @inject 99 | taskService!: TaskService; 100 | 101 | render(): ReactNode { 102 | let {className, task} = this.props; 103 | 104 | let {status} = task; 105 | 106 | let statusText = getTaskStatus(task); 107 | 108 | return ( 109 | 119 | 120 | {task.name} 121 | {statusText} 122 | 123 | 124 | {status === TaskStatus.ready || status === TaskStatus.stopped ? ( 125 | 126 | ) : ( 127 | undefined 128 | )} 129 | {status === TaskStatus.running ? ( 130 | 131 | ) : ( 132 | undefined 133 | )} 134 | {status === TaskStatus.running || status === TaskStatus.restarting ? ( 135 | 136 | ) : ( 137 | undefined 138 | )} 139 | {status !== TaskStatus.ready ? ( 140 | 141 | ) : ( 142 | undefined 143 | )} 144 | 145 | 146 | ); 147 | } 148 | 149 | onItemClick = (): void => { 150 | let {status} = this.props.task; 151 | 152 | switch (status) { 153 | case TaskStatus.ready: 154 | case TaskStatus.stopped: 155 | this.onStartButtonClick(); 156 | break; 157 | case TaskStatus.running: 158 | case TaskStatus.restarting: 159 | this.onStopButtonClick(); 160 | break; 161 | } 162 | }; 163 | 164 | @action 165 | onItemMouseEnter = (): void => { 166 | let {id} = this.props.task; 167 | 168 | if (id) { 169 | this.taskService.currentHoverTaskId = id; 170 | } 171 | }; 172 | 173 | @action 174 | onMouseLeave = (): void => { 175 | let {id} = this.props.task; 176 | 177 | if (id && this.taskService.currentHoverTaskId === id) { 178 | this.taskService.currentHoverTaskId = undefined; 179 | } 180 | }; 181 | 182 | onStartButtonClick = (): void => { 183 | let {task} = this.props; 184 | 185 | this.taskService.start(task); 186 | }; 187 | 188 | onRestartButtonClick = (): void => { 189 | let {task} = this.props; 190 | 191 | this.taskService.restart(task); 192 | }; 193 | 194 | onStopButtonClick = (): void => { 195 | let {task} = this.props; 196 | 197 | this.taskService.stop(task); 198 | }; 199 | 200 | onCloseButtonClick = (): void => { 201 | let {task} = this.props; 202 | 203 | this.taskService.close(task); 204 | }; 205 | 206 | static Wrapper = Wrapper; 207 | } 208 | 209 | function getStatusBarClassName(status: TaskStatus): string { 210 | switch (status) { 211 | case TaskStatus.ready: 212 | return 'status-ready'; 213 | case TaskStatus.running: 214 | return 'status-running'; 215 | case TaskStatus.stopped: 216 | return 'status-stopped'; 217 | case TaskStatus.restarting: 218 | case TaskStatus.stopping: 219 | return 'status-waiting'; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/cli/core/problem-matcher.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import * as Path from 'path'; 3 | 4 | import {ExpectedError} from 'clime'; 5 | import stripColor from 'strip-color'; 6 | 7 | import {ProblemMatcherBackgroundConfig, ProblemMatcherConfig} from './config'; 8 | 9 | export interface ProblemMatcherPatternBase { 10 | severity?: number | string; 11 | file: number | string; 12 | location: number | string; 13 | code?: number | string; 14 | message?: number | string; 15 | loop?: boolean; 16 | } 17 | 18 | export interface ProblemMatcherPattern extends ProblemMatcherPatternBase { 19 | regex: RegExp; 20 | } 21 | 22 | export interface Problem { 23 | severity?: string; 24 | file: string; 25 | location: string; 26 | code?: string; 27 | message?: string; 28 | } 29 | 30 | export class ProblemMatcher extends EventEmitter { 31 | patternIndex!: number; 32 | pendingOutput!: string; 33 | active!: boolean; 34 | problems!: Problem[]; 35 | 36 | patterns: ProblemMatcherPattern[]; 37 | background: ProblemMatcherBackgroundConfig | undefined; 38 | 39 | owner: string; 40 | beginsRegex: RegExp | undefined; 41 | endsRegex: RegExp | undefined; 42 | 43 | private problemsUpdateEventTimer: NodeJS.Timer | undefined; 44 | private loopedPattern: ProblemMatcherPattern | undefined; 45 | private activeMatch!: Partial; 46 | 47 | constructor(config: ProblemMatcherConfig, public cwd: string) { 48 | super(); 49 | 50 | let patternConfigs = Array.isArray(config.pattern) 51 | ? config.pattern 52 | : [config.pattern]; 53 | 54 | this.patterns = patternConfigs.map(config => { 55 | return { 56 | regex: new RegExp(config.regexp), 57 | ...config, 58 | }; 59 | }); 60 | 61 | this.owner = config.owner; 62 | 63 | let background = (this.background = config.background); 64 | 65 | if (background) { 66 | if (background.beginsPattern) { 67 | this.beginsRegex = new RegExp(background.beginsPattern); 68 | } 69 | 70 | if (background.endsPattern) { 71 | this.endsRegex = new RegExp(background.endsPattern); 72 | } 73 | } 74 | 75 | this.reset(); 76 | } 77 | 78 | reset(): void { 79 | clearTimeout(this.problemsUpdateEventTimer!); 80 | 81 | this.problems = []; 82 | this.pendingOutput = ''; 83 | this.patternIndex = 0; 84 | 85 | let background = this.background; 86 | this.active = 87 | !background || !background.beginsPattern || background.activeOnStart; 88 | } 89 | 90 | match(line: string): void { 91 | if (this.beginsRegex && this.endsRegex) { 92 | if (this.active) { 93 | if (this.endsRegex.test(line)) { 94 | this.active = false; 95 | this.emit('problems-update'); 96 | } 97 | } else if (this.beginsRegex.test(line)) { 98 | this.active = true; 99 | this.problems = []; 100 | } 101 | 102 | if (!this.active) { 103 | return; 104 | } 105 | } else if (this.beginsRegex) { 106 | if (this.beginsRegex.test(line)) { 107 | this.active = true; 108 | this.problems = []; 109 | this.scheduleProblemsUpdateEvent(); 110 | } 111 | 112 | if (!this.active) { 113 | return; 114 | } 115 | } else if (this.endsRegex) { 116 | if (this.endsRegex.test(line)) { 117 | this.emit('problems-update'); 118 | } 119 | } else if (this.background) { 120 | throw new ExpectedError( 121 | 'At least one of "beginsPattern" and "endsPattern" is required', 122 | ); 123 | } 124 | 125 | let pattern = this.loopedPattern!; 126 | let groups: RegExpExecArray | undefined | null; 127 | 128 | if (pattern) { 129 | groups = pattern.regex.exec(line); 130 | 131 | if (!groups) { 132 | this.loopedPattern = undefined; 133 | this.patternIndex = 0; 134 | } 135 | } 136 | 137 | if (!groups) { 138 | if (this.patternIndex === 0) { 139 | this.activeMatch = {}; 140 | } 141 | 142 | pattern = this.patterns[this.patternIndex]; 143 | groups = pattern.regex.exec(line); 144 | 145 | if (!groups && this.patternIndex !== 0) { 146 | this.patternIndex = 0; 147 | pattern = this.patterns[this.patternIndex]; 148 | groups = pattern.regex.exec(line); 149 | } 150 | 151 | if (groups) { 152 | this.patternIndex++; 153 | 154 | if (this.patternIndex >= this.patterns.length) { 155 | this.patternIndex = 0; 156 | } 157 | } 158 | } 159 | 160 | if (!groups) { 161 | return; 162 | } 163 | 164 | if ( 165 | this.loopedPattern !== pattern && 166 | this.patternIndex === 0 && 167 | pattern.loop 168 | ) { 169 | this.loopedPattern = pattern; 170 | } 171 | 172 | let activeMatch = this.activeMatch; 173 | 174 | let severity = resolveCapture(groups, pattern.severity); 175 | 176 | if (severity !== undefined) { 177 | activeMatch.severity = severity; 178 | } 179 | 180 | let file = resolveCapture(groups, pattern.file); 181 | 182 | if (file !== undefined) { 183 | activeMatch.file = Path.resolve(this.cwd, file); 184 | } 185 | 186 | let location = resolveCapture(groups, pattern.location); 187 | 188 | if (location !== undefined) { 189 | activeMatch.location = location; 190 | } 191 | 192 | let code = resolveCapture(groups, pattern.code); 193 | 194 | if (code !== undefined) { 195 | activeMatch.code = code; 196 | } 197 | 198 | let message = resolveCapture(groups, pattern.message); 199 | 200 | if (message !== undefined) { 201 | activeMatch.message = message; 202 | } 203 | 204 | if (this.patternIndex === 0) { 205 | this.pushProblem(!!this.background && !this.endsRegex); 206 | } 207 | } 208 | 209 | push(chunk: Buffer): void { 210 | this.pendingOutput += chunk.toString(); 211 | 212 | let lineRegex = /(.*)([\r\n\u2028\u2029]?)/g; 213 | 214 | while (true) { 215 | let groups = lineRegex.exec(this.pendingOutput)!; 216 | 217 | let line = groups[1]; 218 | let lineTerminator = groups[2]; 219 | 220 | if (lineTerminator) { 221 | this.match(stripColor(line)); 222 | } else { 223 | this.pendingOutput = line; 224 | break; 225 | } 226 | } 227 | } 228 | 229 | private pushProblem(schedule: boolean): void { 230 | this.problems.push({...(this.activeMatch as Problem)}); 231 | 232 | if (schedule) { 233 | this.scheduleProblemsUpdateEvent(); 234 | } 235 | } 236 | 237 | private scheduleProblemsUpdateEvent(): void { 238 | clearTimeout(this.problemsUpdateEventTimer!); 239 | 240 | this.problemsUpdateEventTimer = setTimeout(() => { 241 | this.emit('problems-update'); 242 | }, 200); 243 | } 244 | } 245 | 246 | function resolveCapture( 247 | groups: RegExpExecArray, 248 | index: number | string | undefined, 249 | ): string | undefined { 250 | if (index === undefined) { 251 | return undefined; 252 | } 253 | 254 | if (typeof index === 'number') { 255 | return groups[index]; 256 | } 257 | 258 | return index.replace(/\$(&|\d+)/g, (text, indexStr: string) => { 259 | let str = groups[indexStr === '&' ? 0 : Number(indexStr)] as 260 | | string 261 | | undefined; 262 | return typeof str === 'string' ? str : text; 263 | }); 264 | } 265 | -------------------------------------------------------------------------------- /src/client/components/window-manager/@manager-style.tsx: -------------------------------------------------------------------------------- 1 | import {styled} from 'theme'; 2 | 3 | export const ManagerStyle = styled.div` 4 | .mosaic { 5 | height: 100%; 6 | width: 100%; 7 | } 8 | .mosaic, 9 | .mosaic > * { 10 | box-sizing: border-box; 11 | } 12 | .mosaic .mosaic-zero-state { 13 | width: auto; 14 | height: auto; 15 | max-width: none; 16 | z-index: 1; 17 | } 18 | .mosaic .mosaic-zero-state .pt-non-ideal-state-icon .pt-icon { 19 | font-size: 120px; 20 | } 21 | .mosaic-root { 22 | position: absolute; 23 | top: 3px; 24 | right: 3px; 25 | bottom: 3px; 26 | left: 3px; 27 | } 28 | .mosaic-split { 29 | position: absolute; 30 | z-index: 1; 31 | transition: all 0.1s; 32 | } 33 | .mosaic-split:hover { 34 | transition: all 0.1s; 35 | 36 | background: rgba(0, 0, 0, 0.2); 37 | } 38 | .mosaic-split .mosaic-split-line { 39 | position: absolute; 40 | } 41 | .mosaic-split.-row { 42 | margin-left: -2px; 43 | width: 4px; 44 | cursor: ew-resize; 45 | } 46 | .mosaic-split.-row .mosaic-split-line { 47 | top: 0; 48 | bottom: 0; 49 | left: 3px; 50 | right: 3px; 51 | } 52 | .mosaic-split.-column { 53 | margin-top: -2px; 54 | height: 4px; 55 | cursor: ns-resize; 56 | } 57 | .mosaic-split.-column .mosaic-split-line { 58 | top: 3px; 59 | bottom: 3px; 60 | left: 0; 61 | right: 0; 62 | } 63 | .mosaic-tile { 64 | position: absolute; 65 | margin: 10px; 66 | } 67 | .mosaic-tile > * { 68 | height: 100%; 69 | width: 100%; 70 | } 71 | .mosaic-drop-target { 72 | position: relative; 73 | } 74 | .mosaic-drop-target.drop-target-hover .drop-target-container { 75 | display: block; 76 | } 77 | .mosaic-drop-target.mosaic > .drop-target-container .drop-target.left { 78 | right: calc(100% - 10px); 79 | } 80 | .mosaic-drop-target.mosaic > .drop-target-container .drop-target.right { 81 | left: calc(100% - 10px); 82 | } 83 | .mosaic-drop-target.mosaic > .drop-target-container .drop-target.bottom { 84 | top: calc(100% - 10px); 85 | } 86 | .mosaic-drop-target.mosaic > .drop-target-container .drop-target.top { 87 | bottom: calc(100% - 10px); 88 | } 89 | .mosaic-drop-target .drop-target-container { 90 | position: absolute; 91 | top: 0; 92 | right: 0; 93 | bottom: 0; 94 | left: 0; 95 | display: none; 96 | } 97 | .mosaic-drop-target .drop-target-container.-dragging { 98 | display: block; 99 | } 100 | .mosaic-drop-target .drop-target-container .drop-target { 101 | position: absolute; 102 | top: 0; 103 | right: 0; 104 | bottom: 0; 105 | left: 0; 106 | background: rgba(0, 0, 0, 0.2); 107 | border: 2px solid rgba(0, 0, 0, 0.5); 108 | opacity: 0; 109 | transition: all 0.3s; 110 | z-index: 8; 111 | border-radius: 4px; 112 | } 113 | .mosaic-drop-target .drop-target-container .drop-target.left { 114 | right: calc(100% - 30%); 115 | } 116 | .mosaic-drop-target .drop-target-container .drop-target.right { 117 | left: calc(100% - 30%); 118 | } 119 | .mosaic-drop-target .drop-target-container .drop-target.bottom { 120 | top: calc(100% - 30%); 121 | } 122 | .mosaic-drop-target .drop-target-container .drop-target.top { 123 | bottom: calc(100% - 30%); 124 | } 125 | .mosaic-drop-target .drop-target-container .drop-target.drop-target-hover { 126 | opacity: 1; 127 | } 128 | .mosaic-drop-target 129 | .drop-target-container 130 | .drop-target.drop-target-hover.left { 131 | right: calc(100% - 50%); 132 | } 133 | .mosaic-drop-target 134 | .drop-target-container 135 | .drop-target.drop-target-hover.right { 136 | left: calc(100% - 50%); 137 | } 138 | .mosaic-drop-target 139 | .drop-target-container 140 | .drop-target.drop-target-hover.bottom { 141 | top: calc(100% - 50%); 142 | } 143 | .mosaic-drop-target 144 | .drop-target-container 145 | .drop-target.drop-target-hover.top { 146 | bottom: calc(100% - 50%); 147 | } 148 | 149 | /* Window style */ 150 | 151 | .mosaic-window, 152 | .mosaic-preview { 153 | position: relative; 154 | display: flex; 155 | flex-direction: column; 156 | overflow: hidden; 157 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.08); 158 | border-radius: 4px; 159 | } 160 | .mosaic-window .mosaic-window-toolbar, 161 | .mosaic-preview .mosaic-window-toolbar { 162 | z-index: 4; 163 | display: flex; 164 | justify-content: space-between; 165 | align-items: center; 166 | flex-shrink: 0; 167 | height: 35px; 168 | background: white; 169 | } 170 | .mosaic-window .mosaic-window-toolbar.draggable, 171 | .mosaic-preview .mosaic-window-toolbar.draggable { 172 | cursor: move; 173 | } 174 | .mosaic-window .mosaic-window-title, 175 | .mosaic-preview .mosaic-window-title { 176 | padding-left: 15px; 177 | flex: 1; 178 | text-overflow: ellipsis; 179 | white-space: nowrap; 180 | overflow: hidden; 181 | min-height: 16px; 182 | font-size: 13px; 183 | color: ${props => props.theme.text.navPlaceholder}; 184 | } 185 | .mosaic-window .mosaic-window-controls, 186 | .mosaic-preview .mosaic-window-controls { 187 | display: flex; 188 | height: 100%; 189 | } 190 | .mosaic-window .mosaic-window-controls .separator, 191 | .mosaic-preview .mosaic-window-controls .separator { 192 | height: 20px; 193 | border-left: 1px solid black; 194 | margin: 5px 4px; 195 | } 196 | .mosaic-window .mosaic-window-body, 197 | .mosaic-preview .mosaic-window-body { 198 | flex: 1; 199 | height: 0; 200 | background: white; 201 | z-index: 6; 202 | overflow: hidden; 203 | } 204 | .mosaic-window .mosaic-window-additional-actions-bar, 205 | .mosaic-preview .mosaic-window-additional-actions-bar { 206 | position: absolute; 207 | top: 30px; 208 | right: 0; 209 | bottom: initial; 210 | left: 0; 211 | height: 0; 212 | overflow: hidden; 213 | background: white; 214 | justify-content: flex-end; 215 | display: flex; 216 | z-index: 3; 217 | } 218 | .mosaic-window .mosaic-window-additional-actions-bar .pt-button, 219 | .mosaic-preview .mosaic-window-additional-actions-bar .pt-button { 220 | margin: 0; 221 | } 222 | .mosaic-window .mosaic-window-additional-actions-bar .pt-button:after, 223 | .mosaic-preview .mosaic-window-additional-actions-bar .pt-button:after { 224 | display: none; 225 | } 226 | .mosaic-window .mosaic-window-body-overlay, 227 | .mosaic-preview .mosaic-window-body-overlay { 228 | position: absolute; 229 | top: 0; 230 | right: 0; 231 | bottom: 0; 232 | left: 0; 233 | opacity: 0; 234 | background: white; 235 | display: none; 236 | z-index: 2; 237 | } 238 | .mosaic-window.additional-controls-open .mosaic-window-additional-actions-bar, 239 | .mosaic-preview.additional-controls-open 240 | .mosaic-window-additional-actions-bar { 241 | height: 30px; 242 | } 243 | .mosaic-window.additional-controls-open .mosaic-window-body-overlay, 244 | .mosaic-preview.additional-controls-open .mosaic-window-body-overlay { 245 | display: block; 246 | } 247 | .mosaic-window .mosaic-preview, 248 | .mosaic-preview .mosaic-preview { 249 | height: 100%; 250 | width: 100%; 251 | position: absolute; 252 | z-index: 0; 253 | max-height: 400px; 254 | } 255 | .mosaic-window .mosaic-preview .mosaic-window-body, 256 | .mosaic-preview .mosaic-preview .mosaic-window-body { 257 | display: flex; 258 | flex-direction: column; 259 | align-items: center; 260 | justify-content: center; 261 | } 262 | .mosaic-window .mosaic-preview h4, 263 | .mosaic-preview .mosaic-preview h4 { 264 | margin-bottom: 10px; 265 | } 266 | .mosaic-window .mosaic-preview .pt-icon, 267 | .mosaic-preview .mosaic-preview .pt-icon { 268 | font-size: 72px; 269 | } 270 | 271 | .mosaic-window-controls.pt-button-group { 272 | padding-right: 10px; 273 | } 274 | 275 | .mosaic-default-control.pt-button { 276 | width: 12px; 277 | height: 12px; 278 | border-radius: 6px; 279 | border: 0px; 280 | padding: 5px; 281 | margin-top: 11px; 282 | margin-left: 7px; 283 | outline: none; 284 | transition: all 0.3s; 285 | position: relative; 286 | cursor: pointer; 287 | 288 | &::before { 289 | display: inline-block; 290 | font: normal normal normal 14px/1 FontAwesome; 291 | font-size: inherit; 292 | text-rendering: auto; 293 | -webkit-font-smoothing: antialiased; 294 | opacity: 0; 295 | transition: all 0.3s; 296 | color: #000; 297 | font-size: 9px; 298 | position: absolute; 299 | left: 1px; 300 | top: 0; 301 | right: 0; 302 | bottom: 0; 303 | text-align: center; 304 | line-height: 12.8px; 305 | } 306 | 307 | &:hover::before { 308 | opacity: 0.35; 309 | } 310 | 311 | &.pt-icon-cross { 312 | background-color: ${props => props.theme.dangerAccent()}; 313 | &::before { 314 | content: '\f00d'; 315 | } 316 | } 317 | 318 | &.pt-icon-maximize { 319 | background-color: ${props => props.theme.greenAccent()}; 320 | 321 | &::before { 322 | content: '\f067'; 323 | } 324 | } 325 | } 326 | `; 327 | -------------------------------------------------------------------------------- /src/cli/core/server.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {Server as HttpServer, createServer} from 'http'; 3 | import * as Path from 'path'; 4 | 5 | import AnsiConverter from 'ansi-to-html'; 6 | import express from 'express'; 7 | import socketIO from 'socket.io'; 8 | import * as v from 'villa'; 9 | 10 | import {builtInProblemMatcherDict} from '../problem-matchers'; 11 | 12 | import {Config, ProblemMatcherConfig} from './config'; 13 | import {Task, TaskExitEventData, TaskProblemsUpdateEventData} from './task'; 14 | 15 | export interface TaskCreationCommand { 16 | names: string[]; 17 | closeAll: boolean; 18 | } 19 | 20 | export interface TaskOperationCommand { 21 | id: string; 22 | } 23 | 24 | interface TaskInfo { 25 | converter: AnsiConverter; 26 | lastData: string; 27 | } 28 | 29 | interface TaskInfoDict { 30 | stdout: TaskInfo; 31 | stderr: TaskInfo; 32 | } 33 | 34 | export class Server extends EventEmitter { 35 | server: HttpServer; 36 | app: express.Express; 37 | io: SocketIO.Server; 38 | room: SocketIO.Namespace; 39 | 40 | lastTaskId = 0; 41 | taskMap = new Map(); 42 | taskInfoDictMap = new Map(); 43 | 44 | constructor(public config: Config, public configDir: string) { 45 | super(); 46 | 47 | this.app = express(); 48 | this.server = createServer(this.app); 49 | this.io = socketIO(this.server); 50 | this.room = this.io.in('biu'); 51 | 52 | this.setup(); 53 | } 54 | 55 | async listen(port: number): Promise { 56 | await v.call(this.server.listen.bind(this.server), port); 57 | } 58 | 59 | async create(taskNames: string[], closeAll: boolean): Promise { 60 | if (closeAll) { 61 | await this.closeAll(); 62 | } 63 | 64 | let problemMatcherDict: Dictionary = { 65 | ...builtInProblemMatcherDict, 66 | ...this.config.problemMatchers, 67 | }; 68 | 69 | for (let name of taskNames) { 70 | let id = (++this.lastTaskId).toString(); 71 | 72 | let options = this.config.tasks[name]; 73 | 74 | let problemMatcherConfig = 75 | typeof options.problemMatcher === 'string' 76 | ? problemMatcherDict[options.problemMatcher] 77 | : options.problemMatcher; 78 | 79 | let task = new Task(name, options.executable, options.args || [], { 80 | cwd: options.cwd 81 | ? Path.resolve(this.configDir, options.cwd) 82 | : process.cwd(), 83 | stdout: !!options.stdout, 84 | stderr: !!options.stderr, 85 | problemMatcher: problemMatcherConfig, 86 | watch: options.watch, 87 | autoClose: !!options.autoClose, 88 | }); 89 | 90 | this.taskInfoDictMap.set(id, { 91 | stdout: { 92 | converter: new AnsiConverter({stream: true}), 93 | lastData: '', 94 | }, 95 | stderr: { 96 | converter: new AnsiConverter({stream: true}), 97 | lastData: '', 98 | }, 99 | }); 100 | 101 | this.room.emit('create', { 102 | id, 103 | name, 104 | line: task.line, 105 | }); 106 | 107 | this.initializeTask(id, task); 108 | 109 | task.start(); 110 | 111 | this.taskMap.set(id, task); 112 | } 113 | } 114 | 115 | async startAll(): Promise { 116 | await v.parallel(Array.from(this.taskMap), ([id]) => this.start(id)); 117 | } 118 | 119 | async restartAll(): Promise { 120 | await v.parallel(Array.from(this.taskMap), ([id]) => this.restart(id)); 121 | } 122 | 123 | async stopAll(): Promise { 124 | await v.parallel(Array.from(this.taskMap), ([id]) => this.stop(id)); 125 | } 126 | 127 | async closeAll(): Promise { 128 | await v.parallel(Array.from(this.taskMap), ([id]) => this.close(id)); 129 | } 130 | 131 | async start(id: string): Promise { 132 | let task = this.taskMap.get(id); 133 | 134 | if (!task) { 135 | return; 136 | } 137 | 138 | task.start(); 139 | } 140 | 141 | async stop(id: string): Promise { 142 | let task = this.taskMap.get(id); 143 | 144 | if (!task) { 145 | return; 146 | } 147 | 148 | task.stop(); 149 | } 150 | 151 | async restart(id: string): Promise { 152 | let task = this.taskMap.get(id); 153 | 154 | if (!task) { 155 | return; 156 | } 157 | 158 | await task.restart(); 159 | } 160 | 161 | async close(id: string): Promise { 162 | let task = this.taskMap.get(id); 163 | 164 | if (!task) { 165 | return; 166 | } 167 | 168 | await task.stopWait(); 169 | 170 | this.taskMap.delete(id); 171 | 172 | this.room.emit('close', {id}); 173 | } 174 | 175 | private setup(): void { 176 | let clientBuildPath = Path.join(__dirname, '../../client'); 177 | 178 | this.app.use(express.static(clientBuildPath)); 179 | this.io.on('connection', socket => this.initializeConnection(socket)); 180 | } 181 | 182 | private outputProblems(owner: string): void { 183 | let lineSet = new Set(); 184 | 185 | for (let [, {problemMatcherMap}] of this.taskMap) { 186 | let problemMatcher = problemMatcherMap && problemMatcherMap.get(owner); 187 | 188 | if (!problemMatcher) { 189 | continue; 190 | } 191 | 192 | for (let problem of problemMatcher.problems) { 193 | lineSet.add( 194 | [ 195 | problem.severity, 196 | problem.file, 197 | problem.location, 198 | problem.code, 199 | problem.message, 200 | ].join(';'), 201 | ); 202 | } 203 | } 204 | 205 | process.stdout.write(`[biu-problems:${owner}:begin]\n`); 206 | 207 | for (let line of lineSet) { 208 | process.stdout.write(`[biu-problem:${owner}:${line}]\n`); 209 | } 210 | 211 | process.stdout.write(`[biu-problems:${owner}:end]\n`); 212 | } 213 | 214 | private initializeTask(id: string, task: Task): void { 215 | task.on('start', () => this.room.emit('start', {id})); 216 | task.on('stop', () => this.room.emit('stop', {id})); 217 | task.on('restarting-on-change', () => 218 | this.room.emit('restarting-on-change', {id}), 219 | ); 220 | 221 | task.on('error', (error: any) => { 222 | error = 223 | error instanceof Error ? error.stack || error.message : `${error}`; 224 | 225 | this.room.emit('error', { 226 | id, 227 | error: encodeOutput(error), 228 | }); 229 | }); 230 | 231 | task.on('exit', async (data: TaskExitEventData) => { 232 | this.room.emit('exit', {id, code: data.code}); 233 | 234 | if (data.close) { 235 | await this.close(id); 236 | } 237 | }); 238 | 239 | task.on('stdout', (data: Buffer) => { 240 | this.onStdData(id, 'stdout', data); 241 | }); 242 | 243 | task.on('stderr', (data: Buffer) => { 244 | this.onStdData(id, 'stderr', data); 245 | }); 246 | 247 | task.on('problems-update', (data: TaskProblemsUpdateEventData) => { 248 | this.outputProblems(data.owner); 249 | }); 250 | } 251 | 252 | private onStdData(id: string, event: keyof TaskInfoDict, data: Buffer): void { 253 | let html = ''; 254 | let taskInfo = this.taskInfoDictMap.get(id)![event]; 255 | 256 | let lines = data.toString().split('\n'); 257 | let dataCompleted = lines[lines.length - 1] === ''; 258 | 259 | lines[0] = taskInfo.lastData + lines[0]; 260 | 261 | taskInfo.lastData = lines[lines.length - 1]; 262 | 263 | if (dataCompleted) { 264 | lines = lines.slice(0, lines.length - 1); 265 | } 266 | 267 | for (let [index, line] of lines.entries()) { 268 | // Problem: `c` and `G` dramatically come from this line 269 | let lineHtml = taskInfo.converter.toHtml(line); 270 | 271 | if (index === lines.length - 1 && !dataCompleted) { 272 | html += `

${lineHtml}
`; 273 | } else { 274 | html += `
${lineHtml}
`; 275 | } 276 | } 277 | 278 | this.room.emit(event, { 279 | id, 280 | html, 281 | }); 282 | } 283 | 284 | private initializeConnection(socket: SocketIO.Socket): void { 285 | socket.join('biu'); 286 | 287 | socket.on('create', async (data: TaskCreationCommand) => { 288 | await this.create(data.names, data.closeAll); 289 | }); 290 | 291 | socket.on('close', async (data: TaskOperationCommand) => { 292 | await this.close(data.id); 293 | }); 294 | 295 | socket.on('restart', async (data: TaskOperationCommand) => { 296 | await this.restart(data.id); 297 | }); 298 | 299 | socket.on('start', async (data: TaskOperationCommand) => { 300 | await this.start(data.id); 301 | }); 302 | 303 | socket.on('stop', async (data: TaskOperationCommand) => { 304 | await this.stop(data.id); 305 | }); 306 | 307 | socket.on('start-all', async () => { 308 | await this.startAll(); 309 | }); 310 | 311 | socket.on('stop-all', async () => { 312 | await this.stopAll(); 313 | }); 314 | 315 | socket.on('restart-all', async () => { 316 | await this.restartAll(); 317 | }); 318 | 319 | socket.on('close-all', async () => { 320 | await this.closeAll(); 321 | }); 322 | 323 | socket.emit('initialize', { 324 | taskNames: Object.keys(this.config.tasks), 325 | taskGroups: this.config.groups, 326 | createdTasks: Array.from(this.taskMap).map(([id, task]) => { 327 | return { 328 | id, 329 | name: task.name, 330 | line: task.line, 331 | running: task.running, 332 | }; 333 | }), 334 | }); 335 | } 336 | } 337 | 338 | function encodeOutput(text: string): string { 339 | return text 340 | .replace(/&/g, '&') 341 | .replace(//g, '>'); 343 | } 344 | -------------------------------------------------------------------------------- /src/client/assets/zero-state.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 25 | 26 | 95 | 96 | 101 | 102 | 111 | 112 | 123 | 124 | 133 | 134 | 140 | 141 | 152 | 153 | 160 | 161 | 232 | 233 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /src/client/services/task-service.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {action, observable} from 'mobx'; 3 | import { 4 | Corner, 5 | MosaicBranch, 6 | MosaicDirection, 7 | MosaicNode, 8 | MosaicParent, 9 | createBalancedTreeFromLeaves, 10 | createRemoveUpdate, 11 | getLeaves, 12 | getNodeAtPath, 13 | getOtherDirection, 14 | getPathToCorner, 15 | updateTree, 16 | } from 'react-mosaic-component'; 17 | 18 | import {deepCopy} from 'utils/lang'; 19 | import {appendOutput, outputError, outputInfo} from 'utils/output'; 20 | import {getStorageObject, setStorageObject} from 'utils/storage'; 21 | 22 | import {SocketIOService} from './socket-io-service'; 23 | 24 | export type TaskName = string; 25 | export type TaskId = string; 26 | 27 | export enum TaskStatus { 28 | ready, 29 | running, 30 | stopped, 31 | stopping, 32 | restarting, 33 | } 34 | 35 | export interface Task { 36 | id?: TaskId; 37 | name: string; 38 | line?: string; 39 | running: boolean; 40 | status: TaskStatus; 41 | output: string; 42 | } 43 | 44 | export interface CreatedTask extends Task { 45 | id: TaskId; 46 | } 47 | 48 | export interface TaskRef { 49 | id: TaskId; 50 | } 51 | 52 | export interface TaskGroupDict { 53 | [key: string]: string[]; 54 | } 55 | 56 | export interface TaskDict { 57 | [key: string]: Task; 58 | } 59 | 60 | export interface InitializeData { 61 | createdTasks: CreatedTask[]; 62 | taskGroups: TaskGroupDict; 63 | taskNames: string[]; 64 | } 65 | 66 | export interface ErrorData { 67 | id: TaskId; 68 | error: string; 69 | } 70 | 71 | export interface ExitData { 72 | id: TaskId; 73 | code?: string; 74 | } 75 | 76 | export interface StdOutData { 77 | id: TaskId; 78 | html: string; 79 | } 80 | 81 | export interface StdErrData { 82 | id: TaskId; 83 | html: string; 84 | } 85 | 86 | export class TaskService { 87 | @observable 88 | connected = false; 89 | 90 | @observable 91 | taskGroups: TaskGroupDict = {}; 92 | 93 | @observable 94 | tasks: TaskDict = {}; 95 | 96 | @observable 97 | createdTaskMap = new Map(); 98 | 99 | @observable 100 | currentNode: MosaicNode | null = null; 101 | 102 | @observable 103 | currentHoverTaskId: TaskId | undefined; 104 | 105 | lastTaskList: TaskName[] = []; 106 | 107 | constructor(private socketIOService: SocketIOService) { 108 | this.socketIOService.on('connect', this.onConnect); 109 | this.socketIOService.on('disconnect', this.onDisconnect); 110 | this.socketIOService.on('initialize', this.onInitialize); 111 | this.socketIOService.on('create', this.onCreate); 112 | this.socketIOService.on('close', this.onClose); 113 | this.socketIOService.on('start', this.onStart); 114 | this.socketIOService.on('stop', this.onStop); 115 | this.socketIOService.on('restarting-on-change', this.onRestartOnChange); 116 | this.socketIOService.on('error', this.onError); 117 | this.socketIOService.on('exit', this.onExit); 118 | this.socketIOService.on('stdout', this.onStdOut); 119 | this.socketIOService.on('stderr', this.onStdErr); 120 | } 121 | 122 | isCreated(task: Task): task is CreatedTask { 123 | let {id} = task; 124 | 125 | return typeof id !== 'undefined' && this.createdTaskMap.has(id); 126 | } 127 | 128 | start(task: Task): void { 129 | if (!this.isCreated(task)) { 130 | let {name} = task; 131 | 132 | this.socketIOService.emit('create', {names: [name]}); 133 | } else if (!task.running) { 134 | let {id} = task; 135 | 136 | this.socketIOService.emit('start', {id}); 137 | } 138 | } 139 | 140 | startGroup(groupName: string): void { 141 | let tasks = this.filterTasksInGroup(Object.values(this.tasks), groupName); 142 | 143 | for (let task of tasks) { 144 | this.start(task); 145 | } 146 | } 147 | 148 | startAll(): void { 149 | for (let task of this.createdTaskMap.values()) { 150 | this.start(task); 151 | } 152 | } 153 | 154 | @action 155 | restart(task: Task): void { 156 | if (!this.isCreated(task)) { 157 | return; 158 | } 159 | 160 | let {id} = task; 161 | 162 | task.status = TaskStatus.restarting; 163 | 164 | this.socketIOService.emit('restart', {id}); 165 | } 166 | 167 | restartGroup(groupName: string): void { 168 | let tasks = this.filterTasksInGroup( 169 | Array.from(this.createdTaskMap.values()), 170 | groupName, 171 | ); 172 | 173 | for (let task of tasks) { 174 | this.restart(task); 175 | } 176 | } 177 | 178 | restartAll(): void { 179 | for (let task of this.createdTaskMap.values()) { 180 | this.restart(task); 181 | } 182 | } 183 | 184 | @action 185 | stop(task: Task): void { 186 | if ( 187 | !this.isCreated(task) || 188 | (task.status !== TaskStatus.running && 189 | task.status !== TaskStatus.restarting) 190 | ) { 191 | return; 192 | } 193 | 194 | let {id} = task; 195 | 196 | task.status = TaskStatus.stopping; 197 | 198 | this.socketIOService.emit('stop', {id}); 199 | } 200 | 201 | stopGroup(groupName: string): void { 202 | let tasks = this.filterTasksInGroup( 203 | Array.from(this.createdTaskMap.values()), 204 | groupName, 205 | ); 206 | 207 | for (let task of tasks) { 208 | this.stop(task); 209 | } 210 | } 211 | 212 | stopAll(): void { 213 | for (let task of this.createdTaskMap.values()) { 214 | this.stop(task); 215 | } 216 | } 217 | 218 | @action 219 | close(task: Task): void { 220 | if (!this.isCreated(task)) { 221 | return; 222 | } 223 | 224 | let {id, running} = task; 225 | 226 | if (running) { 227 | task.status = TaskStatus.stopping; 228 | } 229 | 230 | this.socketIOService.emit('close', {id}); 231 | } 232 | 233 | closeGroup(groupName: string): void { 234 | let tasks = this.filterTasksInGroup( 235 | Array.from(this.createdTaskMap.values()), 236 | groupName, 237 | ); 238 | 239 | for (let task of tasks) { 240 | this.close(task); 241 | } 242 | } 243 | 244 | closeAll(): void { 245 | for (let task of this.createdTaskMap.values()) { 246 | this.close(task); 247 | } 248 | } 249 | 250 | @action 251 | autoArrangeWindows(): void { 252 | let leaves = getLeaves(this.currentNode); 253 | 254 | this.currentNode = createBalancedTreeFromLeaves(leaves); 255 | } 256 | 257 | restoreNode(node: MosaicNode | null): MosaicNode | null { 258 | if (node) { 259 | let { 260 | taskList, 261 | taskNameToIdMap, 262 | } = this.getTaskListAndTaskNameAndIdMapOutOfNode(node); 263 | 264 | try { 265 | let storedNode = getLayoutFromStorage(taskList, taskNameToIdMap); 266 | 267 | if (storedNode) { 268 | return storedNode; 269 | } 270 | } catch (error) { 271 | console.error(error); 272 | } 273 | } 274 | 275 | return node; 276 | } 277 | 278 | saveNodeLayout(node: MosaicNode | null): void { 279 | if (node) { 280 | let { 281 | taskList, 282 | taskIdToNameMap, 283 | } = this.getTaskListAndTaskNameAndIdMapOutOfNode(node); 284 | 285 | if ( 286 | !this.lastTaskList || 287 | getLayoutStorageKey(taskList) !== getLayoutStorageKey(this.lastTaskList) 288 | ) { 289 | this.lastTaskList = taskList; 290 | 291 | return; 292 | } else { 293 | this.lastTaskList = taskList; 294 | 295 | try { 296 | setLayoutToStorage(taskList, node, taskIdToNameMap); 297 | } catch (error) { 298 | console.error(error); 299 | } 300 | } 301 | } 302 | } 303 | 304 | private getTaskListAndTaskNameAndIdMapOutOfNode( 305 | node: MosaicNode, 306 | ): { 307 | taskList: string[]; 308 | taskNameToIdMap: Map; 309 | taskIdToNameMap: Map; 310 | } { 311 | let taskList: string[] = []; 312 | let taskNameToIdMap = new Map(); 313 | let taskIdToNameMap = new Map(); 314 | 315 | let leaves = getLeaves(node); 316 | 317 | for (let leave of leaves) { 318 | let task = this.createdTaskMap.get(leave); 319 | 320 | if (task) { 321 | taskList.push(task.name); 322 | taskNameToIdMap.set(task.name, leave); 323 | taskIdToNameMap.set(leave, task.name); 324 | } 325 | } 326 | 327 | taskList = taskList.sort(); 328 | 329 | return {taskList, taskNameToIdMap, taskIdToNameMap}; 330 | } 331 | 332 | private filterTasksInGroup(tasks: Task[], groupName: string): Task[] { 333 | let result: Task[] = []; 334 | 335 | if (!(groupName in this.taskGroups)) { 336 | return result; 337 | } 338 | 339 | let taskNamesInGroup = this.taskGroups[groupName]; 340 | 341 | for (let task of tasks) { 342 | let {name} = task; 343 | 344 | if (taskNamesInGroup.includes(name)) { 345 | result.push(task); 346 | } 347 | } 348 | 349 | return result; 350 | } 351 | 352 | private getCreatedTaskByTaskId(id: TaskId): CreatedTask | undefined { 353 | let task = this.createdTaskMap.get(id); 354 | 355 | if (!task) { 356 | return undefined; 357 | } 358 | 359 | let {name} = task; 360 | 361 | return this.tasks[name] as CreatedTask; 362 | } 363 | 364 | private freshCurrentNode(): void { 365 | let createdTaskIds = Array.from(this.createdTaskMap.keys()); 366 | let currentNodeIds = getLeaves(this.currentNode); 367 | 368 | let newTaskIds = _.difference(createdTaskIds, currentNodeIds); 369 | let removedTaskIds = _.difference(currentNodeIds, createdTaskIds); 370 | 371 | for (let taskId of removedTaskIds) { 372 | this.closeTaskWindow(taskId); 373 | } 374 | 375 | for (let newTaskId of newTaskIds) { 376 | this.addToBottomRight(newTaskId); 377 | } 378 | } 379 | 380 | @action 381 | private addToBottomRight = (taskId: TaskId): void => { 382 | let currentNode = this.currentNode; 383 | 384 | if (currentNode) { 385 | const path = getPathToCorner(currentNode, Corner.BOTTOM_RIGHT); 386 | const parent = getNodeAtPath( 387 | currentNode, 388 | _.dropRight(path), 389 | ) as MosaicParent; 390 | const destination = getNodeAtPath(currentNode, path) as MosaicNode< 391 | TaskId 392 | >; 393 | const direction: MosaicDirection = parent 394 | ? getOtherDirection(parent.direction) 395 | : 'row'; 396 | let first: MosaicNode; 397 | let second: MosaicNode; 398 | 399 | first = destination; 400 | second = taskId; 401 | 402 | this.currentNode = updateTree(currentNode, [ 403 | { 404 | path, 405 | spec: { 406 | $set: { 407 | direction, 408 | first, 409 | second, 410 | }, 411 | }, 412 | }, 413 | ]); 414 | } else { 415 | this.currentNode = taskId; 416 | } 417 | }; 418 | 419 | @action 420 | private closeTaskWindow(taskId: TaskId): void { 421 | if (this.currentNode) { 422 | let path = getPathByTaskIdInNode(this.currentNode, taskId); 423 | 424 | if (typeof path === 'object') { 425 | let update = createRemoveUpdate(this.currentNode, path); 426 | 427 | this.currentNode = updateTree(this.currentNode, [update]); 428 | } else if (path === 'clean') { 429 | this.currentNode = null; 430 | } 431 | } 432 | } 433 | 434 | @action 435 | private onConnect = (): void => { 436 | this.connected = true; 437 | }; 438 | 439 | @action 440 | private onDisconnect = (): void => { 441 | this.connected = false; 442 | }; 443 | 444 | @action 445 | private onInitialize = ({ 446 | createdTasks, 447 | taskGroups, 448 | taskNames, 449 | }: InitializeData): void => { 450 | this.taskGroups = {}; 451 | this.tasks = {}; 452 | this.createdTaskMap = new Map(); 453 | this.currentNode = null; 454 | this.currentHoverTaskId = undefined; 455 | 456 | if (taskGroups) { 457 | this.taskGroups = taskGroups; 458 | } 459 | 460 | if (taskNames) { 461 | for (let taskName of taskNames) { 462 | this.tasks[taskName] = { 463 | name: taskName, 464 | running: false, 465 | status: TaskStatus.ready, 466 | output: '', 467 | }; 468 | } 469 | } 470 | 471 | let createdLeaves: TaskId[] = []; 472 | 473 | for (let task of createdTasks) { 474 | let {id, name} = task; 475 | 476 | task.status = task.running ? TaskStatus.running : TaskStatus.stopped; 477 | 478 | task.output = ''; 479 | 480 | this.tasks[name] = task; 481 | 482 | this.createdTaskMap.set(id, task); 483 | 484 | createdLeaves.push(id); 485 | } 486 | 487 | this.currentNode = createBalancedTreeFromLeaves(createdLeaves); 488 | 489 | this.freshCurrentNode(); 490 | }; 491 | 492 | @action 493 | private onCreate = (task: CreatedTask): void => { 494 | let {id, name} = task; 495 | 496 | task.running = true; 497 | task.status = TaskStatus.running; 498 | 499 | this.tasks[name] = task; 500 | 501 | this.createdTaskMap.set(id, task); 502 | 503 | this.freshCurrentNode(); 504 | }; 505 | 506 | @action 507 | private onClose = (taskRef: TaskRef): void => { 508 | let {id} = taskRef; 509 | 510 | let task = this.getCreatedTaskByTaskId(id); 511 | 512 | if (!task) { 513 | return; 514 | } 515 | 516 | task.running = false; 517 | task.status = TaskStatus.ready; 518 | 519 | this.createdTaskMap.delete(id); 520 | 521 | this.freshCurrentNode(); 522 | }; 523 | 524 | @action 525 | private onStart = (taskRef: TaskRef): void => { 526 | let {id} = taskRef; 527 | 528 | let task = this.getCreatedTaskByTaskId(id); 529 | 530 | if (!task) { 531 | return; 532 | } 533 | 534 | task.running = true; 535 | task.status = TaskStatus.running; 536 | 537 | task.output = appendOutput( 538 | task.output, 539 | outputInfo('[biu] Task started.'), 540 | 'system', 541 | ); 542 | 543 | this.createdTaskMap.set(id, task); 544 | }; 545 | 546 | @action 547 | private onStop = (taskRef: TaskRef): void => { 548 | let {id} = taskRef; 549 | 550 | let task = this.getCreatedTaskByTaskId(id); 551 | 552 | if (!task) { 553 | return; 554 | } 555 | 556 | task.running = false; 557 | task.status = TaskStatus.stopped; 558 | 559 | this.createdTaskMap.set(id, task); 560 | }; 561 | 562 | @action 563 | private onRestartOnChange = (taskRef: TaskRef): void => { 564 | let {id} = taskRef; 565 | 566 | let task = this.getCreatedTaskByTaskId(id); 567 | 568 | if (!task) { 569 | return; 570 | } 571 | 572 | task.running = false; 573 | task.status = TaskStatus.restarting; 574 | 575 | task.output = appendOutput( 576 | task.output, 577 | outputInfo('[biu] Restarting on change...'), 578 | 'system', 579 | ); 580 | 581 | this.createdTaskMap.set(id, task); 582 | }; 583 | 584 | @action 585 | private onError = (data: ErrorData): void => { 586 | let {id, error} = data; 587 | 588 | let task = this.getCreatedTaskByTaskId(id); 589 | 590 | if (!task) { 591 | return; 592 | } 593 | 594 | task.output = appendOutput( 595 | task.output, 596 | outputError(error.replace(/\n/g, '
')), 597 | 'system', 598 | ); 599 | 600 | this.createdTaskMap.set(id, task); 601 | }; 602 | 603 | @action 604 | private onExit = (data: ExitData): void => { 605 | let {id, code} = data; 606 | 607 | let task = this.getCreatedTaskByTaskId(id); 608 | 609 | if (!task) { 610 | return; 611 | } 612 | 613 | let text = code 614 | ? `[biu] Task exited with code ${data.code}.` 615 | : '[biu] Task exited.'; 616 | 617 | task.output = appendOutput(task.output, outputInfo(text), 'system'); 618 | 619 | this.createdTaskMap.set(id, task); 620 | }; 621 | 622 | @action 623 | private onStdOut = (data: StdOutData): void => { 624 | let {id, html} = data; 625 | 626 | let task = this.getCreatedTaskByTaskId(id); 627 | 628 | if (!task) { 629 | return; 630 | } 631 | 632 | if (html) { 633 | task.output = appendOutput(task.output, html); 634 | } 635 | 636 | this.createdTaskMap.set(id, task); 637 | }; 638 | 639 | @action 640 | private onStdErr = (data: StdErrData): void => { 641 | let {id, html} = data; 642 | 643 | let task = this.getCreatedTaskByTaskId(id); 644 | 645 | if (!task) { 646 | return; 647 | } 648 | 649 | if (html) { 650 | task.output = appendOutput(task.output, html); 651 | } 652 | 653 | this.createdTaskMap.set(id, task); 654 | }; 655 | } 656 | 657 | export function getTaskStatus(task: Task | undefined): string { 658 | if (!task) { 659 | return 'closed'; 660 | } 661 | 662 | let {status, line} = task; 663 | 664 | switch (status) { 665 | case TaskStatus.ready: 666 | return 'ready'; 667 | case TaskStatus.running: 668 | return line ? line : 'running'; 669 | case TaskStatus.stopped: 670 | return 'stopped'; 671 | case TaskStatus.stopping: 672 | return 'stopping'; 673 | case TaskStatus.restarting: 674 | return 'restarting'; 675 | } 676 | } 677 | 678 | export function getLayoutStorageKey(taskList: TaskName[]): string { 679 | return `layout-${taskList.sort().join('|')}`; 680 | } 681 | 682 | export function getLayoutFromStorage( 683 | taskList: TaskName[], 684 | taskNameToIdMap: Map, 685 | ): MosaicNode | undefined { 686 | if (taskList.length > 1) { 687 | let key = getLayoutStorageKey(taskList); 688 | 689 | let innerMosaicNode = getStorageObject>(key); 690 | 691 | if (innerMosaicNode) { 692 | let newMosaicNode = convertMosaicNode( 693 | innerMosaicNode, 694 | taskNameToIdMap, 695 | ); 696 | 697 | return newMosaicNode; 698 | } 699 | } 700 | 701 | return undefined; 702 | } 703 | 704 | export function setLayoutToStorage( 705 | taskList: TaskName[], 706 | node: MosaicNode, 707 | taskIdToNameMap: Map, 708 | ): void { 709 | if (taskList.length > 1) { 710 | let key = getLayoutStorageKey(taskList); 711 | 712 | let nodeCopy = deepCopy(node); 713 | 714 | let mosaicNode = convertMosaicNode( 715 | nodeCopy, 716 | taskIdToNameMap, 717 | ); 718 | 719 | if (mosaicNode) { 720 | setStorageObject(key, mosaicNode); 721 | } 722 | } 723 | } 724 | 725 | export function convertMosaicNode< 726 | FromIdType extends string, 727 | ToIdType extends string 728 | >( 729 | fromNode: MosaicNode, 730 | idMap: Map, 731 | ): MosaicNode { 732 | if (typeof fromNode === 'string') { 733 | let toId = idMap.get(fromNode); 734 | 735 | if (!toId) { 736 | throw new Error(`fromId: '${fromNode}' fromId not found in \`idMap\``); 737 | } 738 | 739 | return toId; 740 | } else { 741 | fromNode.first = convertMosaicNode( 742 | fromNode.first, 743 | idMap, 744 | ) as any; 745 | fromNode.second = convertMosaicNode( 746 | fromNode.second, 747 | idMap, 748 | ) as any; 749 | 750 | return fromNode as any; 751 | } 752 | } 753 | 754 | export function getPathByTaskIdInNode( 755 | node: MosaicNode | TaskId | undefined, 756 | taskId: TaskId, 757 | path?: string, 758 | ): MosaicBranch[] | 'clean' | undefined { 759 | if (typeof node === 'string' && node === taskId) { 760 | if (path) { 761 | return path.split('|') as MosaicBranch[]; 762 | } else { 763 | return 'clean'; 764 | } 765 | } else if (typeof node === 'object') { 766 | let firstBranchResult = getPathByTaskIdInNode( 767 | node['first'], 768 | taskId, 769 | path ? `${path}|first` : 'first', 770 | ); 771 | 772 | if (firstBranchResult) { 773 | return firstBranchResult; 774 | } 775 | 776 | let secondBranchResult = getPathByTaskIdInNode( 777 | node['second'], 778 | taskId, 779 | path ? `${path}|second` : 'second', 780 | ); 781 | 782 | if (secondBranchResult) { 783 | return secondBranchResult; 784 | } 785 | } 786 | 787 | return undefined; 788 | } 789 | --------------------------------------------------------------------------------