Skip to content

Commit

Permalink
Merge pull request #27 from nathan-fiscaletti/dev
Browse files Browse the repository at this point in the history
Application version: 1.2.0, Backend version: 5.8.6
  • Loading branch information
nathan-fiscaletti authored Nov 28, 2024
2 parents 9e674d5 + 409c47a commit 12c20c9
Show file tree
Hide file tree
Showing 40 changed files with 2,427 additions and 110 deletions.
51 changes: 30 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
<p align="center">
<img src="./application/src/app_icon.png" width="150" height="150">
<h1 align="center">Keyboard Sounds</h1>
<p align="center">Add sound effects to your typing experience.</p>
</p>

<div align="center">
![Banner](./banner.png)

<a href="https://github.com/sponsors/nathan-fiscaletti"><img src="https://img.shields.io/badge/%F0%9F%92%B8-Sponsor%20Me!-blue"></a>
<a href="https://badge.fury.io/py/keyboardsounds"><img src="https://badge.fury.io/py/keyboardsounds.svg"></a>
<a href="https://github.com/nathan-fiscaletti/keyboardsounds/blob/master/LICENSE"><img src="https://img.shields.io/github/license/nathan-fiscaletti/keyboardsounds.svg"></a>
<a href="https://pepy.tech/project/keyboardsounds"><img src="https://static.pepy.tech/badge/keyboardsounds"></a>
# Keyboard Sounds

</div>

<p align="center">
Keyboard Sounds is a tool that runs in your system tray and plays sound effects when you type on your keyboard. It comes with a variety of sound profiles to choose from, and you can even create your own custom profiles.
</p>
Add sound effects to your typing experience.

