From 06a5e78744ff5bd28876abc00472ecfe5b7dc846 Mon Sep 17 00:00:00 2001 From: Nicolas Ramz Date: Mon, 21 Nov 2022 14:50:18 +0100 Subject: [PATCH] Fix depreciated keycode (#309) - use event.key instead of drepreciated event.keyCode & event.which - add new `constants/keys` constant to avoid typos Also made classnames/classNames import name consistant across the codebase. --- e2e/cypress.config.ts | 42 ++- e2e/cypress/e2e/filetable.spec.ts | 334 +++++++++++----------- e2e/cypress/e2e/menu.accelerators.spec.ts | 2 +- e2e/tsconfig.json | 6 +- src/components/App.tsx | 9 +- src/components/AppAlert.tsx | 5 +- src/components/Downloads.tsx | 4 +- src/components/FileTable.tsx | 85 +++--- src/components/Log.tsx | 23 +- src/components/Nav.tsx | 11 +- src/components/Statusbar.tsx | 7 +- src/components/Toolbar.tsx | 20 +- src/components/dialogs/LoginDialog.tsx | 6 +- src/components/dialogs/MakedirDialog.tsx | 14 +- src/constants/keys.ts | 16 ++ src/electron/osSupport.ts | 69 +++-- src/state/clipboardState.ts | 4 +- src/utils/platform.ts | 44 +-- 18 files changed, 370 insertions(+), 331 deletions(-) create mode 100644 src/constants/keys.ts diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts index 7b965a14..58999eca 100644 --- a/e2e/cypress.config.ts +++ b/e2e/cypress.config.ts @@ -1,10 +1,46 @@ -import { defineConfig } from 'cypress'; +import { defineConfig } from 'cypress' +import webpackPreprocessor from '@cypress/webpack-preprocessor' +import { ResolveOptions } from 'webpack' +import { resolve } from 'path' export default defineConfig({ e2e: { setupNodeEvents(on, config) { - // implement node event listeners here + // FIXME: this doesn't work: the exported default config doesn't + // seem to be the one used internally by Cypress. + // So if we use this one, this breaks TypeScript support as the + // default config doesn't include it: + // { + // mode: 'development', + // module: { + // rules: [ + // { + // test: /\.jsx?$/, + // exclude: [/node_modules/], + // use: [ + // { + // loader: 'babel-loader', + // options: { + // presets: ['@babel/preset-env'], + // }, + // }, + // ], + // }, + // ], + // }, + // } + // const options = webpackPreprocessor.defaultOptions.webpackOptions + // + // options.resolve = { + // alias: { + // $src: resolve(__dirname, '../src'), + // }, + // } as ResolveOptions + // + // See: https://github.com/cypress-io/cypress/discussions/24751 + // + // on('file:preprocessor', webpackPreprocessor(webpackPreprocessor.defaultOptions)) }, specPattern: 'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}', }, -}); +}) diff --git a/e2e/cypress/e2e/filetable.spec.ts b/e2e/cypress/e2e/filetable.spec.ts index cf28222f..c5d06574 100644 --- a/e2e/cypress/e2e/filetable.spec.ts +++ b/e2e/cypress/e2e/filetable.spec.ts @@ -1,6 +1,9 @@ /// -import { MOD_KEY } from '../support/constants'; +import { MOD_KEY } from '../support/constants' +// FIXME: define & use $src alias when we'll figure out how to do it +// See: https://github.com/cypress-io/cypress/discussions/24751 +import Keys from '../../../src/constants/keys' const TYPE_ICONS: { [key: string]: string } = { img: 'media', @@ -12,107 +15,97 @@ const TYPE_ICONS: { [key: string]: string } = { doc: 'align-left', cod: 'code', dir: 'folder-close', -}; - -enum KEYS { - Backspace = 8, - Enter = 13, - Escape = 27, - Down = 40, - Up = 38, - PageDown = 34, - PageUp = 33, } describe('filetable', () => { before(() => { - cy.visit('http://127.0.0.1:8080'); - }); + cy.visit('http://127.0.0.1:8080') + }) beforeEach(() => { - createStubs(); - resetSelection(); - }); + createStubs() + resetSelection() + }) - let files: any; + let files: any const stubs: any = { openDirectory: [], openFile: [], - }; + } function resetSelection() { cy.window().then((win) => { win.appState.winStates[0].views.forEach((view: any) => { view.caches.forEach((cache: any) => { - cache.reset(); - }); - }); - }); + cache.reset() + }) + }) + }) } function createStubs() { - stubs.openFile = []; - stubs.openDirectory = []; - stubs.openParentDirectory = []; - stubs.rename = []; + stubs.openFile = [] + stubs.openDirectory = [] + stubs.openParentDirectory = [] + stubs.rename = [] cy.window().then((win) => { - const appState = win.appState; - const views = appState.winStates[0].views; - cy.spy(appState, 'updateSelection').as('updateSelection'); - let count = 0; + const appState = win.appState + const views = appState.winStates[0].views + cy.spy(appState, 'updateSelection').as('updateSelection') + let count = 0 for (const view of views) { for (const cache of view.caches) { cy.stub(cache, 'openFile') .as('stub_openFile' + count) - .resolves(); + .resolves() cy.stub(cache, 'openDirectory') .as('stub_openDirectory' + count) - .resolves(); + .resolves() cy.stub(cache, 'openParentDirectory') .as('stub_openParentDirectory' + count) - .resolves(); + .resolves() cy.stub(cache, 'rename') .as('stub_rename' + count) - .resolves(); + .resolves() // this will be called but we don't care - cy.stub(cache, 'isRoot').returns(false); + cy.stub(cache, 'isRoot').returns(false) - count++; + count++ } } - }); + }) } beforeEach(() => { - cy.get('#view_0 [data-cy-path]').type('/{enter}').focus().blur(); + cy.get('#view_0 [data-cy-path]').type('/{enter}').focus().blur() // load files cy.CDAndList(0, '/').then((array: any) => { - files = array; - }); - }); + files = array + }) + }) describe('initial content', () => { it('should display files if cache is not empty', () => { // since we use a virtualized component which only displays visible rows // we cannot simply compare the number of rows to files.length - cy.get('#view_0 [data-cy-file]').its('length').should('be.gt', 0); - }); + cy.get('#view_0 [data-cy-file]').its('length').should('be.gt', 0) + }) it('should use correct icons for each file type', () => { - cy.get('#view_0 [data-cy-file] .file-label').as('rows'); + cy.get('#view_0 [data-cy-file] .file-label').as('rows') files.forEach((file: any) => { - const name = file.fullname; - const icon = (file.isDir && TYPE_ICONS['dir']) || TYPE_ICONS[file.type]; - cy.get('@rows').contains(name).prev().should('have.attr', 'icon').and('eq', icon); - }); - }); + const name = file.fullname + const icon = (file.isDir && TYPE_ICONS['dir']) || TYPE_ICONS[file.type] + cy.get('@rows').contains(name).prev().should('have.attr', 'icon').and('eq', icon) + }) + }) it('should show folders first', () => { // check that the first two elements are the folders of our files list @@ -120,205 +113,205 @@ describe('filetable', () => { .first() .prev() .should('have.attr', 'icon') - .and('eq', TYPE_ICONS['dir']); + .and('eq', TYPE_ICONS['dir']) cy.get('#view_0 [data-cy-file] .file-label') .eq(1) .prev() .should('have.attr', 'icon') - .and('eq', TYPE_ICONS['dir']); - }); - }); + .and('eq', TYPE_ICONS['dir']) + }) + }) describe('mouse navigation', () => { it('should select an element when clicking on it', () => { - cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected'); - }); + cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected') + }) it('should remove element from selection if already selected when pressing click + shift', () => { - cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected'); + cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected') cy.get('body') .type('{shift}', { release: false }) .get('#view_0 [data-cy-file]:first') .click() - .should('not.have.class', 'selected'); - }); + .should('not.have.class', 'selected') + }) it('should add element to selection if not selected when pressing click + shift', () => { - cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected'); + cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected') cy.get('body') .type('{shift}', { release: false }) .get('#view_0 [data-cy-file]:eq(1)') .click() - .should('have.class', 'selected'); + .should('have.class', 'selected') - cy.get('#view_0 [data-cy-file]:first').should('have.class', 'selected'); - }); + cy.get('#view_0 [data-cy-file]:first').should('have.class', 'selected') + }) it('should call openDirectory when double-clicking on folder', () => { - cy.get('#view_0 [data-cy-file]:first').dblclick(); + cy.get('#view_0 [data-cy-file]:first').dblclick() - cy.get('@stub_openDirectory0').should('be.called'); - }); + cy.get('@stub_openDirectory0').should('be.called') + }) it('should call openFile when clicking on a file', () => { - cy.get('#view_0 [data-cy-file]:eq(5)').dblclick(); + cy.get('#view_0 [data-cy-file]:eq(5)').dblclick() - cy.get('@stub_openFile0').should('be.called'); - }); + cy.get('@stub_openFile0').should('be.called') + }) it('should unselect all files when clicking on empty grid area', () => { // refresh file list but only keep last file from fixture // only keep last file - cy.get('#view_0 [data-cy-path]').type('/{enter}').focus().blur(); + cy.get('#view_0 [data-cy-path]').type('/{enter}').focus().blur() - cy.CDAndList(0, '/', -1); + cy.CDAndList(0, '/', -1) - cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected'); + cy.get('#view_0 [data-cy-file]:first').click().should('have.class', 'selected') - cy.get('#view_0 .fileListSizerWrapper').click('bottom'); + cy.get('#view_0 .fileListSizerWrapper').click('bottom') - cy.get('#view_0 [data-cy-file].selected').should('not.exist'); - }); - }); + cy.get('#view_0 [data-cy-file].selected').should('not.exist') + }) + }) describe('keyboard navigation', () => { it('should select the next element when pressing arrow down', () => { // one press: select the first one cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) + .trigger('keydown', { key: Keys.DOWN }) .find('[data-cy-file]') .first() - .should('have.class', 'selected'); + .should('have.class', 'selected') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) // another press, select the second one cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) + .trigger('keydown', { key: Keys.DOWN }) .find('[data-cy-file]') .eq(1) - .should('have.class', 'selected'); + .should('have.class', 'selected') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); - }); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) + }) it('should select the last element when rapidly pressing arrow down key', () => { cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) + .trigger('keydown', { key: Keys.DOWN }) .find('[data-cy-file]') .first() - .should('have.class', 'selected'); + .should('have.class', 'selected') for (let i = 0; i <= files.length; ++i) { - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Down }); + cy.get('#view_0').trigger('keydown', { key: Keys.DOWN }) } - cy.get('#view_0 [data-cy-file]').last().should('have.class', 'selected'); + cy.get('#view_0 [data-cy-file]').last().should('have.class', 'selected') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); - }); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) + }) it('should scroll down the table if needed when pressing arrow down key', () => { // first make sure the last element is hidden - cy.get('#view_0 [data-cy-file').last().should('not.be.visible'); + cy.get('#view_0 [data-cy-file').last().should('not.be.visible') // press arrow down key until the last item is selected: it should now be visible & selected for (let i = 0; i <= files.length; ++i) { - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Down }); + cy.get('#view_0').trigger('keydown', { key: Keys.DOWN }) } - cy.get('#view_0 [data-cy-file]').last().should('have.class', 'selected').and('be.visible'); + cy.get('#view_0 [data-cy-file]').last().should('have.class', 'selected').and('be.visible') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); - }); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) + }) it('should select the previous element when pressing arrow up key', () => { // be sure to be in a predictable state, without any selected element - cy.triggerHotkey(`${MOD_KEY}a`); - cy.triggerHotkey(`${MOD_KEY}i`); + cy.triggerHotkey(`${MOD_KEY}a`) + cy.triggerHotkey(`${MOD_KEY}i`) // activate the first then second element - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Down }); + cy.get('#view_0').trigger('keydown', { key: Keys.DOWN }) - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Down }); + cy.get('#view_0').trigger('keydown', { key: Keys.DOWN }) // activate previous element: should be the second one cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Up }) + .trigger('keydown', { key: Keys.UP }) .find('[data-cy-file]') .eq(1) - .should('have.class', 'selected'); + .should('have.class', 'selected') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); - }); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) + }) it('should scroll up the table if needed when pressing arrow up key', () => { // press arrow down key until the last item is selected: it should now be visible & selected for (let i = 0; i <= files.length; ++i) { - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Down }); + cy.get('#view_0').trigger('keydown', { key: Keys.DOWN }) } // check that the first element isn't visible anymore - cy.get('#view_0 [data-cy-file]').first().should('not.be.visible'); + cy.get('#view_0 [data-cy-file]').first().should('not.be.visible') // press up arrow key until the first item is selected for (let i = 0; i <= files.length; ++i) { - cy.get('#view_0').trigger('keydown', { keyCode: KEYS.Up }); + cy.get('#view_0').trigger('keydown', { key: Keys.UP }) } // it should now be visible & selected - cy.get('#view_0 [data-cy-file]').first().should('be.visible').and('have.class', 'selected'); + cy.get('#view_0 [data-cy-file]').first().should('be.visible').and('have.class', 'selected') // it's the only one that's selected - cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1); - }); + cy.get('#view_0 [data-cy-file].selected').its('length').should('eq', 1) + }) it('should open folder if folder is selected and mod + o is pressed', () => { - cy.get('#view_0 [data-cy-file]').first().click(); + cy.get('#view_0 [data-cy-file]').first().click() - cy.get('body').type(`${MOD_KEY}o`); + cy.get('body').type(`${MOD_KEY}o`) - cy.get('@stub_openDirectory0').should('be.called'); - }); + cy.get('@stub_openDirectory0').should('be.called') + }) it('should open file if a file is selected and mod + o is pressed', () => { - cy.get('#view_0 [data-cy-file]').eq(5).click(); + cy.get('#view_0 [data-cy-file]').eq(5).click() - cy.get('body').type(`${MOD_KEY}o`); + cy.get('body').type(`${MOD_KEY}o`) - cy.get('@stub_openFile0').should('be.called'); - }); + cy.get('@stub_openFile0').should('be.called') + }) it('should select all files if mod + a is pressed', () => { cy.get('body') .type(`${MOD_KEY}a`) .then(() => { - const length = files.length; + const length = files.length // check that appState.updateSelection is called with every elements cy.get('@updateSelection') .should('be.called') .and((spy: any) => { - const calls = spy.getCalls(); - const { args } = calls[0]; - expect(args[1].length).to.equal(length); - }); + const calls = spy.getCalls() + const { args } = calls[0] + expect(args[1].length).to.equal(length) + }) // and also check that every visible row is selected - cy.get('#view_0 [data-cy-file].selected').filter(':visible').should('have.class', 'selected'); - }); - }); + cy.get('#view_0 [data-cy-file].selected').filter(':visible').should('have.class', 'selected') + }) + }) it('should invert selection if mod + i is pressed', () => { - cy.get('body').type(`${MOD_KEY}a`); - cy.wait(1000); + cy.get('body').type(`${MOD_KEY}a`) + cy.wait(1000) cy.get('body') .type(`${MOD_KEY}i`) .then(() => { @@ -326,96 +319,93 @@ describe('filetable', () => { cy.get('@updateSelection') .should('be.called') .and((spy: any) => { - const calls = spy.getCalls(); + const calls = spy.getCalls() // get the last call - const { args } = calls.pop(); - expect(args[1].length).to.equal(0); - }); + const { args } = calls.pop() + expect(args[1].length).to.equal(0) + }) // and also check that every visible row is selected - cy.get('#view_0 [data-cy-file].selected').should('not.exist'); - }); - }); - }); + cy.get('#view_0 [data-cy-file].selected').should('not.exist') + }) + }) + }) describe('rename feature', () => { it('should activate rename for selected element if enter key is pressed and a file is selected', () => { cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) - .trigger('keydown', { keyCode: KEYS.Enter }) + .trigger('keydown', { key: Keys.DOWN }) + .trigger('keydown', { key: Keys.ENTER }) .find('[data-cy-file]:first') .find('.file-label') - .should('have.attr', 'contenteditable', 'true'); - }); + .should('have.attr', 'contenteditable', 'true') + }) it('should activate rename for selected element if the user keeps mousedown', () => { - cy.get('#view_0 [data-cy-file]:first .file-label').click(); - cy.wait(1000); - cy.get('#view_0 [data-cy-file]:first .file-label').click().should('have.attr', 'contenteditable', 'true'); - }); + cy.get('#view_0 [data-cy-file]:first .file-label').click() + cy.wait(1000) + cy.get('#view_0 [data-cy-file]:first .file-label').click().should('have.attr', 'contenteditable', 'true') + }) it('should select only left part of the filename', () => { // select the second element which is archive.tar.gz - cy.get('#view_0 [data-cy-file]:eq(2) .file-label').click(); - cy.wait(1000); - cy.get('#view_0 [data-cy-file]:eq(2) .file-label').click().should('have.attr', 'contenteditable', 'true'); + cy.get('#view_0 [data-cy-file]:eq(2) .file-label').click() + cy.wait(1000) + cy.get('#view_0 [data-cy-file]:eq(2) .file-label').click().should('have.attr', 'contenteditable', 'true') cy.window().then((window: Window) => { - const selection: any = window.getSelection(); + const selection: any = window.getSelection() cy.get('#view_0 [data-cy-file].selected .file-label') .invoke('text') .then((selectedFilename: any) => { - const expectedSelection = 'archive'; - const actualSelection = selectedFilename.substring( - selection.baseOffset, - selection.extentOffset, - ); - expect(actualSelection).to.equal(expectedSelection); - }); - }); - }); + const expectedSelection = 'archive' + const actualSelection = selectedFilename.substring(selection.baseOffset, selection.extentOffset) + expect(actualSelection).to.equal(expectedSelection) + }) + }) + }) it('should call cache.rename when pressing enter in edit mode', () => { cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) - .trigger('keydown', { keyCode: KEYS.Enter }) + .trigger('keydown', { key: Keys.DOWN }) + .trigger('keydown', { key: Keys.ENTER }) .find('[data-cy-file]:first') .find('.file-label') .type('bar{enter}') // we need to restore previous text to avoid the next test to crash // because React isn't aware of our inline edit since we created a stub for cache.rename // (it's supposed to reload the file cache, which in turns causes a new render of FileTable) - .invoke('text', 'folder2'); + .invoke('text', 'folder2') - cy.get('@stub_rename0').should('be.called'); - }); + cy.get('@stub_rename0').should('be.called') + }) it('should not call cache.rename & restore previous filename when pressing escape in edit mode', () => { cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) - .trigger('keydown', { keyCode: KEYS.Enter }) + .trigger('keydown', { key: Keys.DOWN }) + .trigger('keydown', { key: Keys.ENTER }) .find('[data-cy-file]:first') .find('.file-label') - .type('bar{esc}'); + .type('bar{esc}') // previous label must have been restored - cy.get('#view_0 [data-cy-file].selected').should('contain', 'folder2'); + cy.get('#view_0 [data-cy-file].selected').should('contain', 'folder2') - cy.get('@stub_rename0').should('not.be.called'); - }); + cy.get('@stub_rename0').should('not.be.called') + }) it('renaming should be cancelled if rename input field gets blur event while active', () => { cy.get('#view_0') - .trigger('keydown', { keyCode: KEYS.Down }) - .trigger('keydown', { keyCode: KEYS.Enter }) + .trigger('keydown', { key: Keys.DOWN }) + .trigger('keydown', { key: Keys.ENTER }) .find('[data-cy-file]:first') .find('.file-label') .focus() .type('bar') .blur() - .should('contain', 'folder2'); + .should('contain', 'folder2') - cy.get('@stub_rename0').should('not.be.called'); - }); - }); -}); + cy.get('@stub_rename0').should('not.be.called') + }) + }) +}) diff --git a/e2e/cypress/e2e/menu.accelerators.spec.ts b/e2e/cypress/e2e/menu.accelerators.spec.ts index 8efcf441..1a5e4a92 100644 --- a/e2e/cypress/e2e/menu.accelerators.spec.ts +++ b/e2e/cypress/e2e/menu.accelerators.spec.ts @@ -94,7 +94,7 @@ describe('combo hotkeys', () => { // no selection: triggering fake combo should not show toast message cy.triggerFakeCombo('CmdOrCtrl+Shift+N') - cy.get(`.${Classes.TOAST}`).should('not.be.visible') + cy.get(`.${Classes.TOAST}`).should('not.exist') cy.get('@copySelectedItemsPath').should('be.calledWith', caches[0], true) }) diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 43c9b207..5c6dc6fd 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -10,7 +10,11 @@ "jsx": "react", "experimentalDecorators": true, "esModuleInterop": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "$src/*": ["../src/*"] + } }, "compileOnSave": false } diff --git a/src/components/App.tsx b/src/components/App.tsx index 83946220..4774208e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4,6 +4,7 @@ import { platform } from 'process' import { isMac } from '../utils/platform' import { FocusStyleManager, Alert, Classes, Intent } from '@blueprintjs/core' import classNames from 'classnames' +import { runInAction } from 'mobx' import { Provider, observer, inject } from 'mobx-react' import { SideView } from './SideView' import { LogUI, Logger } from './Log' @@ -23,7 +24,7 @@ import { ViewDescriptor } from './TabList' import { MenuAccelerators } from './shortcuts/MenuAccelerators' import { KeyboardHotkeys } from './shortcuts/KeyboardHotkeys' import { CustomSettings } from '../electron/windowSettings' -import { runInAction } from 'mobx' +import Keys from '$src/constants/keys' require('@blueprintjs/core/lib/css/blueprint.css') require('@blueprintjs/icons/lib/css/blueprint-icons.css') @@ -120,18 +121,18 @@ const App = inject('settingsState')( let caught = false if (e.ctrlKey) { switch (true) { - case !ENV.CY && !isMac && e.keyCode === KEYS.A && shouldCatchEvent(e): + case !ENV.CY && !isMac && e.key === Keys.A && shouldCatchEvent(e): caught = true sendFakeCombo('CmdOrCtrl+A') break - case e.keyCode === KEYS.TAB: + case e.key === Keys.TAB: caught = true const combo = e.shiftKey ? 'Ctrl+Shift+Tab' : 'Ctrl+Tab' sendFakeCombo(combo) break } - } else if (shouldCatchEvent(e) && e.which === 191 && e.shiftKey) { + } else if (shouldCatchEvent(e) && e.key === Keys.FORWARD_SLASH && e.shiftKey) { caught = true } diff --git a/src/components/AppAlert.tsx b/src/components/AppAlert.tsx index 5272717c..ad879ca2 100644 --- a/src/components/AppAlert.tsx +++ b/src/components/AppAlert.tsx @@ -3,6 +3,7 @@ import * as ReactDOM from 'react-dom' import { Alert, AlertProps } from '@blueprintjs/core' import { Deferred } from '../utils/deferred' import { i18n } from '../locale/i18n' +import Keys from '$src/constants/keys' type Message = React.ReactNode | string @@ -10,8 +11,6 @@ interface AlerterState extends AlertProps { message: Message } -const ENTER_KEY = 13 - class Alerter extends React.Component, AlerterState> { defer: Deferred = null @@ -77,7 +76,7 @@ class Alerter extends React.Component, AlerterState> { } private onKeyUp = (e: KeyboardEvent): void => { - if (this.state.isOpen && e.keyCode === ENTER_KEY) { + if (this.state.isOpen && e.key === Keys.ENTER) { this.onClose(true) } } diff --git a/src/components/Downloads.tsx b/src/components/Downloads.tsx index f5bb445c..e89fe499 100644 --- a/src/components/Downloads.tsx +++ b/src/components/Downloads.tsx @@ -8,7 +8,7 @@ import i18next from 'i18next' import { withTranslation, WithTranslation } from 'react-i18next' import { formatBytes } from '../utils/formatBytes' import { FileTransfer } from '../transfers/fileTransfer' -import classnames from 'classnames' +import classNames from 'classnames' import { intentClass } from '@blueprintjs/core/lib/esm/common/classes' import { AppAlert } from './AppAlert' import CONFIG from '../config/appConfig' @@ -241,7 +241,7 @@ class DownloadsClass extends React.Component { const isCancelled = file.status.match(/cancelled/) let errorMessage = '' - const spanClass = classnames({ + const spanClass = classNames({ [Classes.INTENT_DANGER]: isError, [Classes.INTENT_SUCCESS]: done, }) diff --git a/src/components/FileTable.tsx b/src/components/FileTable.tsx index bc293dad..23cedf55 100644 --- a/src/components/FileTable.tsx +++ b/src/components/FileTable.tsx @@ -1,5 +1,9 @@ import * as React from 'react' -import { IconName, Icon, HotkeysTarget2, Classes, Menu, MenuItem } from '@blueprintjs/core' +import { WithTranslation, withTranslation } from 'react-i18next' +import { inject } from 'mobx-react' +import i18next from 'i18next' +import { IReactionDisposer, reaction, toJS, IObservableArray } from 'mobx' +import { IconName, Icon, HotkeysTarget2, Classes } from '@blueprintjs/core' import { ContextMenu2, ContextMenu2ChildrenProps, ContextMenu2ContentProps } from '@blueprintjs/popover2' import { Column, @@ -12,30 +16,27 @@ import { ScrollParams, TableCellProps, } from 'react-virtualized' -import { AppState } from '../state/appState' -import { WithTranslation, withTranslation } from 'react-i18next' -import { inject } from 'mobx-react' -import i18next from 'i18next' -import { IReactionDisposer, reaction, toJS, IObservableArray } from 'mobx' -import { File, FileID } from '../services/Fs' -import { formatBytes } from '../utils/formatBytes' -import { shouldCatchEvent, isEditable, isInRow } from '../utils/dom' +import classNames from 'classnames' +import { ipcRenderer } from 'electron' + +import CONFIG from '$src/config/appConfig' +import { AppState } from '$src/state/appState' +import { File, FileID } from '$src/services/Fs' +import { TSORT_METHOD_NAME, TSORT_ORDER, getSortMethod } from '$src/services/FsSort' +import { formatBytes } from '$src/utils/formatBytes' +import { shouldCatchEvent, isEditable, isInRow } from '$src/utils/dom' import { AppAlert } from './AppAlert' import { WithMenuAccelerators, Accelerators, Accelerator } from './WithMenuAccelerators' -import { isMac } from '../utils/platform' -import { ipcRenderer } from 'electron' -import classnames from 'classnames' +import { isMac } from '$src/utils/platform' import { RowRenderer, RowRendererProps } from './RowRenderer' -import { SettingsState } from '../state/settingsState' -import { ViewState } from '../state/viewState' -import { debounce } from '../utils/debounce' -import { TSORT_METHOD_NAME, TSORT_ORDER, getSortMethod } from '../services/FsSort' -import CONFIG from '../config/appConfig' -import { getSelectionRange } from '../utils/fileUtils' -import { throttle } from '../utils/throttle' -import { FileState } from '../state/fileState' -import { FileContextMenu } from './menus/FileContextMenu' -import classNames from 'classnames' +import { SettingsState } from '$src/state/settingsState' +import { ViewState } from '$src/state/viewState' +import { debounce } from '$src/utils/debounce' +import { getSelectionRange } from '$src/utils/fileUtils' +import { throttle } from '$src/utils/throttle' +import { FileState } from '$src/state/fileState' +import { FileContextMenu } from '$src/components/menus/FileContextMenu' +import Keys from '$src/constants/keys' declare const ENV: { [key: string]: string | boolean | number | Record } @@ -66,16 +67,6 @@ const TYPE_ICONS: { [key: string]: IconName } = { dir: 'folder-close', } -enum KEYS { - Backspace = 8, - Enter = 13, - Escape = 27, - Down = 40, - Up = 38, - PageDown = 34, - PageUp = 33, -} - interface TableRow { name: string icon: IconName @@ -237,7 +228,7 @@ export class FileTableClass extends React.Component { buildNodeFromFile(file: File, keepSelection: boolean): TableRow { const filetype = file.type const isSelected = (keepSelection && this.getSelectedState(file.fullname)) || false - const classes = classnames({ + const classes = classNames({ isHidden: file.fullname.startsWith('.'), isSymlink: file.isSym, }) @@ -358,7 +349,7 @@ export class FileTableClass extends React.Component { const hasResize = data.columnData.index < 1 const { sortMethod, sortOrder } = this.cache const isSort = data.columnData.sortMethod === sortMethod - const classes = classnames('sort', sortOrder) + const classes = classNames('sort', sortOrder) return ( @@ -374,7 +365,7 @@ export class FileTableClass extends React.Component { const error = file && file.nodeData.mode === -1 const mainClass = data.index === -1 ? 'headerRow' : 'tableRow' - return classnames(mainClass, file && file.className, { + return classNames(mainClass, file && file.className, { selected: file && file.isSelected, error: error, headerRow: data.index === -1, @@ -658,11 +649,11 @@ export class FileTableClass extends React.Component { onInputKeyDown = (e: React.KeyboardEvent): void => { if (this.editingElement) { e.nativeEvent.stopImmediatePropagation() - if (e.keyCode === KEYS.Escape || e.keyCode === KEYS.Enter) { - if (e.keyCode === KEYS.Enter) { + if (e.key === Keys.ESCAPE || e.key === Keys.ENTER) { + if (e.key === Keys.ENTER) { e.preventDefault() } - this.onInlineEdit(e.keyCode === KEYS.Escape) + this.onInlineEdit(e.key === Keys.ESCAPE) } } } @@ -732,22 +723,22 @@ export class FileTableClass extends React.Component { return } - switch (e.keyCode) { - case KEYS.Down: - case KEYS.Up: - if (!this.editingElement && (e.keyCode === KEYS.Down || e.keyCode === KEYS.Up)) { - this.moveSelection(e.keyCode === KEYS.Down ? 1 : -1, e.shiftKey) + switch (e.key) { + case Keys.DOWN: + case Keys.UP: + if (!this.editingElement) { + this.moveSelection(e.key === Keys.DOWN ? 1 : -1, e.shiftKey) e.preventDefault() } break - case KEYS.Enter: + case Keys.ENTER: this.getElementAndToggleRename(e) break - case KEYS.PageDown: - case KEYS.PageUp: - this.scrollPage(e.keyCode === KEYS.PageUp) + case Keys.PAGE_DOWN: + case Keys.PAGE_UP: + this.scrollPage(e.key === Keys.PAGE_UP) break } } diff --git a/src/components/Log.tsx b/src/components/Log.tsx index fecf2e5d..c2840efd 100644 --- a/src/components/Log.tsx +++ b/src/components/Log.tsx @@ -1,12 +1,14 @@ import * as React from 'react' import { observable, runInAction } from 'mobx' -import { debounce } from '../utils/debounce' +import classNames from 'classnames' import { Intent, HotkeysTarget2 } from '@blueprintjs/core' import { WithTranslation, withTranslation } from 'react-i18next' -import { shouldCatchEvent } from '../utils/dom' -import classnames from 'classnames' -require('../css/log.css') +import { debounce } from '$src/utils/debounce' +import { shouldCatchEvent } from '$src/utils/dom' +import Keys from '$src/constants/keys' + +require('$src/css/log.css') export interface JSObject extends Object { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -94,13 +96,13 @@ export class LogUIClass extends React.Component { } onKeyUp = (e: KeyboardEvent): void => { - if (e.keyCode === ESCAPE_KEY && this.valid) { + if (e.key === Keys.ESCAPE && this.valid) { this.setState({ visible: !this.state.visible }) } } onKeyDown = (e: KeyboardEvent): void => { - if (e.keyCode === ESCAPE_KEY && shouldCatchEvent(e)) { + if (e.key === Keys.ESCAPE && shouldCatchEvent(e)) { this.valid = true } else { this.valid = false @@ -124,11 +126,6 @@ export class LogUIClass extends React.Component { }) } - // shouldComponentUpdate() { - // console.time('Log Render'); - // return true; - // } - private hotkeys = [ { global: true, @@ -139,7 +136,7 @@ export class LogUIClass extends React.Component { ] public render(): React.ReactElement { - const classes = classnames('console', { visible: this.state.visible }) + const classes = classNames('console', { visible: this.state.visible }) return ( @@ -151,7 +148,7 @@ export class LogUIClass extends React.Component { className={classes} > {Logger.logs.map((line, i) => { - const lineClass = classnames('consoleLine', line.intent) + const lineClass = classNames('consoleLine', line.intent) return (
{/* {line.date} */} diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 1447eca5..b668f673 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -1,14 +1,15 @@ import * as React from 'react' +import { useTranslation } from 'react-i18next' import { observer } from 'mobx-react' +import { runInAction } from 'mobx' import { Navbar, Alignment, Button, Classes, Intent } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons' import { Popover2 } from '@blueprintjs/popover2' -import classnames from 'classnames' -import { useTranslation } from 'react-i18next' +import classNames from 'classnames' + import { HamburgerMenu } from './HamburgerMenu' import { Badge } from './Badge' -import { runInAction } from 'mobx' -import { useStores } from '../hooks/useStores' +import { useStores } from '$src/hooks/useStores' const Nav = observer(() => { const { appState } = useStores('appState') @@ -17,7 +18,7 @@ const Nav = observer(() => { const count = appState.pendingTransfers const badgeText = (count && count + '') || '' const badgeProgress = appState.totalTransferProgress - const downloadClass = classnames(Classes.MINIMAL, 'download') + const downloadClass = classNames(Classes.MINIMAL, 'download') const isSplitViewActive = appState.winStates[0].splitView const showDownloadsTab = (): void => { diff --git a/src/components/Statusbar.tsx b/src/components/Statusbar.tsx index 737c6920..92004001 100644 --- a/src/components/Statusbar.tsx +++ b/src/components/Statusbar.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import { observer } from 'mobx-react' import { InputGroup, ControlGroup, Button, Intent, IconName } from '@blueprintjs/core' -import classnames from 'classnames' +import classNames from 'classnames' import { Tooltip2 } from '@blueprintjs/popover2' -import { useStores } from '$src/hooks/useStores' import { useTranslation } from 'react-i18next' +import { useStores } from '$src/hooks/useStores' + const Statusbar = observer(() => { const { appState, viewState } = useStores('appState', 'viewState') const { t } = useTranslation() @@ -15,7 +16,7 @@ const Statusbar = observer(() => { const numFiles = fileCache.files.filter((file) => !file.isDir).length const numSelected = fileCache.selected.length const iconName = ((fileCache.getFS() && fileCache.getFS().icon) || 'offline') as IconName - const offline = classnames('status-bar', { offline: fileCache.status === 'offline' }) + const offline = classNames('status-bar', { offline: fileCache.status === 'offline' }) const onClipboardCopy = () => { appState.clipboard.setClipboard(viewState.getVisibleCache()) diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index d30e11ac..92415f9e 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -3,19 +3,21 @@ import { reaction, IReactionDisposer } from 'mobx' import { inject, observer } from 'mobx-react' import { InputGroup, ControlGroup, Button, ButtonGroup, Intent, Position, HotkeysTarget2 } from '@blueprintjs/core' import { Tooltip2, Popover2 } from '@blueprintjs/popover2' -import { AppState } from '../state/appState' +import { withTranslation, WithTranslation } from 'react-i18next' + +import { AppState } from '$src/state/appState' import { FileMenu } from './FileMenu' import { MakedirDialog } from './dialogs/MakedirDialog' import { AppAlert } from './AppAlert' import { Logger } from './Log' import { AppToaster } from './AppToaster' -import { withTranslation, WithTranslation } from 'react-i18next' import { WithMenuAccelerators, Accelerators, Accelerator } from './WithMenuAccelerators' -import { throttle } from '../utils/throttle' -import { isWin, isMac } from '../utils/platform' -import { ViewState } from '../state/viewState' -import { FileState } from '../state/fileState' -import { LocalizedError } from '../locale/error' +import { throttle } from '$src/utils/throttle' +import { isWin, isMac } from '$src/utils/platform' +import { ViewState } from '$src/state/viewState' +import { FileState } from '$src/state/fileState' +import { LocalizedError } from '$src/locale/error' +import Keys from '$src/constants/keys' const TOOLTIP_DELAY = 1200 const MOVE_EVENT_THROTTLE = 300 @@ -128,7 +130,7 @@ export const ToolbarClass = inject( private onKeyUp = (event: React.KeyboardEvent): void => { this.hideTooltip() - if (event.keyCode === KEYS.Escape) { + if (event.key === Keys.ESCAPE) { // since React events are attached to the root document // event already has bubbled up so we must stop // its immediate propagation @@ -137,7 +139,7 @@ export const ToolbarClass = inject( this.input.blur() // workaround for Cypress bug https://github.com/cypress-io/cypress/issues/1176 // this.onBlur(); - } else if (event.keyCode === KEYS.Enter) { + } else if (event.key === Keys.ENTER) { this.onSubmit() } } diff --git a/src/components/dialogs/LoginDialog.tsx b/src/components/dialogs/LoginDialog.tsx index a21b3e29..4831649b 100644 --- a/src/components/dialogs/LoginDialog.tsx +++ b/src/components/dialogs/LoginDialog.tsx @@ -1,9 +1,11 @@ import * as React from 'react' import { Dialog, Classes, Intent, Button, InputGroup, FormGroup, Colors } from '@blueprintjs/core' import { inject } from 'mobx-react' -import { FileState } from '../../state/fileState' import { withTranslation, WithTranslation } from 'react-i18next' +import { FileState } from '$src/state/fileState' +import Keys from '$src/constants/keys' + interface LoginProps extends WithTranslation { isOpen: boolean onClose?: (user: string, password: string) => void @@ -57,7 +59,7 @@ class LoginDialogClass extends React.Component { } onKeyUp = (e: KeyboardEvent): void => { - if (e.keyCode === ENTER_KEY) { + if (e.key === Keys.ENTER) { // we assume anonymous login if no username specified if (this.canLogin()) { this.onLogin() diff --git a/src/components/dialogs/MakedirDialog.tsx b/src/components/dialogs/MakedirDialog.tsx index 6a064f9e..5f162601 100644 --- a/src/components/dialogs/MakedirDialog.tsx +++ b/src/components/dialogs/MakedirDialog.tsx @@ -1,8 +1,10 @@ import * as React from 'react' import { Dialog, Classes, Intent, Button, InputGroup, FormGroup } from '@blueprintjs/core' -import { debounce } from '../../utils/debounce' import { withTranslation, WithTranslation } from 'react-i18next' -import { metaKeyCode } from '../../utils/platform' + +import { debounce } from '$src/utils/debounce' +import { metaKey } from '$src/utils/platform' +import Keys from '$src/constants/keys' interface MakedirProps extends WithTranslation { isOpen: boolean @@ -35,18 +37,18 @@ class MakedirDialogClass extends React.Component { } onKeyUp = (e: KeyboardEvent): void => { - if (e.keyCode === metaKeyCode) { + if (e.key === metaKey) { this.setState({ ctrlKey: false }) - } else if (e.keyCode === ENTER_KEY) { + } else if (e.key === Keys.ENTER) { const { valid, path } = this.state valid && path.length && this.onCreate() } } onKeyDown = (e: KeyboardEvent): void => { - if (e.keyCode === metaKeyCode) { + if (e.key === metaKey) { this.setState({ ctrlKey: true }) - } else if (e.keyCode === ENTER_KEY && this.state.ctrlKey) { + } else if (e.key === Keys.ENTER && this.state.ctrlKey) { const { valid, path } = this.state valid && path.length && this.onCreate() } diff --git a/src/constants/keys.ts b/src/constants/keys.ts new file mode 100644 index 00000000..9ad8459b --- /dev/null +++ b/src/constants/keys.ts @@ -0,0 +1,16 @@ +const Keys = { + A: 'a', + BACKSPACE: 'Backspace', + CONTROL: 'Control', + DOWN: 'ArrowDown', + ENTER: 'Enter', + ESCAPE: 'Escape', + META: 'Meta', + PAGE_DOWN: 'PageDown', + PAGE_UP: 'PageUp', + FORWARD_SLASH: '/', + TAB: 'Tab', + UP: 'ArrowUp', +} + +export default Keys diff --git a/src/electron/osSupport.ts b/src/electron/osSupport.ts index 257e7e8b..982efcbf 100644 --- a/src/electron/osSupport.ts +++ b/src/electron/osSupport.ts @@ -1,65 +1,62 @@ -import { platform } from 'process'; -import { release, arch, userInfo } from 'os'; -import { app } from 'electron'; +import { platform } from 'process' +import { release, arch, userInfo } from 'os' +import { app } from 'electron' -declare const ENV: { [key: string]: string | boolean | number | Record }; +declare const ENV: { [key: string]: string | boolean | number | Record } -type App = { getPath: (name: string) => string } | Electron.App; +type App = { getPath: (name: string) => string } | Electron.App function getAppInstance(): App { - console.log('getAppInstance', app); - let appInstance: App = app; + console.log('getAppInstance', app) + let appInstance: App = app if (!appInstance) { // eslint-disable-next-line @typescript-eslint/no-var-requires - const getPath: (name: string) => string = require('./test/helpers').getPath; + const getPath: (name: string) => string = require('./test/helpers').getPath // simulate getPath for test environment appInstance = { getPath, - }; + } } - return appInstance; + return appInstance } -const appInstance = getAppInstance(); +const appInstance = getAppInstance() function getDefaultFolder() { - let defaultFolder = ''; + let defaultFolder = '' if (typeof jest !== 'undefined') { - defaultFolder = ''; + defaultFolder = '' } else { defaultFolder = ENV.NODE_ENV === 'production' ? appInstance.getPath('home') : platform === 'win32' ? appInstance.getPath('temp') - : '/tmp/react-explorer'; + : '/tmp/react-explorer' } - return defaultFolder; + return defaultFolder } -const META_KEY = 91; -const CTRL_KEY = 17; - -export const isMac = platform === 'darwin'; -export const isMojave = isMac && parseInt(release().split('.')[0], 10) - 4 >= 14; -export const isWin = platform === 'win32'; -export const isLinux = platform === 'linux'; -export const metaKeyCode = (isMac && META_KEY) || CTRL_KEY; -export const lineEnding = isWin ? '\r\n' : '\n'; +export const isMac = platform === 'darwin' +export const isMojave = isMac && parseInt(release().split('.')[0], 10) - 4 >= 14 +export const isWin = platform === 'win32' +export const isLinux = platform === 'linux' +export const metaKey = (isMac && 'Meta') || 'Control' +export const lineEnding = isWin ? '\r\n' : '\n' // depends on appInstance -export const defaultFolder = getDefaultFolder(); -export const TMP_DIR = appInstance.getPath('temp'); -export const HOME_DIR = appInstance.getPath('home'); -export const DOWNLOADS_DIR = appInstance.getPath('downloads'); -export const MUSIC_DIR = appInstance.getPath('music'); -export const DOCS_DIR = appInstance.getPath('documents'); -export const DESKTOP_DIR = appInstance.getPath('desktop'); -export const PICTURES_DIR = appInstance.getPath('pictures'); -export const VIDEOS_DIR = appInstance.getPath('videos'); +export const defaultFolder = getDefaultFolder() +export const TMP_DIR = appInstance.getPath('temp') +export const HOME_DIR = appInstance.getPath('home') +export const DOWNLOADS_DIR = appInstance.getPath('downloads') +export const MUSIC_DIR = appInstance.getPath('music') +export const DOCS_DIR = appInstance.getPath('documents') +export const DESKTOP_DIR = appInstance.getPath('desktop') +export const PICTURES_DIR = appInstance.getPath('pictures') +export const VIDEOS_DIR = appInstance.getPath('videos') export const ALL_DIRS: Record = { HOME_DIR, DOWNLOADS_DIR, @@ -68,8 +65,8 @@ export const ALL_DIRS: Record = { DOCS_DIR, DESKTOP_DIR, VIDEOS_DIR, -}; -export const USERNAME = userInfo().username || 'username'; +} +export const USERNAME = userInfo().username || 'username' export const VERSIONS = { platform, release: release(), @@ -77,4 +74,4 @@ export const VERSIONS = { electron: process.versions['electron'], chrome: process.versions['chrome'], node: process.version, -}; +} diff --git a/src/state/clipboardState.ts b/src/state/clipboardState.ts index dd3516d1..922e3459 100644 --- a/src/state/clipboardState.ts +++ b/src/state/clipboardState.ts @@ -66,8 +66,8 @@ export class ClipboardState { AppToaster.show( { message: filenameOnly - ? this.t('COMMON.CP_NAMES_COPIED', { count: length }) - : this.t('COMMON.CP_PATHS_COPIED', { count: length }), + ? this.t('COMMON.CP_NAMES_COPIED', { count: files.length }) + : this.t('COMMON.CP_PATHS_COPIED', { count: files.length }), icon: 'tick', intent: Intent.NONE, }, diff --git a/src/utils/platform.ts b/src/utils/platform.ts index d400d0f2..7f52139d 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -1,12 +1,12 @@ -import { ipcRenderer } from 'electron'; -import { platform } from 'process'; +import { ipcRenderer } from 'electron' +import { platform } from 'process' const OS = (ipcRenderer && ipcRenderer.sendSync('app:getOS')) || { isMac: platform === 'darwin', isMojave: false, isWin: platform === 'win32', isLinux: platform === 'linux', - metaKeyCode: platform === 'darwin' ? 91 : 17, + metaKey: platform === 'darwin' ? 'Meta' : 'Control', lineEnding: platform === 'win32' ? '\r\n' : '\n', defaultFolder: '/', TMP_DIR: '/tmp', @@ -35,23 +35,23 @@ const OS = (ipcRenderer && ipcRenderer.sendSync('app:getOS')) || { chrome: 'chrome version', node: 'node version', }, -}; +} -export const isMac = OS.isMac; -export const isMojave = OS.isMojave; -export const isWin = OS.isWin; -export const isLinux = OS.isLinux; -export const metaKeyCode = OS.metaKeyCode; -export const lineEnding = OS.lineEnding; -export const defaultFolder = OS.defaultFolder; -export const TMP_DIR = OS.TMP_DIR; -export const HOME_DIR = OS.HOME_DIR; -export const DOWNLOADS_DIR = OS.DOWNLOADS_DIR; -export const MUSIC_DIR = OS.MUSIC_DIR; -export const DOCS_DIR = OS.DOCS_DIR; -export const DESKTOP_DIR = OS.DESKTOP_DIR; -export const PICTURES_DIR = OS.PICTURES_DIR; -export const VIDEOS_DIR = OS.VIDEOS_DIR; -export const ALL_DIRS: { [key: string]: string } = OS.ALL_DIRS; -export const USERNAME = OS.USERNAME; -export const VERSIONS = OS.VERSIONS; +export const isMac = OS.isMac +export const isMojave = OS.isMojave +export const isWin = OS.isWin +export const isLinux = OS.isLinux +export const metaKey = OS.metaKey +export const lineEnding = OS.lineEnding +export const defaultFolder = OS.defaultFolder +export const TMP_DIR = OS.TMP_DIR +export const HOME_DIR = OS.HOME_DIR +export const DOWNLOADS_DIR = OS.DOWNLOADS_DIR +export const MUSIC_DIR = OS.MUSIC_DIR +export const DOCS_DIR = OS.DOCS_DIR +export const DESKTOP_DIR = OS.DESKTOP_DIR +export const PICTURES_DIR = OS.PICTURES_DIR +export const VIDEOS_DIR = OS.VIDEOS_DIR +export const ALL_DIRS: { [key: string]: string } = OS.ALL_DIRS +export const USERNAME = OS.USERNAME +export const VERSIONS = OS.VERSIONS