diff --git a/package.json b/package.json index 65bada6..c5b96d0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "terminus-web-container", + "name": "tabby-web", "version": "1.0.0", "main": "index.js", "scripts": { @@ -17,6 +17,7 @@ "@angular/forms": "^11.0.0", "@angular/platform-browser": "^11.0.0", "@angular/platform-browser-dynamic": "^11.0.0", + "@angular/router": "^11.0.0", "@fortawesome/angular-fontawesome": "0.8", "@fortawesome/fontawesome-free": "^5.7.2", "@fortawesome/fontawesome-svg-core": "^1.2.35", diff --git a/src/app.module.ts b/src/app.module.ts index 4ae70e4..a8117d8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,12 +4,21 @@ import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http' import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' import { AppComponent } from './components/app.component' import { MainComponent } from './components/main.component' import { ConfigModalComponent } from './components/configModal.component' import { SettingsModalComponent } from './components/settingsModal.component' +import { HomeComponent } from './components/home.component' +import { LoginComponent } from './components/login.component' + +const ROUTES = [ + { path: '', component: HomeComponent }, + { path: 'app', component: MainComponent }, + { path: 'login', component: LoginComponent }, +] @NgModule({ imports: [ @@ -22,10 +31,13 @@ import { SettingsModalComponent } from './components/settingsModal.component' NgbDropdownModule, NgbModalModule, FontAwesomeModule, + RouterModule.forRoot(ROUTES), ], declarations: [ AppComponent, MainComponent, + HomeComponent, + LoginComponent, ConfigModalComponent, SettingsModalComponent, ], diff --git a/src/assets/screenshots/serial.png b/src/assets/screenshots/serial.png new file mode 100644 index 0000000..874a403 Binary files /dev/null and b/src/assets/screenshots/serial.png differ diff --git a/src/assets/screenshots/ssh.png b/src/assets/screenshots/ssh.png new file mode 100644 index 0000000..b48ce1b Binary files /dev/null and b/src/assets/screenshots/ssh.png differ diff --git a/src/assets/screenshots/tabs.png b/src/assets/screenshots/tabs.png new file mode 100644 index 0000000..8f01b95 Binary files /dev/null and b/src/assets/screenshots/tabs.png differ diff --git a/src/assets/screenshots/window.png b/src/assets/screenshots/window.png new file mode 100644 index 0000000..ee97ed7 Binary files /dev/null and b/src/assets/screenshots/window.png differ diff --git a/src/components/app.component.pug b/src/components/app.component.pug index 314c3e1..6053953 100644 --- a/src/components/app.component.pug +++ b/src/components/app.component.pug @@ -1,10 +1 @@ -main(*ngIf='ready && loggedIn') -.login-view(*ngIf='ready && !loggedIn') - .buttons - a.btn( - *ngFor='let provider of providers', - [class]='provider.cls', - href='/api/1/auth/social/login/{{provider.id}}' - ) - fa-icon([icon]='provider.icon', [fixedWidth]='true') - span Log in with {{provider.name}} +router-outlet diff --git a/src/components/app.component.ts b/src/components/app.component.ts index dc7257c..c3b9ee9 100644 --- a/src/components/app.component.ts +++ b/src/components/app.component.ts @@ -1,33 +1,9 @@ import { Component } from '@angular/core' -import { LoginService } from '../services/login.service' - -import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons' @Component({ selector: 'app', templateUrl: './app.component.pug', - styleUrls: ['./app.component.scss'], + // styleUrls: ['./app.component.scss'], }) export class AppComponent { - loggedIn: any - ready = false - - providers = [ - { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, - { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, - { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, - { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, - ] - - constructor ( - private loginService: LoginService, - ) { - this.providers = [this.providers[0]] // only keep GH for now - } - - async ngOnInit () { - await this.loginService.ready$.toPromise() - this.loggedIn = !!this.loginService.user - this.ready = true - } } diff --git a/src/components/home.component.pug b/src/components/home.component.pug new file mode 100644 index 0000000..b440cbd --- /dev/null +++ b/src/components/home.component.pug @@ -0,0 +1,96 @@ +.top-half + .navbar + img.brand(src='{{_logo}}') + .me-auto + a.btn.btn-primary([href]='releaseURL', target='_blank') + fa-icon([icon]='_downloadIcon', [fixedWidth]='true') + span Download + a.btn.btn-secondary(routerLink='/login') + fa-icon([icon]='_loginIcon', [fixedWidth]='true') + span Login + + .container + .intro + h1 Hey. + div My name is Eugene and I've built a nice terminal app, #[em.ms-1 just for you]. + div Crossplatform, local, SSH, serial - it's all there. + div Go on, try it out 👇 + + iframe(#iframe) + +.bottom-half + .container + .d-flex.m-auto + a.btn.btn-lg.btn-primary.ms-auto.me-3([href]='releaseURL', target='_blank') + fa-icon([icon]='_downloadIcon', [fixedWidth]='true') + span Latest release + + a.btn.btn-lg.btn-secondary.me-auto([href]='githubURL', target='_blank') + fa-icon([icon]='_githubIcon', [fixedWidth]='true') + span GitHub + + .quotes + .quote + .text "reasonable" + .author — Michael + + .quote + .text "cool" + .author — some dude on my ko-fi + + .quote + .text "very cool" + .author — datsukan + + .section.section-a + .container + .row + .col + img.screenshot([src]='screenshots.window') + .col + h1 The important stuff + ul + li Runs on #[strong Windows, Mac and Linux] + li Integrated #[strong SSH client] with a connection manager + li Integrated #[strong serial terminal] + li PowerShell, PS Core, WSL, Git-Bash, Cygwin, Cmder and CMD support + li Full #[strong Unicode support] including double-width characters + li File transfer from/to SSH sessions via #[strong SFTP and Zmodem] + li Theming and color schemes + li Fully #[strong configurable shortcuts] and multi-chord shortcuts + li #[strong Remembers your tabs] and split panes + li Proper shell experience on Windows including #[strong tab completion] + li Integrated #[strong encrypted container] for SSH secrets and configuration + + .section.section-b + .container + .row + .col + h1 Terminal features + ul + li Multiple #[strong nested panes] + li #[strong Progress bars] and activity notifications for tabs + li Terminus remembers open tabs and panes where you left off + li Tabs on #[strong any side of the window] + li Optional #[strong quake mode] (terminal docked to a side of the screen) + li Optional #[strong global hotkey] to focus/hide the terminal + li Bracketed paste + .col + img.screenshot([src]='screenshots.tabs') + + .section.section-a + .container + .row + .col + img.screenshot([src]='screenshots.ssh') + .col + h1 SSH + ul + li SSH2 client with a connection manager + li #[strong SFTP and Zmodem] file transfers + li #[strong X11] and #[strong port forwarding] + li Jump hosts + li #[strong Agent forwarding] - including Pageant and Windows native OpenSSH Agent + li Login scripts + li Optional built-in #[strong password manager] with a master passphrase + li #[strong Proxy command] support diff --git a/src/components/home.component.scss b/src/components/home.component.scss new file mode 100644 index 0000000..a7c508f --- /dev/null +++ b/src/components/home.component.scss @@ -0,0 +1,95 @@ +@import "../theme/vars.scss"; + +:host { + font-size: 20px; +} + +.top-half { + background: linear-gradient(#0c141c, #15202b); +} + +.navbar { + display: flex; + padding: 15px 30px; + + a, button { + margin-left: 10px; + } +} + +.brand { + width: 32px; +} + +h1 { + font-family: $font-family-monospace; + font-weight: bold; + font-size: 48px; + text-shadow: 0 0 1px black; +} + +.intro { + width: 60vw; + margin: auto; +} + +iframe { + display: block; + margin: auto; + position: relative; + top: 20vw; + margin-top: -18vw; + + width: 60vw; + height: 42vw; + overflow: hidden; + border-radius: 5px; + box-shadow: 0 0 2px black, 0 0 50px #6ef2ff05, 0 0 150px #6854ff14; +} + +.bottom-half { + padding-top: 24vw; +} + +.quotes { + margin: 30px 0; + display: flex; + justify-content: center; + text-align: center; + + .quote { + margin: 0 30px; + + .text { + font-size: 50px; + font-style: italic; + } + + .author { + font-size: 14px; + } + } +} + +strong { + background: #9400ff57; + font-weight: normal; + padding: 2px 7px; + text-shadow: 0 0 2px black; +} + +.section { + padding: 30px 0; + + .screenshot { + min-width: 0; + max-width: 100%; + } +} + +.section-a { + background: rgba(0, 0, 0, .5); +} + +.section-b { +} diff --git a/src/components/home.component.ts b/src/components/home.component.ts new file mode 100644 index 0000000..468401f --- /dev/null +++ b/src/components/home.component.ts @@ -0,0 +1,81 @@ +import * as semverGT from 'semver/functions/gt' +import { HttpClient } from '@angular/common/http' +import { Component, ElementRef, ViewChild } from '@angular/core' +import { Version } from '../api' +import { faDownload, faSignInAlt } from '@fortawesome/free-solid-svg-icons' +import { faGithub } from '@fortawesome/free-brands-svg-icons' + +class DemoConnector { + constructor (targetWindow: Window, private version: Version) { + targetWindow['terminusWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/terminus-web-demo/data` + } + + async loadConfig (): Promise { + return '{}' + } + + async saveConfig (content: string): Promise { + } + + getAppVersion (): string { + return this.version.version + } + + getDistURL (): string { + return '/app-dist' + } + + getPluginsToLoad (): string[] { + return [ + 'terminus-core', + 'terminus-settings', + 'terminus-terminal', + 'terminus-community-color-schemes', + 'terminus-web', + 'terminus-web-demo', + ] + } +} + +@Component({ + selector: 'home', + templateUrl: './home.component.pug', + styleUrls: ['./home.component.scss'], +}) +export class HomeComponent { + @ViewChild('iframe') iframe: ElementRef + connector: DemoConnector + githubURL = 'https://github.com/Eugeny/terminus' + releaseURL = `${this.githubURL}/releases/latest` + _logo = require('../assets/logo.svg') + _downloadIcon = faDownload + _loginIcon = faSignInAlt + _githubIcon = faGithub + screenshots = { + window: require('../assets/screenshots/window.png'), + tabs: require('../assets/screenshots/tabs.png'), + ssh: require('../assets/screenshots/ssh.png'), + } + + constructor (private http: HttpClient) { + window.addEventListener('message', this.connectorRequestHandler) + } + + connectorRequestHandler = event => { + if (event.data === 'request-connector') { + this.iframe.nativeElement.contentWindow['__connector__'] = this.connector + this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') + } + } + + async ngAfterViewInit () { + const versions = await this.http.get('/api/1/versions').toPromise() + versions.sort((a, b) => semverGT(a, b)) + this.connector = new DemoConnector(this.iframe.nativeElement.contentWindow, versions[0]) + this.iframe.nativeElement.src = '/terminal' + } + + ngOnDestroy () { + window.removeEventListener('message', this.connectorRequestHandler) + } +} diff --git a/src/components/login.component.pug b/src/components/login.component.pug new file mode 100644 index 0000000..314c3e1 --- /dev/null +++ b/src/components/login.component.pug @@ -0,0 +1,10 @@ +main(*ngIf='ready && loggedIn') +.login-view(*ngIf='ready && !loggedIn') + .buttons + a.btn( + *ngFor='let provider of providers', + [class]='provider.cls', + href='/api/1/auth/social/login/{{provider.id}}' + ) + fa-icon([icon]='provider.icon', [fixedWidth]='true') + span Log in with {{provider.name}} diff --git a/src/components/app.component.scss b/src/components/login.component.scss similarity index 100% rename from src/components/app.component.scss rename to src/components/login.component.scss diff --git a/src/components/login.component.ts b/src/components/login.component.ts new file mode 100644 index 0000000..b4a1e59 --- /dev/null +++ b/src/components/login.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core' +import { LoginService } from '../services/login.service' + +import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons' + +@Component({ + selector: 'login', + templateUrl: './login.component.pug', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + loggedIn: any + ready = false + + providers = [ + { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, + { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, + { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, + { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, + ] + + constructor ( + private loginService: LoginService, + ) { + this.providers = [this.providers[0]] // only keep GH for now + } + + async ngOnInit () { + await this.loginService.ready$.toPromise() + this.loggedIn = !!this.loginService.user + this.ready = true + } +} diff --git a/src/components/main.component.ts b/src/components/main.component.ts index 95ca649..94afe02 100644 --- a/src/components/main.component.ts +++ b/src/components/main.component.ts @@ -10,6 +10,7 @@ import { ConfigModalComponent } from './configModal.component' import { ConfigService } from '../services/config.service' import { combineLatest } from 'rxjs' import { Config, Version } from '../api' +import { Router } from '@angular/router' @Component({ selector: 'main', @@ -35,16 +36,25 @@ export class MainComponent { public loginService: LoginService, private ngbModal: NgbModal, private config: ConfigService, + private router: Router, ) { - window.addEventListener('message', event => { - if (event.data === 'request-connector') { - this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector - this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') - } - }) + window.addEventListener('message', this.connectorRequestHandler) + } + + connectorRequestHandler = event => { + if (event.data === 'request-connector') { + this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector + this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') + } } async ngAfterViewInit () { + await this.loginService.ready$.toPromise() + if (!this.loginService.user) { + this.router.navigate(['/login']) + return + } + combineLatest( this.config.activeConfig$, this.config.activeVersion$ @@ -58,6 +68,10 @@ export class MainComponent { await this.config.selectDefaultConfig() } + ngOnDestroy () { + window.removeEventListener('message', this.connectorRequestHandler) + } + unloadApp () { this.showApp = false this.iframe.nativeElement.src = 'about:blank' diff --git a/src/index.pug b/src/index.pug index d570fb6..1de7191 100644 --- a/src/index.pug +++ b/src/index.pug @@ -1,8 +1,8 @@ doctype html html head + base(href='/') meta(name='viewport', content='initial-scale=1, minimal-ui, shrink-to-fit=no') - link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400", rel="stylesheet") script(src='/build/index.js', defer) title Terminus body diff --git a/src/services/appConnector.service.ts b/src/services/appConnector.service.ts index 9ec831c..2f8dc74 100644 --- a/src/services/appConnector.service.ts +++ b/src/services/appConnector.service.ts @@ -119,6 +119,21 @@ export class AppConnectorService { return this.version.version } + getDistURL (): string { + return '../app-dist' + } + + getPluginsToLoad (): string[] { + return [ + 'terminus-core', + 'terminus-settings', + 'terminus-terminal', + 'terminus-ssh', + 'terminus-community-color-schemes', + 'terminus-web', + ] + } + createSocket () { return new SocketProxy(this) } diff --git a/src/styles.scss b/src/styles.scss index 5780c01..387efc4 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,4 +6,7 @@ body { overscroll-behavior: none; } +@import "~source-code-pro/source-code-pro.css"; +@import "~source-sans-pro/source-sans-pro.css"; + @import "./theme/index.scss" diff --git a/src/terminal.ts b/src/terminal.ts index 3c24902..bdb06ba 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -27,35 +27,32 @@ async function start () { return window['module'].exports } - const baseUrl = `../app-dist/${appVersion}` + const baseUrl = `${connector.getDistURL()}/${appVersion}` await webRequire(`${baseUrl}/web/dist/preload.js`) await webRequire(`${baseUrl}/web/dist/bundle.js`) const terminus = window['Terminus'] const pluginModules = [] - for (const plugin of [ - 'terminus-core', - 'terminus-settings', - 'terminus-terminal', - 'terminus-ssh', - 'terminus-community-color-schemes', - 'terminus-web', - ]) { + for (const plugin of connector.getPluginsToLoad()) { pluginModules.push(await terminus.loadPlugin(`${baseUrl}/${plugin}`)) } document.querySelector('app-root')['style'].display = 'flex' const config = connector.loadConfig() - terminus.bootstrap(pluginModules, { - config, - executable: 'web', - isFirstWindow: true, - windowID: 1, - installedPlugins: [], - userPluginsPath: '/', + terminus.bootstrap({ + packageModules: pluginModules, + bootstrapData: { + config, + executable: 'web', + isFirstWindow: true, + windowID: 1, + installedPlugins: [], + userPluginsPath: '/', + }, + debugMode: false, + connector, }) } - start() diff --git a/src/theme/index.scss b/src/theme/index.scss index f6f9664..22e29a8 100644 --- a/src/theme/index.scss +++ b/src/theme/index.scss @@ -66,3 +66,13 @@ .modal-footer { background: #00000030; } + +a, button { + fa-icon { + opacity: .75; + } + + fa-icon + * { + margin-left: 5px; + } +} diff --git a/terminus/app/api.py b/terminus/app/api.py index 647d5ca..545c193 100644 --- a/terminus/app/api.py +++ b/terminus/app/api.py @@ -51,7 +51,6 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet): lookup_field = 'id' lookup_value_regex = r'[\w\d.-]+' queryset = '' - permission_classes = [IsAuthenticated] def _get_versions(self): return [AppVersion(version=x) for x in os.listdir(settings.APP_DIST_PATH)] diff --git a/terminus/app/urls.py b/terminus/app/urls.py index 80adb38..1cb30b8 100644 --- a/terminus/app/urls.py +++ b/terminus/app/urls.py @@ -14,7 +14,8 @@ urlpatterns = [ path('api/1/auth/logout', api.LogoutView.as_view()), path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})), - path('', views.IndexView.as_view()), + re_path('^(|login|app)$', views.IndexView.as_view()), + path('terminal', views.TerminalView.as_view()), path('app-dist//', views.AppDistView.as_view()), path('build/', views.BuildView.as_view()), diff --git a/terminus/settings.py b/terminus/settings.py index 8ffc368..3a5cde1 100644 --- a/terminus/settings.py +++ b/terminus/settings.py @@ -138,7 +138,7 @@ AUTHENTICATION_BACKENDS = ( SOCIAL_AUTH_GITHUB_SCOPE = ['read:user', 'user:email'] -LOGIN_REDIRECT_URL = '/' +LOGIN_REDIRECT_URL = '/app' APP_DIST_PATH = BASE_DIR / 'app-dist' diff --git a/yarn.lock b/yarn.lock index 339f2b2..ce062c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,6 +72,13 @@ dependencies: tslib "^2.0.0" +"@angular/router@^11.0.0": + version "11.2.14" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.14.tgz#4a8eb6d010a1a45c8e7f0c3b5b705959bdb69294" + integrity sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA== + dependencies: + tslib "^2.0.0" + "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"