<p align="center">
[![Sponsor Me](https://img.shields.io/badge/%F0%9F%92%B8-Sponsor%20Me!-blue)](https://github.com/sponsors/nathan-fiscaletti)
[![PyPi](https://badge.fury.io/py/keyboardsounds.svg)](https://badge.fury.io/py/keyboardsounds)
[![License](https://img.shields.io/github/license/nathan-fiscaletti/keyboardsounds.svg)](https://github.com/nathan-fiscaletti/keyboardsounds/blob/master/LICENSE)
[![Downloads](https://static.pepy.tech/badge/keyboardsounds)](https://pepy.tech/project/keyboardsounds)

<img src="./application/preview.png" alt="Preview" style="max-width: 100%;">
[⬇️ Download Desktop Application (Windows Only)](https://github.com/nathan-fiscaletti/keyboardsounds/releases/latest)

</p>
Keyboard Sounds is a lightweight system tray application that adds sound effects to your typing experience.

### Getting Started

- [Install Keyboard Sounds](#installation)
- [Create Custom Profiles](#custom-profiles)
- [Application Rules](#application-rules)
- [Command Line Usage](#command-line)

### Helpful Links
Expand All @@ -42,6 +33,8 @@ Keyboard Sounds can be installed as a desktop application or as a Python package

[⬇️ Download (Windows Only)](https://github.com/nathan-fiscaletti/keyboardsounds/releases/latest)

<img align="right" src="./application/main.png" width="250" />

Currently the desktop application is only available for **Windows**. The Python package can be used on any platform that supports Python.

The desktop application still requires the Python package to be installed on your system. On first launch, the application will check that both Python and the required Python packages are installed.
Expand All @@ -50,7 +43,9 @@ The desktop application still requires the Python package to be installed on you

You may need to restart the application after doing this for the changes to take effect.

### Python Package
> For information on uninstalling Keyboard Sounds, see [Uninstall Keyboard Sounds](#uninstalling)
### Install as Python Package

To install this application as a CLI utility via the Python package, you will need to have Python installed on your system. You can download Python from the [official website](https://www.python.org/).

Expand All @@ -63,12 +58,26 @@ To install this application as a CLI utility via the Python package, you will ne

## Custom Profiles

This application supports custom profiles in which you can provide your own WAV or MP3 files to be used for the different keys pressed.
Keyboard Sounds comes bundled with eleven built-in sound profiles and supports custom profiles in which you can provide your own WAV or MP3 files to be used for the different keys pressed.

Read more about creating and editing profiles [here](./docs/custom-profiles.md).

![Custom Profiles](./application/editor-with-profiles.png)

## Application Rules

Keyboard Sounds supports application rules in which you can control the behavior of the sound daemon based on the currently running applications. Read more about application rules [here](./docs/backend.md#managing-application-rules-windows-only)

<p align="center">
<img src="./application/app-rule.png" />
</p>

## Command Line

<p align="center">
<img src="./application/cli.png" />
</p>

Keyboard Sounds has a comprehensive backend that can be used to manage the daemon, application rules, and profiles. This backend can be accessed via the command line interface (CLI) in your terminal application.

Read more about backend usage [here](./docs/backend.md).
Expand Down
Binary file added application/app-rule.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/application_rules.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/cli.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/editor-with-profiles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/editor_preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/editor_preview2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions application/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "kbs-electron",
"productName": "Keyboard Sounds",
"version": "1.1.1",
"version": "1.2.0",
"description": "https://keyboardsounds.net/",
"main": ".webpack/main",
"repository": {
Expand Down Expand Up @@ -31,7 +31,7 @@
}
},
"scripts": {
"start": "electron-forge start",
"start": "cross-env NODE_ENV=development electron-forge start",
"package": "electron-forge package",
"make-installer:win": "electron-builder -w \"-c.extraMetadata.main=.webpack\\x64\\main\\index.js\"",
"lint": "echo \"No linting configured\""
Expand All @@ -51,6 +51,7 @@
"@electron/fuses": "^1.7.0",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"babel-loader": "^9.1.3",
"cross-env": "^7.0.3",
"css-loader": "^6.0.0",
"electron": "29.2.0",
"electron-builder": "^24.13.3",
Expand All @@ -72,9 +73,12 @@
"@mui/material": "^5.15.15",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^8.2.0",
"js-yaml": "^4.1.0",
"react": "^18.2.0",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-simple-keyboard": "^3.8.22",
"semver": "^7.6.2"
}
}
Binary file added application/profiles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added application/running.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 134 additions & 15 deletions application/src/api/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Socket } from "net";
import { BrowserWindow, shell, dialog } from 'electron';
import { exec } from 'child_process';
import semver from 'semver';
import fs from 'fs';
import path from 'path';
import os from 'os';
import yaml from 'js-yaml';

import Store from 'electron-store';

Expand All @@ -18,6 +22,8 @@ const MinimumPythonPackageVersion = '5.7.2';

const kbs = {
mainWindow: null,
editorWindowCreateHandler: null,
editorWindow: null,
openFileDialogIsOpen: false,
appVersion: '1.0.0',

Expand Down Expand Up @@ -78,6 +84,21 @@ const kbs = {
});
},

profileNames: function() {
return new Promise((resolve, reject) => {
this.exec('list-profiles --short', false).then((stdout) => {
try {
const profiles = JSON.parse(stdout);
resolve(profiles.map(p => p.name));
} catch (err) {
reject(err);
}
}).catch((err) => {
reject(err);
});
});
},

rules: function() {
return new Promise((resolve, reject) => {
this.exec('list-rules --short', false).then((stdout) => {
Expand Down Expand Up @@ -149,6 +170,21 @@ const kbs = {
this.mainWindow.focus();
},

selectAudioFile: async function() {
const res = await dialog.showOpenDialog(this.mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Audio File', extensions: ['wav', 'mp3'] }
]
});

this.editorWindow.focus();
if (!res.canceled) {
return res.filePaths[0];
}
return "";
},

selectExecutableFile: async function() {
if (this.openFileDialogIsOpen) {
return;
Expand Down Expand Up @@ -306,26 +342,59 @@ const kbs = {
this.mainWindow = mainWindow;
},

registerKbsIpcHandler: function (ipcMain, shouldNotify=()=>true) {
setEditorWindowCreateHandler: function (handler) {
this.editorWindowCreateHandler = handler;
},

showEditorWindow: function() {
if (!this.editorWindow) {
this.editorWindow = this.editorWindowCreateHandler();
// Make links open in browser.
this.editorWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});

// When the window is closed, set this.editorWindow to null
this.editorWindow.on('closed', () => {
this.editorWindow = null;
});
}
this.editorWindow.show();
this.editorWindow.focus();
},

registerKbsIpcHandler: function (ipcMain, shouldNotify=()=>false) {
// Listen for incoming IPC messages.
ipcMain.on('kbs', async (event, data) => {
const { command, channelId } = data;
console.log(`ipcMain.on kbs[${channelId}] ${command}`);

const [commandName, ...commandArgs] = command.split(' ');

// check if cmd is a member of this
if (typeof this[commandName] === 'function') {
console.log(`running as functional command`)
try {
const result = await this[commandName](...commandArgs);
event.reply(`kbs_execute_result_${channelId}`, result);
} catch (err) {
console.log(`error running command: ${err}`);
event.reply(`kbs_execute_result_${channelId}`, err);
}
} else if (commandName == "reset_last_known") {
lastKnownStatus = null;
lastKnownGlobalAction = null;
lastKnownAppRules = null;
lastKnownProfiles = null;
lastKnownPerformNotify = null;
} else {
console.log(`running as direct command`);
// attempt to execute the command directly
this.exec(command).then((result) => {
event.reply(`kbs_execute_result_${channelId}`, result);
}).catch((err) => {
console.log(`error running command: ${err}`);
event.reply(`kbs_execute_result_${channelId}`, err);
});
}
Expand All @@ -335,17 +404,29 @@ const kbs = {
let lastKnownGlobalAction = null;
let lastKnownAppRules = null;
let lastKnownProfiles = null;
let lastKnownPerformNotify = null;

const notify = (key, val) => {
console.log(`notify ${key} ${JSON.stringify(val)}`);
BrowserWindow.getAllWindows().forEach(window => {
console.log(`window ${window.id}`);
window.webContents.send(key, val);
});
};

setInterval(() => {
// Watch the status and notify the renderer process when it changes
if (shouldNotify()) {
const performNotify = shouldNotify()
if (lastKnownPerformNotify !== performNotify) {
console.log('performNotify', performNotify);
lastKnownPerformNotify = performNotify;
}
if (performNotify) {
this.status().then((status) => {
const stringifiedStatus = JSON.stringify(status);
if (lastKnownStatus === null || stringifiedStatus !== lastKnownStatus) {
console.log('notifying status change');
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('kbs-status', status);
});
notify('kbs-status', status);
// Update the last known status
lastKnownStatus = stringifiedStatus;
}
Expand All @@ -357,24 +438,20 @@ const kbs = {
this.getGlobalAction().then((action) => {
if (lastKnownGlobalAction === null || action !== lastKnownGlobalAction) {
console.log('notifying global action change');
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('kbs-global-action', action);
});
notify('kbs-global-action', action);
// Update the last known global action
lastKnownGlobalAction = action;
}
}).catch((err) => {
console.error('Failed to fetch global action:', err);
});

// Watch the profiles and notify the renderer process when they change
// Watch the rules and notify the renderer process when they change
this.rules().then((rules) => {
const stringifiedRules = JSON.stringify(rules)
if (lastKnownAppRules === null || stringifiedRules !== lastKnownAppRules) {
console.log('notifying app rules change');
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('kbs-app-rules', rules);
});
notify('kbs-app-rules', rules);
// Update the last known app rules
lastKnownAppRules = stringifiedRules;
}
Expand All @@ -387,9 +464,7 @@ const kbs = {
const stringifiedProfiles = JSON.stringify(profiles);
if (lastKnownProfiles === null || stringifiedProfiles !== lastKnownProfiles) {
console.log('notifying profiles change');
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('kbs-profiles', profiles);
});
notify('kbs-profiles', profiles);
// Update the last known profiles
lastKnownProfiles = stringifiedProfiles;
}
Expand All @@ -399,6 +474,50 @@ const kbs = {
}
}, 1000);
},

finalizeProfileEdit: async function(resJsonBase64) {
const buildData = JSON.parse(Buffer.from(resJsonBase64, 'base64').toString());

// buildData.profileYaml = the object representing the profile.yaml
// buildData.sources = array of source file paths

// create temporary directory
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'kbs-editor-'));
// write the profile.yaml file to it
console.log(`writing profile.yaml to ${tmpdir}`);
fs.writeFileSync(path.join(tmpdir, 'profile.yaml'), yaml.dump(buildData.profileYaml));
// copy each of the source files to the temporary directory
buildData.sources.forEach(source => {
console.log(`copying ${source} to ${tmpdir}`);
fs.copyFileSync(source, path.join(tmpdir, path.basename(source)));
});
// build the profile
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kbs-editor-output-'));
console.log(`using output dir ${outputDir}`);
try {
console.log(`running bp -d "${tmpdir}" -o "${path.join(outputDir, `${buildData.profileYaml.profile.name}.zip`)}"`);
await this.exec(`bp -d "${tmpdir}" -o "${path.join(outputDir, `${buildData.profileYaml.profile.name}.zip`)}"`, true);
} catch (err) {
console.error('Failed to build profile:', err);
return;
}
// import the profile into keyboard sounds
try {
console.log(`running ap -z "${path.join(outputDir, `${buildData.profileYaml.profile.name}.zip`)}"`);
await this.exec(`ap -z "${path.join(outputDir, `${buildData.profileYaml.profile.name}.zip`)}"`, true);
} catch (err) {
console.error('Failed to import profile:', err);
return;
}

// clean up the temporary directory
console.log('cleaning up temporary directories');
fs.rmSync(tmpdir, { recursive: true, force: true });
fs.rmSync(outputDir, { recursive: true, force: true });

// notify the editor window that the profile has been imported
return true;
},
}

export {
Expand Down
Loading

0 comments on commit 12c20c9

Please sign in to comment.