mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-08 05:20:01 +00:00
.
This commit is contained in:
parent
f659a45532
commit
86cb06e25e
@ -138,7 +138,7 @@ start = () => {
|
|||||||
//- background to avoid the flash of unstyled window
|
//- background to avoid the flash of unstyled window
|
||||||
backgroundColor: '#1D272D',
|
backgroundColor: '#1D272D',
|
||||||
frame: false,
|
frame: false,
|
||||||
type: 'toolbar',
|
//type: 'toolbar',
|
||||||
}
|
}
|
||||||
Object.assign(options, windowConfig.get('windowBoundaries'))
|
Object.assign(options, windowConfig.get('windowBoundaries'))
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import { HotkeyDisplayComponent } from 'components/hotkeyDisplay'
|
|||||||
import { HotkeyHintComponent } from 'components/hotkeyHint'
|
import { HotkeyHintComponent } from 'components/hotkeyHint'
|
||||||
import { HotkeyInputModalComponent } from 'components/hotkeyInputModal'
|
import { HotkeyInputModalComponent } from 'components/hotkeyInputModal'
|
||||||
import { SettingsPaneComponent } from 'components/settingsPane'
|
import { SettingsPaneComponent } from 'components/settingsPane'
|
||||||
|
import { TabBodyComponent } from 'components/tabBody'
|
||||||
|
import { TabHeaderComponent } from 'components/tabHeader'
|
||||||
import { TerminalComponent } from 'components/terminal'
|
import { TerminalComponent } from 'components/terminal'
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +52,8 @@ import { TerminalComponent } from 'components/terminal'
|
|||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
HotkeyInputModalComponent,
|
HotkeyInputModalComponent,
|
||||||
|
SettingsPaneComponent,
|
||||||
|
TerminalComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
@ -59,10 +63,12 @@ import { TerminalComponent } from 'components/terminal'
|
|||||||
HotkeyInputComponent,
|
HotkeyInputComponent,
|
||||||
HotkeyInputModalComponent,
|
HotkeyInputModalComponent,
|
||||||
SettingsPaneComponent,
|
SettingsPaneComponent,
|
||||||
|
TabBodyComponent,
|
||||||
|
TabHeaderComponent,
|
||||||
TerminalComponent,
|
TerminalComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [
|
bootstrap: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
background: @body-bg;
|
background: @body-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@titlebar-height: 35px;
|
@titlebar-height: 30px;
|
||||||
@tabs-height: 40px;
|
@tabs-height: 40px;
|
||||||
@tab-border-radius: 4px;
|
@tab-border-radius: 4px;
|
||||||
|
|
||||||
@ -51,6 +51,10 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: @titlebar-height;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
&:not(:hover):not(:active) {
|
&:not(:hover):not(:active) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -62,6 +66,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host > .spacer {
|
||||||
|
flex: 0 0 5px;
|
||||||
|
background: @title-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: @tabs-height;
|
height: @tabs-height;
|
||||||
@ -69,12 +78,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
&>button, .tab {
|
&>button {
|
||||||
line-height: @tabs-height - 2px;
|
line-height: @tabs-height - 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
&>button {
|
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
@ -96,130 +103,14 @@
|
|||||||
border-bottom-right-radius: @tab-border-radius;
|
border-bottom-right-radius: @tab-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active + button {
|
tab-header.active + button {
|
||||||
border-bottom-left-radius: @tab-border-radius;
|
border-bottom-left-radius: @tab-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: auto;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-grow: 1000;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
min-width: 0;
|
|
||||||
background: @body-bg;
|
|
||||||
transition: 0.25s all;
|
|
||||||
|
|
||||||
.button-states();
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex: auto;
|
|
||||||
min-width: 0;
|
|
||||||
background: @title-bg;
|
|
||||||
transition: 0.25s all;
|
|
||||||
|
|
||||||
div.index {
|
|
||||||
flex: none;
|
|
||||||
padding: 0 0 0 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.name {
|
|
||||||
flex: auto;
|
|
||||||
margin: 0 1px 0 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
flex: none;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
color: @text-color;
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
@button-size: @tabs-height * 0.6;
|
|
||||||
width: @button-size;
|
|
||||||
height: @button-size;
|
|
||||||
border-radius: @button-size / 2;
|
|
||||||
line-height: @button-size * 0.8;
|
|
||||||
margin-top: (@tabs-height - @button-size) * 0.4;
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
|
|
||||||
.button-states();
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover button {
|
|
||||||
transition: 0.25s opacity;
|
|
||||||
display: block;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//border-bottom: 2px solid transparent;
|
|
||||||
transition: 0.25s all;
|
|
||||||
|
|
||||||
&.pre-selected, &:nth-last-child(1) {
|
|
||||||
.content-wrapper {
|
|
||||||
border-bottom-right-radius: @tab-border-radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.post-selected {
|
|
||||||
.content-wrapper {
|
|
||||||
border-bottom-left-radius: @tab-border-radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: @title-bg;
|
|
||||||
box-shadow: 0px -1px 0px 0px blue;
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
//border-bottom: 2px solid #69bbea;
|
|
||||||
background: @body-bg;
|
|
||||||
border-top-left-radius: @tab-border-radius;
|
|
||||||
border-top-right-radius: @tab-border-radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-content {
|
.tabs-content {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.tab {
|
|
||||||
display: none;
|
|
||||||
flex: auto;
|
|
||||||
position: relative;
|
|
||||||
padding: 15px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
&.scrollable {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
>* {
|
|
||||||
flex: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hotkey-hint {
|
hotkey-hint {
|
||||||
|
@ -7,32 +7,33 @@
|
|||||||
button.btn.btn-secondary.btn-close((click)='hostApp.quit()')
|
button.btn.btn-secondary.btn-close((click)='hostApp.quit()')
|
||||||
i.fa.fa-close
|
i.fa.fa-close
|
||||||
|
|
||||||
|
.spacer
|
||||||
|
|
||||||
.tabs(class='active-tab-{{tabs.indexOf(activeTab)}}')
|
.tabs(class='active-tab-{{tabs.indexOf(activeTab)}}')
|
||||||
button.btn.btn-secondary.btn-new-tab((click)='newTab()')
|
button.btn.btn-secondary.btn-new-tab((click)='newTab()')
|
||||||
i.fa.fa-plus
|
i.fa.fa-plus
|
||||||
.tab(
|
tab-header(
|
||||||
*ngFor='let tab of tabs; let idx = index; trackBy: tab?.id',
|
*ngFor='let tab of tabs; let idx = index; trackBy: tab?.id',
|
||||||
(click)='selectTab(tab)',
|
[index]='idx',
|
||||||
[class.active]='tab == activeTab',
|
[model]='tab',
|
||||||
[class.pre-selected]='tabs[idx + 1] == activeTab',
|
[active]='tab == activeTab',
|
||||||
[class.post-selected]='tabs[idx - 1] == activeTab',
|
[hasActivity]='tab.hasActivity',
|
||||||
@animateTab,
|
@animateTab,
|
||||||
|
(click)='selectTab(tab)',
|
||||||
|
(closeClicked)='closeTab(tab)',
|
||||||
)
|
)
|
||||||
.content-wrapper
|
|
||||||
div.index {{idx + 1}}
|
|
||||||
div.name {{tab.name || 'Terminal'}}
|
|
||||||
button((click)='closeTab(tab)') ×
|
|
||||||
button.btn.btn-secondary.btn-settings((click)='showSettings()')
|
button.btn.btn-secondary.btn-settings((click)='showSettings()')
|
||||||
i.fa.fa-cog
|
i.fa.fa-cog
|
||||||
|
|
||||||
.tabs-content
|
.tabs-content
|
||||||
.tab(
|
tab-body(
|
||||||
*ngFor='let tab of tabs; trackBy: tab?.id',
|
*ngFor='let tab of tabs; trackBy: tab?.id',
|
||||||
[class.active]='tab == activeTab',
|
[active]='tab == activeTab',
|
||||||
|
[model]='tab',
|
||||||
[class.scrollable]='tab.scrollable',
|
[class.scrollable]='tab.scrollable',
|
||||||
)
|
)
|
||||||
terminal(*ngIf='tab.type == "terminal"', [session]='tab.session', '[(title)]'='tab.name')
|
//-terminal(*ngIf='tab.type == "terminal"', [session]='tab.session', '[(title)]'='tab.name')
|
||||||
settings-pane(*ngIf='tab.type == "settings"')
|
//-settings-pane(*ngIf='tab.type == "settings"')
|
||||||
|
|
||||||
hotkey-hint
|
hotkey-hint
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ElementRef, trigger, style, animate, transition, state } from '@angular/core'
|
import { Component, ElementRef, Input, trigger, style, animate, transition, state } from '@angular/core'
|
||||||
import { ToasterConfig } from 'angular2-toaster'
|
import { ToasterConfig } from 'angular2-toaster'
|
||||||
|
|
||||||
import { ElectronService } from 'services/electron'
|
import { ElectronService } from 'services/electron'
|
||||||
@ -8,31 +8,15 @@ import { LogService } from 'services/log'
|
|||||||
import { QuitterService } from 'services/quitter'
|
import { QuitterService } from 'services/quitter'
|
||||||
import { ConfigService } from 'services/config'
|
import { ConfigService } from 'services/config'
|
||||||
import { DockingService } from 'services/docking'
|
import { DockingService } from 'services/docking'
|
||||||
import { Session, SessionsService } from 'services/sessions'
|
import { SessionsService } from 'services/sessions'
|
||||||
|
|
||||||
|
import { Tab, SettingsTab, TerminalTab } from 'models/tab'
|
||||||
|
|
||||||
import 'angular2-toaster/lib/toaster.css'
|
import 'angular2-toaster/lib/toaster.css'
|
||||||
import 'global.less'
|
import 'global.less'
|
||||||
import 'theme.scss'
|
import 'theme.scss'
|
||||||
|
|
||||||
|
|
||||||
const TYPE_TERMINAL = 'terminal'
|
|
||||||
const TYPE_SETTINGS = 'settings'
|
|
||||||
|
|
||||||
class Tab {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
scrollable: boolean
|
|
||||||
static lastTabID = 0
|
|
||||||
|
|
||||||
constructor (public type: string, public session: Session) {
|
|
||||||
this.id = Tab.lastTabID++
|
|
||||||
if (type == TYPE_SETTINGS) {
|
|
||||||
this.name = 'Settings'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app',
|
selector: 'app',
|
||||||
template: require('./app.pug'),
|
template: require('./app.pug'),
|
||||||
@ -58,8 +42,8 @@ class Tab {
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
toasterConfig: ToasterConfig
|
toasterConfig: ToasterConfig
|
||||||
tabs: Tab[] = []
|
@Input() tabs: Tab[] = []
|
||||||
activeTab: Tab
|
@Input() activeTab: Tab
|
||||||
lastTabIndex = 0
|
lastTabIndex = 0
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -161,11 +145,11 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newTab () {
|
newTab () {
|
||||||
this.addTerminalTab(this.sessions.createNewSession({shell: 'zsh'}))
|
this.addTerminalTab(this.sessions.createNewSession({command: 'zsh'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
addTerminalTab (session) {
|
addTerminalTab (session) {
|
||||||
let tab = new Tab(TYPE_TERMINAL, session)
|
let tab = new TerminalTab(session)
|
||||||
this.tabs.push(tab)
|
this.tabs.push(tab)
|
||||||
this.selectTab(tab)
|
this.selectTab(tab)
|
||||||
}
|
}
|
||||||
@ -176,6 +160,9 @@ export class AppComponent {
|
|||||||
} else {
|
} else {
|
||||||
this.lastTabIndex = null
|
this.lastTabIndex = null
|
||||||
}
|
}
|
||||||
|
if (this.activeTab) {
|
||||||
|
this.activeTab.hasActivity = false
|
||||||
|
}
|
||||||
this.activeTab = tab
|
this.activeTab = tab
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
let iframe = this.elementRef.nativeElement.querySelector(':scope .tab.active iframe')
|
let iframe = this.elementRef.nativeElement.querySelector(':scope .tab.active iframe')
|
||||||
@ -207,8 +194,9 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeTab (tab) {
|
closeTab (tab) {
|
||||||
|
tab.destroy()
|
||||||
if (tab.session) {
|
if (tab.session) {
|
||||||
tab.session.gracefullyDestroy()
|
this.sessions.destroySession(tab.session)
|
||||||
}
|
}
|
||||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||||
this.tabs = this.tabs.filter((x) => x != tab)
|
this.tabs = this.tabs.filter((x) => x != tab)
|
||||||
@ -231,10 +219,9 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showSettings() {
|
showSettings() {
|
||||||
let settingsTab = this.tabs.find((x) => x.type == TYPE_SETTINGS)
|
let settingsTab = this.tabs.find((x) => x instanceof SettingsTab)
|
||||||
if (!settingsTab) {
|
if (!settingsTab) {
|
||||||
settingsTab = new Tab(TYPE_SETTINGS, null)
|
settingsTab = new SettingsTab()
|
||||||
settingsTab.scrollable = true
|
|
||||||
this.tabs.push(settingsTab)
|
this.tabs.push(settingsTab)
|
||||||
}
|
}
|
||||||
this.selectTab(settingsTab)
|
this.selectTab(settingsTab)
|
||||||
|
12
app/src/components/baseTab.ts
Normal file
12
app/src/components/baseTab.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Tab } from 'models/tab'
|
||||||
|
|
||||||
|
export class BaseTabComponent<T extends Tab> {
|
||||||
|
protected model: T
|
||||||
|
|
||||||
|
initModel (model: T) {
|
||||||
|
this.model = model
|
||||||
|
this.initTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
initTab () { }
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
|
flex: auto;
|
||||||
|
margin: 15px;
|
||||||
|
|
||||||
>.btn-block {
|
>.btn-block {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,16 @@ import 'rxjs/add/operator/debounceTime'
|
|||||||
import 'rxjs/add/operator/distinctUntilChanged'
|
import 'rxjs/add/operator/distinctUntilChanged'
|
||||||
const childProcessPromise = nodeRequire('child-process-promise')
|
const childProcessPromise = nodeRequire('child-process-promise')
|
||||||
|
|
||||||
|
import { BaseTabComponent } from 'components/baseTab'
|
||||||
|
import { SettingsTab } from 'models/tab'
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-pane',
|
selector: 'settings-pane',
|
||||||
template: require('./settingsPane.pug'),
|
template: require('./settingsPane.pug'),
|
||||||
styles: [require('./settingsPane.less')],
|
styles: [require('./settingsPane.less')],
|
||||||
})
|
})
|
||||||
export class SettingsPaneComponent {
|
export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> {
|
||||||
isWindows: boolean
|
isWindows: boolean
|
||||||
isMac: boolean
|
isMac: boolean
|
||||||
isLinux: boolean
|
isLinux: boolean
|
||||||
@ -31,6 +34,7 @@ export class SettingsPaneComponent {
|
|||||||
public docking: DockingService,
|
public docking: DockingService,
|
||||||
hostApp: HostAppService,
|
hostApp: HostAppService,
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.isWindows = hostApp.platform == PLATFORM_WINDOWS
|
this.isWindows = hostApp.platform == PLATFORM_WINDOWS
|
||||||
this.isMac = hostApp.platform == PLATFORM_MAC
|
this.isMac = hostApp.platform == PLATFORM_MAC
|
||||||
this.isLinux = hostApp.platform == PLATFORM_LINUX
|
this.isLinux = hostApp.platform == PLATFORM_LINUX
|
||||||
|
18
app/src/components/tabBody.scss
Normal file
18
app/src/components/tabBody.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
:host {
|
||||||
|
display: none;
|
||||||
|
flex: auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.scrollable {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
>* {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
app/src/components/tabBody.ts
Normal file
29
app/src/components/tabBody.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Component, Input, ViewContainerRef, ViewChild, HostBinding, ComponentFactoryResolver, ComponentRef } from '@angular/core'
|
||||||
|
import { Tab } from 'models/tab'
|
||||||
|
import { BaseTabComponent } from 'components/baseTab'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tab-body',
|
||||||
|
template: '<template #placeholder></template>',
|
||||||
|
styles: [require('./tabBody.scss')],
|
||||||
|
})
|
||||||
|
export class TabBodyComponent {
|
||||||
|
@Input() @HostBinding('class.active') active: boolean
|
||||||
|
@Input() model: Tab
|
||||||
|
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||||
|
private component: ComponentRef<BaseTabComponent<Tab>>
|
||||||
|
|
||||||
|
constructor (private componentFactoryResolver: ComponentFactoryResolver) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
// run after the change detection finishes
|
||||||
|
setImmediate(() => {
|
||||||
|
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.model.getComponentType())
|
||||||
|
this.component = this.placeholder.createComponent(componentFactory)
|
||||||
|
setImmediate(() => {
|
||||||
|
this.component.instance.initModel(this.model)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
4
app/src/components/tabHeader.pug
Normal file
4
app/src/components/tabHeader.pug
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.content-wrapper
|
||||||
|
.index {{index + 1}}
|
||||||
|
.name {{model.title || "Terminal"}}
|
||||||
|
button((click)='closeClicked.emit()') ×
|
80
app/src/components/tabHeader.scss
Normal file
80
app/src/components/tabHeader.scss
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
@import '~variables.scss';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
line-height: $tabs-height - 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
flex: auto;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-grow: 1000;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
transition: 0.25s all;
|
||||||
|
//.button-states();
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: auto;
|
||||||
|
min-width: 0;
|
||||||
|
transition: 0.25s all;
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
flex: none;
|
||||||
|
font-weight: bold;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transition: 0.25s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: auto;
|
||||||
|
margin: 0 1px 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: none;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
$button-size: $tabs-height * 0.6;
|
||||||
|
width: $button-size;
|
||||||
|
height: $button-size;
|
||||||
|
border-radius: $button-size / 2;
|
||||||
|
line-height: $button-size * 0.8;
|
||||||
|
margin-top: ($tabs-height - $button-size) * 0.4;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
//.button-states();
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button {
|
||||||
|
transition: 0.25s opacity;
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//border-bottom: 2px solid transparent;
|
||||||
|
transition: 0.25s all;
|
||||||
|
}
|
17
app/src/components/tabHeader.ts
Normal file
17
app/src/components/tabHeader.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'
|
||||||
|
import { Tab } from 'models/tab'
|
||||||
|
|
||||||
|
import './tabHeader.scss'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tab-header',
|
||||||
|
template: require('./tabHeader.pug'),
|
||||||
|
styles: [require('./tabHeader.scss')],
|
||||||
|
})
|
||||||
|
export class TabHeaderComponent {
|
||||||
|
@Input() index: number
|
||||||
|
@Input() @HostBinding('class.active') active: boolean
|
||||||
|
@Input() @HostBinding('class.has-activity') hasActivity: boolean
|
||||||
|
@Input() model: Tab
|
||||||
|
@Output() closeClicked = new EventEmitter()
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
:host {
|
:host {
|
||||||
|
flex: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin: 15px;
|
||||||
|
|
||||||
div[style]:last-child {
|
div[style]:last-child {
|
||||||
background: black !important;
|
background: black !important;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core'
|
import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core'
|
||||||
|
|
||||||
import { ConfigService } from 'services/config'
|
import { ConfigService } from 'services/config'
|
||||||
import { PluginDispatcherService } from 'services/pluginDispatcher'
|
import { PluginDispatcherService } from 'services/pluginDispatcher'
|
||||||
import { Session } from 'services/sessions'
|
|
||||||
|
import { BaseTabComponent } from 'components/baseTab'
|
||||||
|
import { TerminalTab } from 'models/tab'
|
||||||
|
|
||||||
|
|
||||||
const hterm = require('hterm-commonjs')
|
const hterm = require('hterm-commonjs')
|
||||||
const dataurl = require('dataurl')
|
const dataurl = require('dataurl')
|
||||||
@ -47,8 +50,7 @@ hterm.hterm.Terminal.prototype.showOverlay = () => null
|
|||||||
template: '',
|
template: '',
|
||||||
styles: [require('./terminal.scss')],
|
styles: [require('./terminal.scss')],
|
||||||
})
|
})
|
||||||
export class TerminalComponent {
|
export class TerminalComponent extends BaseTabComponent<TerminalTab> {
|
||||||
@Input() session: Session
|
|
||||||
title: string
|
title: string
|
||||||
@Output() titleChange = new EventEmitter()
|
@Output() titleChange = new EventEmitter()
|
||||||
terminal: any
|
terminal: any
|
||||||
@ -60,12 +62,13 @@ export class TerminalComponent {
|
|||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
private pluginDispatcher: PluginDispatcherService,
|
private pluginDispatcher: PluginDispatcherService,
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.configSubscription = config.change.subscribe(() => {
|
this.configSubscription = config.change.subscribe(() => {
|
||||||
this.configure()
|
this.configure()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
initTab () {
|
||||||
let io
|
let io
|
||||||
this.terminal = new hterm.hterm.Terminal()
|
this.terminal = new hterm.hterm.Terminal()
|
||||||
this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal })
|
this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal })
|
||||||
@ -78,23 +81,23 @@ export class TerminalComponent {
|
|||||||
this.terminal.onTerminalReady = () => {
|
this.terminal.onTerminalReady = () => {
|
||||||
this.terminal.installKeyboard()
|
this.terminal.installKeyboard()
|
||||||
io = this.terminal.io.push()
|
io = this.terminal.io.push()
|
||||||
const dataSubscription = this.session.dataAvailable.subscribe((data) => {
|
const dataSubscription = this.model.session.dataAvailable.subscribe((data) => {
|
||||||
io.writeUTF16(data)
|
io.writeUTF8(data)
|
||||||
})
|
})
|
||||||
const closedSubscription = this.session.closed.subscribe(() => {
|
const closedSubscription = this.model.session.closed.subscribe(() => {
|
||||||
dataSubscription.unsubscribe()
|
dataSubscription.unsubscribe()
|
||||||
closedSubscription.unsubscribe()
|
closedSubscription.unsubscribe()
|
||||||
})
|
})
|
||||||
|
|
||||||
io.onVTKeystroke = io.sendString = (str) => {
|
io.onVTKeystroke = io.sendString = (str) => {
|
||||||
this.session.write(str)
|
this.model.session.write(str)
|
||||||
}
|
}
|
||||||
io.onTerminalResize = (columns, rows) => {
|
io.onTerminalResize = (columns, rows) => {
|
||||||
console.log(`Resizing to ${columns}x${rows}`)
|
console.log(`Resizing to ${columns}x${rows}`)
|
||||||
this.session.resize(columns, rows)
|
this.model.session.resize(columns, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session.releaseInitialDataBuffer()
|
this.model.session.releaseInitialDataBuffer()
|
||||||
}
|
}
|
||||||
this.terminal.decorate(this.elementRef.nativeElement)
|
this.terminal.decorate(this.elementRef.nativeElement)
|
||||||
this.configure()
|
this.configure()
|
||||||
@ -108,6 +111,8 @@ export class TerminalComponent {
|
|||||||
preferenceManager.set('audible-bell-sound', '')
|
preferenceManager.set('audible-bell-sound', '')
|
||||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification')
|
preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification')
|
||||||
preferenceManager.set('enable-clipboard-notice', false)
|
preferenceManager.set('enable-clipboard-notice', false)
|
||||||
|
preferenceManager.set('receive-encoding', 'raw')
|
||||||
|
preferenceManager.set('send-encoding', 'raw')
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
|
64
app/src/models/tab.ts
Normal file
64
app/src/models/tab.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
import { Session } from 'services/sessions'
|
||||||
|
|
||||||
|
|
||||||
|
export class Tab {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
scrollable: boolean
|
||||||
|
hasActivity = false
|
||||||
|
static lastTabID = 0
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.id = Tab.lastTabID++
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentType (): (new (...args: any[])) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy (): void { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { SettingsPaneComponent } from 'components/settingsPane'
|
||||||
|
|
||||||
|
export class SettingsTab extends Tab {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this.title = 'Settings'
|
||||||
|
this.scrollable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentType (): (new (...args: any[])) {
|
||||||
|
return SettingsPaneComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { TerminalComponent } from 'components/terminal'
|
||||||
|
|
||||||
|
export class TerminalTab extends Tab {
|
||||||
|
private activitySubscription: Subscription
|
||||||
|
|
||||||
|
constructor (public session: Session) {
|
||||||
|
super()
|
||||||
|
// ignore the initial refresh
|
||||||
|
setTimeout(() => {
|
||||||
|
this.activitySubscription = this.session.dataAvailable.subscribe(() => {
|
||||||
|
this.hasActivity = true
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentType (): (new (...args: any[])) {
|
||||||
|
return TerminalComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
super.destroy()
|
||||||
|
if (this.activitySubscription) {
|
||||||
|
this.activitySubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +1,38 @@
|
|||||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||||
import { Logger, LogService } from 'services/log'
|
import { Logger, LogService } from 'services/log'
|
||||||
const exec = require('child-process-promise').exec
|
const exec = require('child-process-promise').exec
|
||||||
import * as crypto from 'crypto'
|
|
||||||
import * as nodePTY from 'node-pty'
|
import * as nodePTY from 'node-pty'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
|
||||||
export interface SessionRecoveryProvider {
|
export interface ISessionRecoveryProvider {
|
||||||
list(): Promise<any[]>
|
list (): Promise<any[]>
|
||||||
getRecoveryCommand(item: any): string
|
getRecoverySession (recoveryId: any): SessionOptions
|
||||||
getNewSessionCommand(command: string): string
|
wrapNewSession (options: SessionOptions): SessionOptions
|
||||||
|
terminateSession (recoveryId: string): Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NullSessionRecoveryProvider implements SessionRecoveryProvider {
|
export class NullSessionRecoveryProvider implements ISessionRecoveryProvider {
|
||||||
list(): Promise<any[]> {
|
async list (): Promise<any[]> {
|
||||||
return Promise.resolve([])
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecoveryCommand(_: any): string {
|
getRecoverySession (_recoveryId: any): SessionOptions {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
getNewSessionCommand(command: string) {
|
wrapNewSession (options: SessionOptions): SessionOptions {
|
||||||
return command
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminateSession (_recoveryId: string): Promise<any> {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
|
export class ScreenSessionRecoveryProvider implements ISessionRecoveryProvider {
|
||||||
list(): Promise<any[]> {
|
list(): Promise<any[]> {
|
||||||
return exec('screen -ls').then((result) => {
|
return exec('screen -list').then((result) => {
|
||||||
return result.stdout.split('\n')
|
return result.stdout.split('\n')
|
||||||
.filter((line) => /\bterm-tab-/.exec(line))
|
.filter((line) => /\bterm-tab-/.exec(line))
|
||||||
.map((line) => line.trim().split('.')[0])
|
.map((line) => line.trim().split('.')[0])
|
||||||
@ -37,12 +41,14 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecoveryCommand(item: any): string {
|
getRecoverySession (recoveryId: any): SessionOptions {
|
||||||
return `screen -r ${item}`
|
return {
|
||||||
|
command: 'screen',
|
||||||
|
args: ['-r', recoveryId],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNewSessionCommand(command: string): string {
|
wrapNewSession (options: SessionOptions): SessionOptions {
|
||||||
const id = crypto.randomBytes(8).toString('hex')
|
|
||||||
// TODO
|
// TODO
|
||||||
let configPath = '/tmp/.termScreenConfig'
|
let configPath = '/tmp/.termScreenConfig'
|
||||||
fs.writeFileSync(configPath, `
|
fs.writeFileSync(configPath, `
|
||||||
@ -51,8 +57,19 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
|
|||||||
term xterm-color
|
term xterm-color
|
||||||
bindkey "^[OH" beginning-of-line
|
bindkey "^[OH" beginning-of-line
|
||||||
bindkey "^[OF" end-of-line
|
bindkey "^[OF" end-of-line
|
||||||
|
termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
|
||||||
|
defhstatus "^Et"
|
||||||
|
hardstatus off
|
||||||
`, 'utf-8')
|
`, 'utf-8')
|
||||||
return `screen -c ${configPath} -U -S term-tab-${id} -- ${command}`
|
let recoveryId = `term-tab-${Date.now()}`
|
||||||
|
options.args = ['-c', configPath, '-U', '-S', recoveryId, '--', options.command].concat(options.args || [])
|
||||||
|
options.command = 'screen'
|
||||||
|
options.recoveryId = recoveryId
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminateSession (recoveryId: string): Promise<any> {
|
||||||
|
return exec(`screen -S ${recoveryId} -X quit`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,9 +77,10 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
|
|||||||
export interface SessionOptions {
|
export interface SessionOptions {
|
||||||
name?: string,
|
name?: string,
|
||||||
command?: string,
|
command?: string,
|
||||||
shell?: string,
|
args?: string[],
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
env?: any,
|
env?: any,
|
||||||
|
recoveryId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
@ -71,6 +89,7 @@ export class Session {
|
|||||||
dataAvailable = new EventEmitter()
|
dataAvailable = new EventEmitter()
|
||||||
closed = new EventEmitter()
|
closed = new EventEmitter()
|
||||||
destroyed = new EventEmitter()
|
destroyed = new EventEmitter()
|
||||||
|
recoveryId: string
|
||||||
private pty: any
|
private pty: any
|
||||||
private initialDataBuffer = ''
|
private initialDataBuffer = ''
|
||||||
private initialDataBufferReleased = false
|
private initialDataBufferReleased = false
|
||||||
@ -79,14 +98,16 @@ export class Session {
|
|||||||
this.name = options.name
|
this.name = options.name
|
||||||
console.log('Spawning', options.command)
|
console.log('Spawning', options.command)
|
||||||
|
|
||||||
let binary = options.shell || 'sh'
|
|
||||||
let args = options.shell ? [] : ['-c', options.command]
|
|
||||||
let env = {
|
let env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
...options.env,
|
...options.env,
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
}
|
}
|
||||||
this.pty = nodePTY.spawn(binary, args, {
|
if (options.command.includes(' ')) {
|
||||||
|
options.args = ['-c', options.command]
|
||||||
|
options.command = 'sh'
|
||||||
|
}
|
||||||
|
this.pty = nodePTY.spawn(options.command, options.args || [], {
|
||||||
//name: 'screen-256color',
|
//name: 'screen-256color',
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
//name: 'xterm-color',
|
//name: 'xterm-color',
|
||||||
@ -168,7 +189,7 @@ export class SessionsService {
|
|||||||
sessions: {[id: string]: Session} = {}
|
sessions: {[id: string]: Session} = {}
|
||||||
logger: Logger
|
logger: Logger
|
||||||
private lastID = 0
|
private lastID = 0
|
||||||
recoveryProvider: SessionRecoveryProvider
|
recoveryProvider: ISessionRecoveryProvider
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
@ -180,8 +201,10 @@ export class SessionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createNewSession (options: SessionOptions) : Session {
|
createNewSession (options: SessionOptions) : Session {
|
||||||
options.command = this.recoveryProvider.getNewSessionCommand(options.command)
|
options = this.recoveryProvider.wrapNewSession(options)
|
||||||
return this.createSession(options)
|
let session = this.createSession(options)
|
||||||
|
session.recoveryId = options.recoveryId
|
||||||
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
createSession (options: SessionOptions) : Session {
|
createSession (options: SessionOptions) : Session {
|
||||||
@ -196,12 +219,20 @@ export class SessionsService {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async destroySession (session: Session): Promise<any> {
|
||||||
|
await session.gracefullyDestroy()
|
||||||
|
await this.recoveryProvider.terminateSession(session.recoveryId)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
recoverAll () : Promise<Session[]> {
|
recoverAll () : Promise<Session[]> {
|
||||||
return <Promise<Session[]>>(this.recoveryProvider.list().then((items) => {
|
return <Promise<Session[]>>(this.recoveryProvider.list().then((items) => {
|
||||||
return this.zone.run(() => {
|
return this.zone.run(() => {
|
||||||
return items.map((item) => {
|
return items.map((recoveryId) => {
|
||||||
const command = this.recoveryProvider.getRecoveryCommand(item)
|
const options = this.recoveryProvider.getRecoverySession(recoveryId)
|
||||||
return this.createSession({command})
|
let session = this.createSession(options)
|
||||||
|
session.recoveryId = recoveryId
|
||||||
|
return session
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
@ -63,3 +63,61 @@ ngb-tabset .tab-content {
|
|||||||
[ngbradiogroup] > label.active {
|
[ngbradiogroup] > label.active {
|
||||||
background: $blue;
|
background: $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tab-border-radius: 5px;
|
||||||
|
|
||||||
|
.tabs tab-header {
|
||||||
|
background: $body-bg;
|
||||||
|
.content-wrapper {
|
||||||
|
background: $body-bg2;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: $body-color;
|
||||||
|
border: none;
|
||||||
|
transition: 0.25s all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, .25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(0, 0, 0, .5) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pre-selected, &:nth-last-child(1) {
|
||||||
|
.content-wrapper {
|
||||||
|
border-bottom-right-radius: $tab-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post-selected {
|
||||||
|
.content-wrapper {
|
||||||
|
border-bottom-left-radius: $tab-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $body-bg2;
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
border-top: 1px solid $blue;
|
||||||
|
background: $body-bg;
|
||||||
|
border-top-left-radius: $tab-border-radius;
|
||||||
|
border-top-right-radius: $tab-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-activity:not(.active) {
|
||||||
|
.content-wrapper .index {
|
||||||
|
background: $blue;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1
app/src/variables.scss
Normal file
1
app/src/variables.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
$tabs-height: 40px;
|
@ -23,7 +23,7 @@ module.exports = {
|
|||||||
loaders: [
|
loaders: [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
loader: 'awesome-typescript-loader'
|
loader: 'awesome-typescript-loader',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.pug$/,
|
test: /\.pug$/,
|
||||||
@ -63,14 +63,14 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.(png|svg)$/,
|
test: /\.(png|svg)$/,
|
||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
query: {
|
options: {
|
||||||
name: 'images/[name].[hash:8].[ext]'
|
name: 'images/[name].[hash:8].[ext]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
query: {
|
options: {
|
||||||
name: 'fonts/[name].[hash:8].[ext]'
|
name: 'fonts/[name].[hash:8].[ext]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user