diff --git a/.gitignore b/.gitignore
index 30bc162..4c32ea0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
-/node_modules
\ No newline at end of file
+# Specific system files should be ignored by user in global `.gitignore`.
+# More info here https://gist.github.com/subfuzion/db7f57fff2fb6998a16c
+
+/node_modules
diff --git a/assets/dark/icon-active.png b/assets/dark/icon-active.png
new file mode 100644
index 0000000..5696010
Binary files /dev/null and b/assets/dark/icon-active.png differ
diff --git a/assets/dark/icon-active@2x.png b/assets/dark/icon-active@2x.png
new file mode 100644
index 0000000..22ecfa2
Binary files /dev/null and b/assets/dark/icon-active@2x.png differ
diff --git a/assets/dark/icon.png b/assets/dark/icon.png
new file mode 100644
index 0000000..9307f9c
Binary files /dev/null and b/assets/dark/icon.png differ
diff --git a/assets/dark/icon@2x.png b/assets/dark/icon@2x.png
new file mode 100644
index 0000000..9a1587d
Binary files /dev/null and b/assets/dark/icon@2x.png differ
diff --git a/assets/light/icon-active.png b/assets/light/icon-active.png
new file mode 100644
index 0000000..d1cb4ee
Binary files /dev/null and b/assets/light/icon-active.png differ
diff --git a/assets/light/icon-active@2x.png b/assets/light/icon-active@2x.png
new file mode 100644
index 0000000..faa44ce
Binary files /dev/null and b/assets/light/icon-active@2x.png differ
diff --git a/assets/light/icon.png b/assets/light/icon.png
new file mode 100644
index 0000000..f50e3bd
Binary files /dev/null and b/assets/light/icon.png differ
diff --git a/assets/light/icon@2x.png b/assets/light/icon@2x.png
new file mode 100644
index 0000000..49d5f12
Binary files /dev/null and b/assets/light/icon@2x.png differ
diff --git a/client/constants.js b/client/constants.js
new file mode 100644
index 0000000..9272fff
--- /dev/null
+++ b/client/constants.js
@@ -0,0 +1,82 @@
+module.exports.humanizedKeyCodes = new Map([
+ [41, '`'],
+ [2, '1'],
+ [3, '2'],
+ [4, '3'],
+ [5, '4'],
+ [6, '5'],
+ [7, '6'],
+ [8, '7'],
+ [9, '8'],
+ [10, '9'],
+ [11, '0'],
+ [12, '-'],
+ [13, '+'],
+
+ [16, 'q'],
+ [17, 'w'],
+ [18, 'e'],
+ [19, 'r'],
+ [20, 't'],
+ [21, 'y'],
+ [22, 'u'],
+ [23, 'i'],
+ [24, 'o'],
+ [25, 'p'],
+ [26, '['],
+ [27, ']'],
+ [43, '\\'],
+
+ [30, 'a'],
+ [31, 's'],
+ [32, 'd'],
+ [33, 'f'],
+ [34, 'g'],
+ [35, 'h'],
+ [36, 'j'],
+ [37, 'k'],
+ [38, 'l'],
+ [39, ';'],
+ [40, '\''],
+
+ [44, 'z'],
+ [45, 'x'],
+ [46, 'c'],
+ [47, 'v'],
+ [48, 'b'],
+ [49, 'n'],
+ [50, 'm'],
+ [51, ','],
+ [52, '.'],
+ [53, '/'],
+
+ [59, 'f1'],
+ [60, 'f2'],
+ [61, 'f3'],
+ [62, 'f4'],
+ [63, 'f5'],
+ [64, 'f6'],
+ [65, 'f7'],
+ [66, 'f8'],
+ [67, 'f9'],
+ [68, 'f10'],
+ [87, 'f11'],
+ [88, 'f12'],
+
+ [1, 'esc'],
+ [14, '⌫'], // delete
+ [15, 'tab'],
+ [29, '⌃'], // ctrl
+ [56, '⌥'], // alt
+ [57, '␣'], // space
+ [42, '⇧'], // shift
+ [54, 'right⇧'], // rshift
+ [28, '⏎︎'], // enter
+ [3675, '⌘'], // cmd
+ [3676, 'right⌘'], // cmd
+ [3640, 'right⌥'], // ralt
+ [57416, '↑'], // up
+ [57424, '↓'], // down
+ [57419, '←'], // left
+ [57421, '→'], // right
+]);
diff --git a/client/index.css b/client/index.css
new file mode 100644
index 0000000..54b894e
--- /dev/null
+++ b/client/index.css
@@ -0,0 +1,47 @@
+body {
+ margin: 0;
+ width: 100vw;
+ height: 100vh;
+}
+
+.keypress {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 2vmax 1vmax;
+ width: 100%;
+ text-align: center;
+ box-sizing: border-box;
+}
+
+.keypress__item {
+ padding: 1vmax;
+}
+
+.keypress__item_type_plus {
+ padding: 0;
+}
+
+.keypress-item {
+ padding: 1vmax;
+ font-family: Arial, -apple-system, sans-serif;
+ font-weight: 700;
+ font-size: 4vmax;
+ line-height: 1;
+ text-transform: uppercase;
+ color: #fff;
+ border-radius: 0.5vmax;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.keypress-item-plus {
+ font-family: Arial, -apple-system, sans-serif;
+ font-weight: 700;
+ font-size: 4vmax;
+ line-height: 1;
+ color: #fff;
+}
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..5922538
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Keypress Shower
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/index.js b/client/index.js
new file mode 100644
index 0000000..6e0804f
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,37 @@
+const { ipcRenderer } = require('electron');
+const { humanizedKeyCodes } = require('./constants.js');
+
+ipcRenderer.on('keydown', (event, message) => {
+ computeKeyPressed(message.type, message.keycode);
+});
+
+ipcRenderer.on('keyup', (event, message) => {
+ computeKeyPressed(message.type, message.keycode);
+});
+
+const pressedKeys = new Map();
+
+function computeKeyPressed(type, keyCode) {
+ switch (type) {
+ case 'keydown':
+ pressedKeys.set(keyCode, humanizedKeyCodes.get(keyCode));
+ break;
+ case 'keyup':
+ pressedKeys.delete(keyCode);
+ break;
+ }
+
+ renderKeysPressed();
+}
+
+const elemRoot = document.getElementById('root');
+
+function renderKeysPressed() {
+ const result = [];
+
+ pressedKeys.forEach((value) => {
+ result.push(``);
+ });
+
+ elemRoot.innerHTML = result.join(``);
+}
diff --git a/index.html b/index.html
deleted file mode 100644
index ec20cca..0000000
--- a/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
- Привет мир!
-
-
-
-
- Привет мир!
- Мы используем node ,
- Chrome ,
- и Electron .
-
-
diff --git a/main.js b/main.js
index d624143..86f65e6 100644
--- a/main.js
+++ b/main.js
@@ -1,17 +1,33 @@
-const { app, BrowserWindow } = require('electron')
-
-function createWindow () {
- // Создаем окно браузера.
- let win = new BrowserWindow({
- width: 800,
- height: 600,
- webPreferences: {
- nodeIntegration: true
- }
- })
-
- // и загрузить index.html приложения.
- win.loadFile('index.html')
+/*
+ * TODO
+ * - Hide App in Mission Control
+ */
+
+// libs
+const { app } = require('electron');
+
+// modules
+const { AppController } = require('./server/AppController.js');
+const { BrowserWindowController } = require('./server/BrowserWindowController.js');
+
+let appController = null;
+
+function handleAppReady() {
+ appController = new AppController(new BrowserWindowController('./client/index.html'));
+
+ appController.onQuit(() => {
+ appController.stop();
+ app.exit(0);
+ });
+
+ appController.start();
+}
+
+function handleAppWindowAllClosed(event) {
+ // Hook for prevent app quit.
+ event.preventDefault();
}
-app.whenReady().then(createWindow)
\ No newline at end of file
+// app.dock.hide();
+app.once('ready', handleAppReady);
+app.on('window-all-closed', handleAppWindowAllClosed);
diff --git a/server/AppController.js b/server/AppController.js
new file mode 100644
index 0000000..27bc62c
--- /dev/null
+++ b/server/AppController.js
@@ -0,0 +1,173 @@
+const { Tray, Menu, nativeImage, NativeImage, MenuItem, nativeTheme } = require('electron');
+const path = require('path');
+
+/**
+ * @namespace AppController
+ * @typedef {('dark' | 'light')} Theme
+ */
+class AppController {
+ /** @return {Theme} */
+ static getCurrentThemeName() {
+ return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
+ }
+
+ /**
+ * @param {Theme} theme
+ * @return {Theme}
+ */
+ static invertThemeByName(theme) {
+ switch (theme) {
+ case 'dark':
+ return 'light';
+ case 'light':
+ return 'dark';
+ }
+ }
+
+ /** @param {(BrowserWindowController)} browserWindowController */
+ constructor(browserWindowController) {
+ this.isActive = false;
+ this.theme = AppController.getCurrentThemeName();
+ this.tray = new Tray(nativeImage.createEmpty());
+
+ this.setTrayImage();
+ this.setTrayTooltip();
+
+ this.handleActivateMenuItemClick = this.handleActivateMenuItemClick.bind(this);
+ this.handleQuitClick = this.handleQuitClick.bind(this);
+ this.handleThemeChange = this.handleThemeChange.bind(this);
+
+ this.tray.setContextMenu(Menu.buildFromTemplate([
+ {
+ type: 'checkbox',
+ label: 'Activate',
+ click: this.handleActivateMenuItemClick,
+ checked: this.isActive,
+ },
+ {
+ label: 'Quit',
+ click: this.handleQuitClick,
+ },
+ ]));
+
+ this.browserWindowController = browserWindowController;
+ }
+
+ start() {
+ this.bindEventListeners();
+ }
+
+ stop() {
+ this.unbindEventListeners();
+
+ if (!this.tray.isDestroyed()) {
+ this.tray.destroy();
+ }
+
+ this.browserWindowController.destroy();
+ }
+
+ /**
+ * @param {function} callback
+ * @return {function}
+ */
+ onQuit(callback) {
+ this.quitHandler = callback;
+
+ return () => {
+ this.quitHandler = () => {};
+ };
+ }
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ isActive;
+ /**
+ * @type {Theme}
+ * @private
+ */
+ theme;
+ /**
+ * @type {(Electron.Tray)}
+ * @private
+ */
+ tray;
+ /**
+ * @type {(BrowserWindowController)}
+ * @private
+ */
+ browserWindowController;
+ /**
+ * @type {function}
+ * @private
+ */
+ quitHandler = () => {};
+
+ /** @private */
+ bindEventListeners() {
+ nativeTheme.on('updated', this.handleThemeChange);
+ }
+
+ /** @private */
+ unbindEventListeners() {
+ nativeTheme.off('updated', this.handleThemeChange);
+ }
+
+ /** @private */
+ setTrayImage() {
+ this.tray.setImage(this.getImagePath(AppController.invertThemeByName(this.theme)));
+ this.tray.setPressedImage(this.getImagePath(this.theme));
+ }
+
+ /** @private */
+ setTrayTooltip() {
+ if (this.isActive) {
+ this.tray.setToolTip('Keypress shower is turned on');
+ } else {
+ this.tray.setToolTip('Keypress shower is turned off');
+ }
+ }
+
+ /**
+ * @param {Theme} theme
+ * @return string
+ * @private
+ */
+ getImagePath(theme) {
+ const pathToImage = this.isActive ? `assets/${theme}/icon-active.png` : `assets/${theme}/icon.png`;
+
+ return path.join(process.cwd(), pathToImage);
+ }
+
+ /**
+ * @param {Electron.MenuItem} menuItem
+ * @private
+ */
+ handleActivateMenuItemClick(menuItem) {
+ this.isActive = menuItem.checked;
+
+ this.setTrayImage();
+ this.setTrayTooltip();
+
+ if (this.isActive) {
+ this.browserWindowController.create();
+ } else {
+ this.browserWindowController.destroy();
+ }
+ }
+
+ /** @private */
+ handleQuitClick() {
+ this.quitHandler();
+ }
+
+ /** @private */
+ handleThemeChange() {
+ this.theme = AppController.getCurrentThemeName();
+ this.setTrayImage();
+ }
+}
+
+module.exports.AppController = AppController;
diff --git a/server/BrowserWindowController.js b/server/BrowserWindowController.js
new file mode 100644
index 0000000..db158f1
--- /dev/null
+++ b/server/BrowserWindowController.js
@@ -0,0 +1,91 @@
+const { screen, BrowserWindow } = require('electron');
+const ioHook = require('iohook');
+
+/**
+ * @namespace BrowserWindowController
+ */
+class BrowserWindowController {
+ constructor(viewUrl) {
+ this.viewUrl = viewUrl;
+ this.handleKeyEvent = this.handleKeyEvent.bind(this);
+ }
+
+ create() {
+ const { width, height } = screen.getPrimaryDisplay().workAreaSize;
+
+ this.instance = new BrowserWindow({
+ width,
+ height,
+ frame: false,
+ transparent: true,
+ // backgroundColor: 'rgba(0, 0, 0, 0)',
+ // skipTaskbar: true,
+ hasShadow: false,
+ webPreferences: {
+ nodeIntegration: true
+ }
+ });
+
+ // 'screen-saver' move our window to the higher level.
+ this.instance.setAlwaysOnTop(true, 'screen-saver');
+
+ this.instance.setIgnoreMouseEvents(true);
+
+ this.instance.loadFile(this.viewUrl);
+
+ this.bindKeyEventsListeners();
+ }
+
+ destroy() {
+ if (this.instance) {
+ if (!this.instance.isDestroyed()) {
+ this.instance.destroy();
+ }
+
+ this.instance = null;
+ }
+
+ this.unbindKeyEventsListeners();
+ }
+
+ /**
+ * @type {(Electron.BrowserWindow | null)}
+ * @private
+ */
+ instance = null;
+
+ /**
+ * @type {string}
+ * @private
+ */
+ viewUrl = '';
+
+ /** @private */
+ bindKeyEventsListeners() {
+ ioHook.start();
+ ioHook.on('keydown', this.handleKeyEvent);
+ ioHook.on('keyup', this.handleKeyEvent);
+ }
+
+ /** @private */
+ unbindKeyEventsListeners() {
+ ioHook.stop();
+ ioHook.off('keydown', this.handleKeyEvent);
+ ioHook.off('keyup', this.handleKeyEvent);
+ }
+
+ /** @private */
+ handleKeyEvent(event) {
+ if (!this.instance || this.instance.isDestroyed()) {
+ return;
+ }
+
+ try {
+ this.instance.webContents.send(event.type, event);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+}
+
+module.exports.BrowserWindowController = BrowserWindowController;