diff --git a/.travis.yml b/.travis.yml index 0ec304a..a11af94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,20 @@ -osx_image: xcode10.3 # define OS X image which will be mounted +osx_image: xcode10.3 -dist: xenial # use Ubuntu Xenial for Linux operation system +dist: xenial -# Note: if you switch to sudo: false, you'll need to launch chrome with --no-sandbox. -# See https://github.com/travis-ci/travis-ci/issues/8836 sudo: required -# Define Node.js as the programming language as we have a web application language: node_js node_js: '11' addons: - chrome: stable # Install chrome stable on operating systems + chrome: stable apt: sources: - ubuntu-toolchain-r-test packages: - - g++-5 + - g++ -# A list of operating systems which are used for tests os: - linux - osx @@ -28,7 +24,7 @@ env: - ELECTRON_CACHE=$HOME/.cache/electron - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - YARN-GPG=no - - CXX=g++-5 + - CXX=g++ cache: yarn: true @@ -40,16 +36,15 @@ cache: before_install: - npm install -g node-gyp - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install python; fi + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libsecret-1-dev; fi -# These commands are executed before the scripts are executed install: - # Install all dependencies listed in your package.json file - npm install + - node_modules/.bin/electron-rebuild script: - tsc - echo "Deploy linux release to GitHub" - if [[ "$TRAVIS_BRANCH" == "canary" ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run publish:linux; fi - - echo "Deploy mac release to GitHub" - if [[ "$TRAVIS_BRANCH" == "canary" ]] && [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run publish:mac; fi diff --git a/README.md b/README.md index 3b1cb47..23ca1ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![](https://i.imgur.com/gcAGPt1.png) +[![Build Status](https://travis-ci.org/QuiiBz/squid.svg?branch=canary)](https://travis-ci.org/QuiiBz/squid) [![Known Vulnerabilities](https://snyk.io/test/github/QuiiBz/squid/badge.svg?targetFile=package.json)](https://snyk.io/test/github/QuiiBz/squid?targetFile=package.json) [![Quality score](https://www.code-inspector.com/project/4175/score/svg)](https://www.code-inspector.com/project/4175/score/svg) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/QuiiBz/squid/issues) @@ -8,7 +9,7 @@ **Squid** is a terminal emulator, build with new technologies. ## Download -A **canary** version of Squid is available for download in [releases](https://github.com/QuiiBz/squid/releases) +**Canary** versions of Squid are availables for download in [releases](https://github.com/QuiiBz/squid/releases) ## Contribute First of all, make sure you have NPM installed on your system. Then : @@ -18,7 +19,15 @@ First of all, make sure you have NPM installed on your system. Then : 4) In another terminal tab, run `npm start` to launch the app ## Screenshots -![](https://i.imgur.com/ya1IA7J.png) + +### Home page +![](https://i.imgur.com/XrNMD9k.png) + +### Terminals +![](https://i.imgur.com/NWbRhjR.png) + +### Add/edit host +![](https://i.imgur.com/EIUqjmk.png) ## Themes and settings You can create and set a theme very easily. Themes are basic JSON files, located in `userData` : @@ -53,4 +62,42 @@ To create a theme, create a new file in `userData` folder, named `.th } ``` -Then, to apply your theme, locate the key `currentTheme` in the settings file (`userData/settings.squid.json`), and change it to the name of the theme you created or downloaded. Restart the app to see the changes. +Then, to apply your theme, locate the key `currentTheme` in the settings file (`userData/settings.squid.json`), and change it to the name of the theme you created or downloaded. You dont't need to restart, the app watch for changes in the settings file and automatically process the changes. + +Where is the settings file, more options are coming : +``` +{ + "theme": { + ... + }, + "cursor": { + "style": "block", + "blink": true + }, + "font": { + "size": 13, + "family": "\"Fira Code\", \"Consolas\", monospace" + }, + "backgroundImage": { + "path": "", + "opacity": 1 + }, + "bash": "", + "currentTheme": "default", + "fastScrollModifier": "shift", + "shortcuts": [ + { + "keys": "CommandOrControl+Shift+T", + "action": "pane:open" + }, + { + "keys": "CommandOrControl+Shift+W", + "action": "pane:close" + }, + { + "keys": "CommandOrControl+Tab", + "action": "pane:switch" + } + ] +} +``` diff --git a/app/components/Pane.ts b/app/components/Pane.ts index 8a5a09d..abd2249 100644 --- a/app/components/Pane.ts +++ b/app/components/Pane.ts @@ -1,14 +1,38 @@ import Settings, { ISettings } from '../settings/Settings'; -export default class Pane { +export default abstract class Pane { protected settings: Settings; protected id: number; + protected opened: boolean; - constructor(settings: Settings, id: number) { + protected constructor(settings: Settings, id: number) { this.settings = settings; this.id = id; + this.opened = false; + } + + abstract adapt(); + abstract onData(data: string); + abstract applySettings(settings: ISettings); + abstract fit(); + + /** + * Return if the pane is opened or in the index + * @return If the pane is opened + */ + isOpened(): boolean { + + return this.opened; + } + + /** + * Set the pane to opened + */ + setOpened() { + + this.opened = true; } /** diff --git a/app/components/Panes.ts b/app/components/Panes.ts index d967379..18d7da7 100644 --- a/app/components/Panes.ts +++ b/app/components/Panes.ts @@ -2,21 +2,23 @@ import { remote } from 'electron'; import Settings, { ISettings } from '../settings/Settings'; import * as dragula from 'dragula'; import { Drake } from 'dragula'; -import SquidTerminal from "./SquidTerminal"; +import SquidTerminal from './SquidTerminal'; import * as os from 'os'; -import HostHandler, {IHost} from '../hosts/HostHandler'; -import { createHostElement } from '../hosts/hostHelper'; +import HostHandler, { IHost } from '../hosts/HostHandler'; +import { addListeners, createHostElement, openSide, closeSide, provideHost } from '../hosts/hostHelper'; import SSHTerminal from './SSHTerminal'; +import Pane from './Pane'; export default class Panes { private settings: Settings; - private panes: SquidTerminal[]; - private currentPane: SquidTerminal; + private panes: Pane[]; + private currentPane: Pane; private node: HTMLElement; private drag: Drake; private index: HTMLElement; private hostHandler: HostHandler; + private currentHost: IHost; constructor(settings: Settings) { @@ -39,10 +41,64 @@ export default class Panes { const node = document.getElementById('hosts-container'); this.hostHandler.on('keytarLoaded', () => this.hostHandler.getHosts().forEach(current => { - const element = createHostElement(current, (event: MouseEvent) => this.open(event, null, current)); + node.appendChild(createHostElement(current, () => { - node.appendChild(element); + this.currentHost = current; + openSide('edit', current); + + },(event: MouseEvent) => this.open(event, null, current))); })); + + document.getElementById('valid-create-host').addEventListener('click', (event) => { + + event.preventDefault(); + closeSide('create'); + + const host: IHost = provideHost('create'); + + this.hostHandler.addHost(host, () => { + + node.appendChild(createHostElement(host, () => { + + this.currentHost = host; + openSide('edit', host); + + },(event: MouseEvent) => this.open(event, null, host))); + }); + }); + + document.getElementById('valid-edit-host').addEventListener('click', (event) => { + + event.preventDefault(); + closeSide('edit'); + + const newHost: IHost = provideHost('edit'); + + this.hostHandler.editHost(this.currentHost, newHost, () => { + + document.getElementById('host-' + this.currentHost.name).remove(); + + node.appendChild(createHostElement(newHost, () => { + + this.currentHost = newHost; + openSide('edit', newHost); + + },(event: MouseEvent) => this.open(event, null, newHost))); + }); + }); + + document.getElementById('delete-host').addEventListener('click', (event) => { + + event.preventDefault(); + closeSide('edit'); + + this.hostHandler.removeHost(this.currentHost, () => { + + document.getElementById('host-' + this.currentHost.name).remove(); + }); + }); + + addListeners(); } /** @@ -53,17 +109,30 @@ export default class Panes { */ open(event: MouseEvent, path: string, host?: IHost) { - event.preventDefault(); + if(event) + event.preventDefault(); this.hideIndex(); if(host) { - this.currentPane = new SSHTerminal(this.settings, this.currentPane.getId(), host); - (this.currentPane as SSHTerminal).open(); + const ssh = new SSHTerminal(this.settings, this.currentPane.getId(), host); + + this.panes.splice(this.panes.indexOf(this.currentPane), 1, ssh); + this.currentPane = ssh; + + ssh.open(); + + this.setTabTitle(this.currentPane, host.name); - } else - this.currentPane.open(path); + } else { + + (this.currentPane as SquidTerminal).open(path); + this.setTabTitle(this.currentPane, (path === '' ? 'Custom bash' : path)); + } + + this.currentPane.setOpened(); + this.currentPane.adapt(); } /** @@ -80,7 +149,8 @@ export default class Panes { // Add the element to the DOM this.node.appendChild(terminalElement); - let pane = new SquidTerminal(this.settings, id); + // Open a squid terminal by default + const pane = new SquidTerminal(this.settings, id); this.addPane(pane); @@ -136,7 +206,7 @@ export default class Panes { this.togglePane(document.querySelector('.tab.active'), this.getNextPane()); } - getNextPane(): SquidTerminal { + getNextPane(): Pane { const currentIndex = this.panes.indexOf(this.currentPane); const toIndex = (currentIndex == this.panes.length - 1) ? 0 : currentIndex + 1; @@ -148,7 +218,7 @@ export default class Panes { * Add a new pane and register it to default * @param pane */ - addPane(pane: SquidTerminal) { + addPane(pane: Pane) { this.currentPane = pane; this.panes.push(pane); @@ -160,27 +230,47 @@ export default class Panes { * Create a new tab thanks to a pane * @param pane */ - createTab(pane: SquidTerminal) { + createTab(pane: Pane) { const node = document.getElementById('tabs-container'); const tabElement = document.createElement('div'); - tabElement.innerText = 'Terminal'; + tabElement.innerText = 'Squid'; tabElement.className = 'tab'; tabElement.id = 'tab-' + pane.getId(); node.appendChild(tabElement); - tabElement.addEventListener('click', () => this.togglePane(tabElement, pane)); + tabElement.addEventListener('click', () => this.togglePane(tabElement)); + } + + /** + * Set a tab title + * @param pane + * @param title + */ + setTabTitle(pane: Pane, title: string) { + + document.getElementById('tab-' + pane.getId()).innerText = title; } /** * Toggle the panes and tabs * @param tab - * @param pane + * @param desiredPane? */ - togglePane(tab: HTMLElement, pane: SquidTerminal) { + togglePane(tab: HTMLElement, desiredPane?: Pane) { + + let pane: Pane; + + if(desiredPane) + pane = desiredPane; + else + pane = this.panes.find(current => 'tab-' + current.getId() === tab.id); tab.classList.add('active'); + closeSide('create'); + closeSide('edit'); + // Old pane document.getElementById(this.currentPane.getPrefixId()).classList.add('hidden'); document.getElementById('tab-' + this.currentPane.getId()).classList.remove('active'); @@ -220,7 +310,7 @@ export default class Panes { * Get the current opened pane * @return The current pane */ - getCurrentPane(): SquidTerminal { + getCurrentPane(): Pane { return this.currentPane; } @@ -229,7 +319,7 @@ export default class Panes { * Get all the panes * @return The panes */ - getPanes(): SquidTerminal[] { + getPanes(): Pane[] { return this.panes; } diff --git a/app/components/SSHTerminal.ts b/app/components/SSHTerminal.ts index fc37c14..6390134 100644 --- a/app/components/SSHTerminal.ts +++ b/app/components/SSHTerminal.ts @@ -1,11 +1,19 @@ -import SquidTerminal from './SquidTerminal'; -import { Client, ClientChannel } from 'ssh2'; -import Settings from '../settings/Settings'; +import { Client, ClientChannel, ClientErrorExtensions, ConnectConfig } from 'ssh2'; +import Settings, {ISettings, ITheme} from '../settings/Settings'; import { remote } from 'electron'; import { IHost } from '../hosts/HostHandler'; - -export default class SSHTerminal extends SquidTerminal { - +import Pane from './Pane'; +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { loadTheme } from '../settings/handler'; +import { WebLinksAddon } from 'xterm-addon-web-links'; +import { LigaturesAddon } from 'xterm-addon-ligatures'; +import * as fs from 'fs'; + +export default class SSHTerminal extends Pane { + + protected xterm: Terminal; + private fitAddon: FitAddon; private connection: Client; private host: IHost; @@ -31,28 +39,126 @@ export default class SSHTerminal extends SquidTerminal { this.applyAddons(); - this.xterm.onResize((data: {cols: number, rows: number}) => this.fit()); - window.onresize = () => this.fit(); - this.opened = true; - this.adapt(); this.setupConnection(); } + /** + * Build the terminal object + * @return The terminal + */ + buildTerminal(): Terminal { + + return new Terminal({ + + cursorBlink: this.settings.get('cursor').blink, + cursorStyle: this.settings.get('cursor').style, + fontSize: this.settings.get('font').size, + fontFamily: this.settings.get('font').family, + fastScrollModifier: this.settings.get('fastScrollModifier') + }); + } + + /** + * Apply the theme + */ + applyTheme() { + + const currentTheme = this.settings.get('currentTheme'); + let theme = this.settings.get('theme'); + + if(currentTheme != theme.name) + theme = loadTheme(currentTheme); + + this.xterm.setOption('theme', theme); + } + + /** + * Apply a new theme + * @param theme + */ + applyNewTheme(theme: ITheme) { + + this.xterm.setOption('theme', theme); + } + + /** + * Apply the addons needed + */ + applyAddons() { + + this.xterm.loadAddon(this.fitAddon = new FitAddon()); + this.xterm.loadAddon(new WebLinksAddon()); + this.xterm.loadAddon(new LigaturesAddon()); + } + + /** + * Apply the new settings + * @param settings + */ + applySettings(settings: ISettings) { + + this.applyNewTheme(loadTheme(settings.currentTheme)); + + this.xterm.setOption('cursorBlink', settings.cursor.blink); + this.xterm.setOption('cursorStyle', settings.cursor.style); + this.xterm.setOption('fontSize', settings.font.size); + this.xterm.setOption('fontFamily', settings.font.family); + } + + /** + * Called when the pane is created or focused + */ + adapt() { + + if(this.isOpened()) { + + this.xterm.focus(); + this.fit(); + } + } + + /** + * Fit the terminal + */ + fit() { + + if(this.isOpened()) + this.fitAddon.fit(); + } + /** * Setup the SSH connection */ setupConnection() { + this.connection.on('error', (error: Error & ClientErrorExtensions) => this.xterm.write(error.message)); + + const config: ConnectConfig = { + host: this.host.ip, + port: this.host.port, + username: this.host.username, + password: this.host.password + }; + + if(fs.existsSync(this.host.privateKey)) + config.privateKey = fs.readFileSync(this.host.privateKey); + this.connection.on('ready', () => { - this.connection.shell((error: Error | undefined, stream: ClientChannel) => { + this.connection.shell({rows: this.xterm.rows, cols: this.xterm.cols}, (error: Error | undefined, stream: ClientChannel) => { if(error) throw error; + this.xterm.onResize((data: {cols: number, rows: number}) => { + + this.fit(); + stream.setWindow(data.rows, data.cols, window.innerHeight, window.innerWidth); + }); + stream.on('close', () => { this.connection.end(); @@ -67,17 +173,9 @@ export default class SSHTerminal extends SquidTerminal { this.xterm.write(data.toString()); }); - this.xterm.onKey((data: {key: string, domEvent: KeyboardEvent }) => { - - stream.write(data.key); - }); + this.xterm.onKey((data: {key: string, domEvent: KeyboardEvent }) => stream.write(data.key)); }); - }).connect({ - host: this.host.ip, - port: this.host.port, - username: this.host.username, - password: this.host.password, - }); + }).connect(config); } /** diff --git a/app/components/SquidTerminal.ts b/app/components/SquidTerminal.ts index c3d6f0c..b38c39c 100644 --- a/app/components/SquidTerminal.ts +++ b/app/components/SquidTerminal.ts @@ -14,13 +14,10 @@ export default class SquidTerminal extends Pane { protected xterm: Terminal; private ptyProcess: IPty; private fitAddon: FitAddon; - protected opened: boolean; constructor(settings: Settings, id: number) { super(settings, id); - - this.opened = false; } /** @@ -46,8 +43,6 @@ export default class SquidTerminal extends Pane { window.onresize = () => this.fit(); - this.opened = true; - this.adapt(); } @@ -63,15 +58,6 @@ export default class SquidTerminal extends Pane { } } - /** - * Return if the pane is opened or in the index - * @return If the pane is opened - */ - isOpened(): boolean { - - return this.opened; - } - /** * Build the terminal object * @return The terminal diff --git a/app/hosts/HostHandler.ts b/app/hosts/HostHandler.ts index 6c621e0..28935f5 100644 --- a/app/hosts/HostHandler.ts +++ b/app/hosts/HostHandler.ts @@ -47,7 +47,7 @@ export default class HostHandler extends events.EventEmitter { */ loadKeytar(done: (hosts: IHost[]) => void) { - let hosts = []; + const hosts = []; let count = 0; this.hostNames.forEach(current => { @@ -74,12 +74,41 @@ export default class HostHandler extends events.EventEmitter { const name = host.name; - if(this.hostNames.includes(host.name)) - return; + if(!this.hostNames.includes(host.name)) + this.hostNames.push(name); - this.hostNames.push(name); + keytar.setPassword('squid', name, JSON.stringify(host)).then(() => { - keytar.setPassword('squid', name, JSON.stringify(host)).then(() => done()); + this.save(); + done(); + }); + } + + /** + * Remove a host from keytar + * @param host + * @param done + */ + removeHost(host: IHost, done: () => void) { + + this.hostNames.slice(this.hostNames.indexOf(host.name), 1); + + keytar.deletePassword('squid', host.name).then(() => { + + this.save(); + done(); + }); + } + + /** + * Edit a host credentials + * @param oldHost + * @param newHost + * @param done + */ + editHost(oldHost: IHost, newHost: IHost, done: () => void) { + + this.removeHost(oldHost, () => this.addHost(newHost, () => done())); } /** @@ -93,8 +122,6 @@ export default class HostHandler extends events.EventEmitter { this.hosts.forEach(current => { - console.log(current.name + ' ' + name); - if(current.name == name) host = current; }); @@ -136,4 +163,5 @@ export interface IHost { port: number; username: string; password: string; + privateKey: string; } diff --git a/app/hosts/hostHelper.ts b/app/hosts/hostHelper.ts index b4b62e3..b785200 100644 --- a/app/hosts/hostHelper.ts +++ b/app/hosts/hostHelper.ts @@ -1,31 +1,137 @@ import { IHost } from './HostHandler'; -export function createHostElement(current: IHost, onClick: (event: MouseEvent) => void): HTMLElement { +type side = 'create' | 'edit'; + +/** + * Add listeners + */ +export function addListeners() { + + document.getElementById('create-host').addEventListener('click', (event) => { + + event.preventDefault(); + openSide('create', { + name: '', + ip: '', + port: 22, + username: 'root', + password: '', + privateKey: '' + }); + }); + + document.getElementById('close-create-host').addEventListener('click', (event) => { + + event.preventDefault(); + closeSide('create'); + }); + + document.getElementById('close-edit-host').addEventListener('click', (event) => { + + event.preventDefault(); + closeSide('edit'); + }); + + document.getElementById('search-host').addEventListener('keyup', () => { + + const hosts: HTMLCollection = document.getElementById('hosts-container').children; + const searching = (document.getElementById('search-host') as HTMLInputElement).value; + + Array.from(hosts).forEach((current: HTMLElement) => { + + if(current.id.replace('host-', '').toLowerCase().includes(searching)) + current.style.display = 'flex'; + else + current.style.display = 'none'; + }) + }); +} + +export function openSide(type: side, host: IHost) { + + fillInputs(type, host); + document.getElementById(type + '-host-container').classList.add('visible'); +} + +export function closeSide(type: side) { + + document.getElementById(type + '-host-container').classList.remove('visible'); +} + +/** + * Get a host with filled inputs + * @return The IHost + */ +export function provideHost(type: side): IHost { + + return { + name: (document.getElementById(type + '-host-name') as HTMLInputElement).value, + ip: (document.getElementById(type + '-host-ip') as HTMLInputElement).value, + port: Number((document.getElementById(type + '-host-port') as HTMLInputElement).value), + username: (document.getElementById(type + '-host-username') as HTMLInputElement).value, + password: (document.getElementById(type + '-host-password') as HTMLInputElement).value, + privateKey: (document.getElementById(type + '-host-privateKey') as HTMLInputElement).value, + }; +} + +/** + * Fill the inputs with the host credentials + * @param type + * @param host + */ +export function fillInputs(type: side, host: IHost) { + + (document.getElementById(type + '-host-name') as HTMLInputElement).value = host.name; + (document.getElementById(type + '-host-ip') as HTMLInputElement).value = host.ip; + (document.getElementById(type + '-host-port') as HTMLInputElement).value = String(host.port); + (document.getElementById(type + '-host-username') as HTMLInputElement).value = host.username; + (document.getElementById(type + '-host-password') as HTMLInputElement).value = host.password; + (document.getElementById(type + '-host-privateKey') as HTMLInputElement).value = host.privateKey; +} + +/** + * Create a host element or replace the old one + * @param current + * @param infoClick + * @param sshClick + * @return The created DOM element + */ +export function createHostElement(current: IHost, infoClick: (event: MouseEvent) => void, sshClick: (event: MouseEvent) => void): HTMLElement { + + const old = document.getElementById('host-' + current.name); + + if(old) + old.remove(); const host = document.createElement('div'); host.className = 'host'; + host.id = 'host-' + current.name; const infoContainer = document.createElement('div'); - infoContainer.className = 'host-info-container'; + infoContainer.className = 'info'; const hostName = document.createElement('p'); - hostName.className = 'host-name'; + hostName.className = 'name'; hostName.innerText = current.name; const hostInfo = document.createElement('p'); - hostInfo.className = 'host-info'; + hostInfo.className = 'description'; hostInfo.innerText = current.username + '@' + current.ip + ':' + current.port; - const ssh = document.createElement('div'); - ssh.className = 'host-btn ssh'; + const buttons = document.createElement('div'); + buttons.className = 'buttons'; + const ssh = document.createElement('button'); + ssh.className = 'btn'; ssh.innerText = 'SSH'; - const sftp = document.createElement('div'); - sftp.className = 'host-btn sftp'; + const sftp = document.createElement('button'); + sftp.className = 'btn blue'; sftp.innerText = 'SFTP'; infoContainer.appendChild(hostName); infoContainer.appendChild(hostInfo); host.appendChild(infoContainer); - host.appendChild(ssh); - host.appendChild(sftp); + host.appendChild(buttons); + buttons.appendChild(ssh); + buttons.appendChild(sftp); - ssh.addEventListener('click', (event: MouseEvent) => onClick(event)); + infoContainer.addEventListener('click', (event: MouseEvent) => infoClick(event)); + ssh.addEventListener('click', (event: MouseEvent) => sshClick(event)); return host; } diff --git a/app/renderer.ts b/app/renderer.ts index 4745fed..1995f48 100644 --- a/app/renderer.ts +++ b/app/renderer.ts @@ -3,60 +3,103 @@ import { watchForChanges } from './settings/handler'; import { ISettings } from './settings/Settings'; import Panes from './components/Panes'; import Settings from './settings/Settings'; -import SquidTerminal from './components/SquidTerminal'; -const settings = new Settings(); -const panes = new Panes(settings); -// Open a new tab by default -panes.openPane(); +new class Renderer { -watchForChanges((newFile: ISettings) => { + private readonly settings: Settings; + private panes: Panes; + private updateElement: HTMLElement; - panes.setSettings(newFile); + constructor() { - panes.getPanes().forEach(current => { + this.settings = new Settings(); + this.panes = new Panes(this.settings); + this.updateElement = document.getElementById('update-status'); - current.applySettings(newFile); - }); -}); + this.openPane(); + this.watchForChanges(); + this.setupListeners(); + } + + /** + * Watch for changes on the settings file + */ + watchForChanges() { -ipcRenderer.on('shortcuts', (event, message) => { + if(this.settings.exists()) { - switch (message) { + watchForChanges((newFile: ISettings) => { - case 'paste': - panes.getCurrentPane().onData(clipboard.readText()); - break; + this.panes.setSettings(newFile); + this.panes.getPanes().forEach(current => current.applySettings(newFile)); + }); + } + } - case 'pane:open': - panes.openPane(); - break; + /** + * Setup the listeners + */ + setupListeners() { - case 'pane:close': - panes.closePane(); - break; + // Shortcuts + ipcRenderer.on('shortcuts', (event, message) => { - case 'pane:switch': - panes.switchPane(); - break; - } -}); + switch (message) { + + case 'paste': + this.panes.getCurrentPane().onData(clipboard.readText()); + break; + + case 'pane:open': + this.panes.openPane(); + break; + + case 'pane:close': + this.panes.closePane(); + break; + + case 'pane:switch': + this.panes.switchPane(); + break; + } + }); -ipcRenderer.on('update:ready', () => { + // Updates + ipcRenderer.on('update:latest', () => { - const node = document.getElementById('panel-container'); - const updateElement = document.createElement('div'); - updateElement.innerText = 'Restart to apply update'; - updateElement.className = 'apply-update'; - node.appendChild(updateElement); + this.updateElement.innerText = 'You are using the latest version'; + this.updateElement.className = 'uptodate-update'; + }); - updateElement.addEventListener('click', () => ipcRenderer.send('update:apply')); -}); + ipcRenderer.on('update:download', (event, args) => { -ipcRenderer.on('resize', () => panes.getPanes().forEach(current => current.fit())); + this.updateElement.innerText = 'Downloading latest version... (' + args + '%)'; + this.updateElement.className = 'downloading-update'; + }); -document.addEventListener('contextmenu', (event) => { + ipcRenderer.on('update:ready', () => { - event.preventDefault(); - ipcRenderer.send('contextmenu'); -}); + this.updateElement.innerText = 'Restart to apply update'; + this.updateElement.className = 'apply-update'; + this.updateElement.addEventListener('click', () => ipcRenderer.send('update:apply')); + }); + + // Resize + ipcRenderer.on('resize', () => this.panes.getPanes().forEach(current => current.fit())); + + // Right-click listener + document.addEventListener('contextmenu', (event) => { + + event.preventDefault(); + ipcRenderer.send('contextmenu'); + }); + } + + /** + * Open a pane by default + */ + openPane() { + + this.panes.openPane(); + } +} diff --git a/app/update/Updater.ts b/app/update/Updater.ts index 0d27ed6..d7a48f1 100644 --- a/app/update/Updater.ts +++ b/app/update/Updater.ts @@ -9,7 +9,6 @@ const electronIsDev = require('electron-is-dev'); export default class Updater { private window: Window; - private progress: number; constructor(window: Window) { @@ -31,8 +30,8 @@ export default class Updater { autoUpdater.checkForUpdates(); - autoUpdater.on('update-available', () => console.log('update available')); - autoUpdater.on('download-progress', (info) => this.progress = info.percent); + autoUpdater.on('update-not-available', () => this.window.getWindow().webContents.send('update:latest')); + autoUpdater.on('download-progress', (info) => this.window.getWindow().webContents.send('update:download', info.percent)); autoUpdater.on('update-downloaded', () => this.window.getWindow().webContents.send('update:ready')); ipcMain.on('update:apply', () => autoUpdater.quitAndInstall()); diff --git a/package-lock.json b/package-lock.json index 804891e..89e73e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "squid", - "version": "1.0.3", + "version": "1.0.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -615,6 +615,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.6.0.tgz", "integrity": "sha512-WTDhTUVrm7zkFyd6Qn7AXgmWifjpZ/fYnEdV3XCOIDMNNb/KPddBTbQ8bUlxxVeuOYlhGpcLUypG+4USdGL1ww==", + "dev": true, "requires": { "debug": "^4.1.1", "sax": "^1.2.4" @@ -624,6 +625,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -1495,12 +1497,12 @@ } }, "electron-updater": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.2.2.tgz", - "integrity": "sha512-e/OZhr5tLW0GcgmpR5wD0ImxgKMa8pPoNWRcwRyMzTL9pGej7+ORp0t9DtI5ZBHUbObIoEbrk+6EDGUGtJf+aA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.2.4.tgz", + "integrity": "sha512-iqN0uoP2+Nkiljp/o4DzZVeSpOOCMtP8+pqL5/qDI+1/ARW99T2TFcpRrPwX4dntowNV7X5T19aKFLK3+9AdkA==", "requires": { "@types/semver": "^7.1.0", - "builder-util-runtime": "8.6.0", + "builder-util-runtime": "8.6.1", "fs-extra": "^8.1.0", "js-yaml": "^3.13.1", "lazy-val": "^1.0.4", @@ -1509,6 +1511,23 @@ "semver": "^7.1.3" }, "dependencies": { + "builder-util-runtime": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.6.1.tgz", + "integrity": "sha512-gwIUtMaICmc+e2EC3u3byXcwCyfhtG40LJRNnGfs8AYqacKl4ZLP50ab+uDttn7QAXe0LfMAuKz9v8bCODV0yg==", + "requires": { + "debug": "^4.1.1", + "sax": "^1.2.4" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -3173,17 +3192,17 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssh2": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.7.tgz", - "integrity": "sha512-/u1BO12kb0lDVxJXejWB9pxyF3/ncgRqI9vPCZuPzo05pdNDzqUeQRavScwSPsfMGK+5H/VRqp1IierIx0Bcxw==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.8.tgz", + "integrity": "sha512-egJVQkf3sbjECTY6rCeg8rgV/fab6S/7E5kpYqHT3Fe/YpfJbLYeA1qTcB2d+LRUUAjqKi7rlbfWkaP66YdpAQ==", "requires": { - "ssh2-streams": "~0.4.8" + "ssh2-streams": "~0.4.9" } }, "ssh2-streams": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.8.tgz", - "integrity": "sha512-auxXfgYySz2vYw7TMU7PK7vFI7EPvhvTH8/tZPgGaWocK4p/vwCMiV3icz9AEkb0R40kOKZtFtqYIxDJyJiytw==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", "requires": { "asn1": "~0.2.0", "bcrypt-pbkdf": "^1.0.2", @@ -3482,9 +3501,9 @@ } }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==" + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz", + "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==" }, "unique-string": { "version": "2.0.0", diff --git a/package.json b/package.json index 2ddf215..6c338a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "squid", - "version": "1.0.4", + "version": "1.0.8", "author": "QuiiBz", "productName": "Squid", "description": "A terminal for everyone", @@ -56,13 +56,13 @@ "dragula": "^3.7.2", "electron-is-dev": "^1.1.0", "electron-log": "^4.0.6", - "electron-updater": "^4.2.2", + "electron-updater": "^4.2.4", "keytar": "^5.3.0", "md5": "^2.2.1", "node-gyp": "^6.1.0", "node-pty": "^0.9.0", - "ssh2": "^0.8.7", - "typescript": "^3.7.5", + "typescript": "^3.8.2", + "ssh2": "^0.8.8", "xterm": "^4.4.0", "xterm-addon-fit": "^0.3.0", "xterm-addon-ligatures": "^0.2.1", diff --git a/ui/css/app.css b/ui/css/app.css index 1cc3f58..0298b7c 100644 --- a/ui/css/app.css +++ b/ui/css/app.css @@ -143,171 +143,6 @@ body { border: 1px dashed #FFFFFF; } -.apply-update { - - color: #22da6e; - font-size: 12px; - - position: absolute; - bottom: 5px; - right: 5px; - - z-index: 999; - - border: 1px solid #22da6e; - border-radius: 3px; - padding: 2px 5px; -} - -.apply-update:hover { - - cursor: pointer; -} - -.index { - - flex-direction: column; - align-items: center; - justify-content: space-around; - - width: 100vw; - - position: absolute; - top: 50%; - transform: translateY(-50%); -} - -.index h2 { - - font-size: 20px; - color: #ffffff; - font-weight: 100; - - margin: 50px 0 20px 0; -} - -.logo { - - width: 15%; -} - -.index-buttons { - - display: flex; - align-items: center; - flex-direction: row; -} - -.index-btn { - - font-family: 'Fira Code'; - font-size: 14px; - - background: none; - color: #646464; - - border: 1px solid #151515; - border-radius: 5px; - padding: 10px 20px; - - display: flex; - flex-direction: row; - align-items: center; - - margin: 0 10px; - - transition: .3s ease; -} - -.index-btn:hover { - - background-color: #151515; - color: #FFFFFF; - - cursor: pointer; -} - -.index-btn svg { - - width: 15px; - margin-right: 10px; -} - -.hosts { - - overflow-y: scroll; - max-height: 30vh; -} - -.host { - - display: flex; - flex-direction: row; - align-items: center; - - padding: 10px 20px; - - border: 1px solid #151515; - border-radius: 5px; -} - -.host-info-container { - - display: flex; - flex-direction: column; -} - -.host-name { - - font-size: 18px; - color: #ffffff; - margin: 0; -} - -.host-info { - - font-size: 12px; - color: #646464; - margin: 0; -} - -.host-btn { - - background: none; - color: #22da6e; - font-size: 14px; - - border: 1px solid #22da6e; - border-radius: 5px; - - padding: 5px 10px; - text-shadow: 0 0 1px #22da6e; - margin-left: 50px; - - transition: .3s ease; -} - -.host-btn:hover { - - text-shadow: 0 0 10px #22da6e; - cursor: pointer; -} - -.host-btn.sftp { - - color: #82aaff; - border: 1px solid #82aaff; - text-shadow: 0 0 1px #82aaff; - - margin-left: 10px; -} - -.host-btn.sftp:hover { - - text-shadow: 0 0 10px #82aaff; - cursor: not-allowed; -} - ::-webkit-scrollbar { background: none; diff --git a/ui/css/index.css b/ui/css/index.css new file mode 100644 index 0000000..8251445 --- /dev/null +++ b/ui/css/index.css @@ -0,0 +1,242 @@ +.index { + + flex-direction: column; + justify-content: flex-start; + margin: auto; + height: 100vh; +} + +.logo-container { + + height: 20vh; + display: flex; + justify-content: center; + margin: 5% 0; +} + +.logo { + + width: 200px; + height: 200px; +} + +.main-container { + + display: flex; + flex-direction: row; + align-items: center; + margin: 0 auto; +} + +.terminals, .hosts { + + width: 35vw; + max-width: 500px; + height: auto; + margin: 0 50px; +} + +.index .title { + + display: flex; + flex-direction: row; + justify-content: space-between; + border-bottom: 1px solid #151515; + margin-bottom: 25px; +} + +.index .title h1 { + + font-size: 25px; + font-weight: normal; + margin: 0; + color: #ffffff; +} + +.index .title .search { + + display: flex; + flex-direction: row; +} + +.index .title .search input { + + background: none; + border: none; + color: #646464; + text-align: right; + margin-right: 20px; + font-size: 14px; +} + +.index .title .button { + + background: none; + border: none; + padding: 0; +} + +.index .title .button:hover { + + cursor: pointer; +} + +.index .scroll-container { + + scroll-snap-type: y mandatory; + overflow-y: scroll; + height: calc(50vh - 50px); +} + +.host { + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 10px; + background: #151515; + border-radius: 5px; + margin-top: 10px; + scroll-snap-align: start; +} + +.host .info .name { + + color: #ffffff; + font-size: 18px; + margin: 0; +} + +.host .info .description { + + color: #646464; + font-size: 12px; + margin: 0; +} + +.host .info :hover { + + cursor: pointer; +} + +.btn { + + background: none; + border: 1px solid #22da6e; + padding: 5px 10px; + border-radius: 5px; + color: #22da6e; + font-size: 16px; + margin: 0 5px; + text-shadow: 0 0 1px #22da6e; + transition: .3s ease; +} + +.btn.blue { + + border: 1px solid #82aaff; + color: #82aaff; + text-shadow: 0 0 1px #82aaff; +} + +.btn.red { + + border: 1px solid #EF5350; + color: #EF5350; + text-shadow: 0 0 1px #EF5350; +} + +.btn:hover { + + text-shadow: 0 0 5px #22da6e; + cursor: pointer; +} + +.btn.blue:hover { + + text-shadow: 0 0 5px #82aaff; +} + +.btn.red:hover { + + text-shadow: 0 0 5px #EF5350; +} + +#add-host { + + position: absolute; + left: calc(30% - 60px); +} + +.create-host-container, .edit-host-container { + + display: none; + position: absolute; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + flex-direction: row-reverse; +} + +.create-host-container.visible, .edit-host-container.visible { + + display: flex; +} + +.side { + + height: 100vh; + width: 350px; + background: #0F0F0F; + padding: 20px; + display: flex; + flex-direction: column; +} + +.side input { + + margin: 10px; + background: #151515; + border: none; + border-radius: 5px; + padding: 10px; + font-size: 16px; + color: #ffffff; +} + +#create-host { + + position: absolute; + right: calc(50% - 22px); +} + +#valid-create-host, #valid-edit-host, #delete-host { + + width: 50%; + margin: 20px auto; +} + +.status-bar { + + position: absolute; + left: 0; + bottom: 0; + display: flex; + flex-direction: row; + padding: 5px; + justify-content: space-between; + width: calc(100vw - 10px); + color: #646464; + font-size: 12px; +} + +.apply-update { + + color: #22da6e; +} + +.apply-update:hover { + + cursor: pointer; +} diff --git a/ui/icons/close.png b/ui/icons/close.png new file mode 100644 index 0000000..00b9cee Binary files /dev/null and b/ui/icons/close.png differ diff --git a/ui/icons/search.png b/ui/icons/search.png new file mode 100644 index 0000000..daaf01f Binary files /dev/null and b/ui/icons/search.png differ diff --git a/ui/images/logo.png b/ui/images/logo.png new file mode 100644 index 0000000..499d9c1 Binary files /dev/null and b/ui/images/logo.png differ diff --git a/ui/index.html b/ui/index.html index 1334756..245d4aa 100644 --- a/ui/index.html +++ b/ui/index.html @@ -8,6 +8,7 @@ +