├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── assets
├── anagrams.png
├── bigrams.png
├── phrases.png
└── words.png
├── components
├── ButtonAppBar.js
├── CC1ChordGenerator.js
├── ChordDiagnostic.css
├── ChordDiagnostic.js
├── ChordStatistics.js
├── ChordViewer.js
├── LinearWithValueLabel.js
└── ToleranceRecommender.js
├── functions
├── anagramWorker.js
├── chordGenerationWorker.js
├── createCsv.js
└── wordAnalysis.js
├── index.css
├── index.js
├── logo.svg
├── pages
├── CCXDebugging.js
├── ChordTools.js
├── HomePage.js
├── Practice.css
├── Practice.js
└── WordTools.js
├── reportWebVitals.js
├── setupTests.js
├── theme.js
└── words
├── english-1000.json
├── english-500.json
└── english-5000.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 CharaChorder Community Projects
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CharaChorder Utilities
2 |
3 | Welcome to the CharaChorder Utilities project! This project contains a set of tools for CharaChorder users.
4 |
5 | The best way to check them out is just by visiting the site here: [CharaChorder Utilities](https://typing-tech.github.io/CharaChorder-utilities/).
6 |
7 | ## Contributing
8 |
9 | Clone the repo and run:
10 |
11 | ### `npm install`
12 |
13 | Then, to start the server and begin developing run:
14 |
15 | ### `npm start`
16 |
17 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
18 |
19 | The page will reload when you make changes.\
20 | You may also see any lint errors in the console.
21 |
22 | I am open to any contributions--if you have an improvement you would like to make you can make a PR, and I will review it.
23 |
24 | ## Reporting issues
25 |
26 | If you encounter any problems or have any suggestions for improvement, please open an issue.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "charachorder-utilities",
3 | "version": "1.0.0",
4 | "private": true,
5 | "homepage": "https://typing-tech.github.io/CharaChorder-utilities",
6 | "dependencies": {
7 | "@emotion/react": "^11.11.1",
8 | "@emotion/styled": "^11.11.0",
9 | "@mui/icons-material": "^5.14.6",
10 | "@mui/material": "^5.14.6",
11 | "@mui/x-data-grid": "^6.12.1",
12 | "@testing-library/jest-dom": "^6.1.2",
13 | "@testing-library/react": "^14.0.0",
14 | "@testing-library/user-event": "^14.4.3",
15 | "gh-pages": "^6.0.0",
16 | "papaparse": "^5.4.1",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-router-dom": "^6.15.0",
20 | "react-scripts": "^5.0.1",
21 | "vis-data": "^7.1.6",
22 | "vis-timeline": "^7.7.2",
23 | "web-vitals": "^3.4.0",
24 | "workerize-loader": "^2.0.2"
25 | },
26 | "scripts": {
27 | "predeploy": "npm run build",
28 | "deploy": "gh-pages -d build",
29 | "start": "react-scripts start",
30 | "build": "react-scripts build"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
136 |
137 | Either upload a .csv file that has words sorted by importance in the first column (such as a CharaChorder Nexus export),
138 | type words separated by commas, or choose from a predetermined set of word lists. Note: using the word lists can take
139 | quite a bit of time, so you may have to wait a few minutes.
140 |
141 |
142 |
146 | {csvWords.length > 0 && (
147 | <>
148 | {csvWords.length} words loaded. How many do you want to generate chords for?
149 | setNumRowsToUse(Number(e.target.value))}
154 | InputProps={{
155 | inputProps: {
156 | min: 0,
157 | step: 50,
158 | max: csvWords.length
159 | }
160 | }}
161 | />
162 |
165 | >
166 | )}
167 |
168 |
169 | {csvWords.length === 0 && (
170 | <>
171 |
172 | - or -
173 |
174 |
184 |
185 |
186 |
187 |
188 | )
189 | }}
190 | />
191 |
203 | >
204 | )}
205 |
206 |
207 |
208 |
209 | Select number of keys for the chord inputs
210 |
218 | Selected range: {sliderValue[0]} - {sliderValue[1]}
219 |
146 |
147 | This tool is designed to help you look at the press and release timings of a chord.
148 | To use, try to chord the word in the input box and then shortly after it will be plotted.
149 | There is also a table of the presses and releases.
150 | Credit to Tangent Chang (andy23512) from the CharaChorder Discord for this tool.
151 |
152 |
351 |
352 | )}
353 | {attemptSummaries.length > 0 && recommendations.recommendedPress > 0 && (
354 |
355 |
356 |
357 | Press mean (scaled): {recommendations.pressMedian.toFixed(2)} ms/key
358 |
359 |
360 | Release mean (scaled): {recommendations.releaseMedian.toFixed(2)} ms/key
361 |
362 |
363 |
364 | )}
365 |
366 | );
367 | };
368 |
369 | export default ToleranceRecommender;
--------------------------------------------------------------------------------
/src/functions/anagramWorker.js:
--------------------------------------------------------------------------------
1 | function countWordOccurrences(textInput, word) {
2 | var words = getCleanWordsFromString(textInput);
3 | let count = 0;
4 | for (let i = 0; i < words.length; i++) {
5 | if (words[i] === word) {
6 | count++;
7 | }
8 | }
9 | return count;
10 | }
11 |
12 | function getCleanWordsFromString(inputString) {
13 | // Replace all new line and tab characters with a space character, and remove consecutive spaces
14 | const textWithSpaces = inputString.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' ');
15 |
16 | // Split the text into an array of words
17 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, ''));
18 | const words = origWords
19 | // Remove empty strings from the array
20 | .filter(word => word.trim().length > 0)
21 | // Convert all words to lower case
22 | .map(word => word.toLowerCase());
23 | return words;
24 | }
25 |
26 | function findPartialAnagrams(inputString) {
27 | const words = getCleanWordsFromString(inputString);
28 | const result = [];
29 | const uniqueWords = [...new Set(words)];
30 |
31 | // Calculate the total number of iterations (for progress calculation)
32 | const totalIterations = uniqueWords.length * (uniqueWords.length - 1);
33 | console.time("Analyzing Time");
34 | console.log("Expected total iterations:", totalIterations);
35 | let currentIteration = 0;
36 |
37 | for (let i = 0; i < uniqueWords.length; i++) {
38 | // For each word, compare it to the other words in the array
39 | for (let j = 0; j < uniqueWords.length; j++) {
40 | // Skip the current word if it is being compared to itself
41 | if (i !== j) {
42 | // Increment current iteration and calculate progress
43 | currentIteration++;
44 |
45 | const progress = (currentIteration / totalIterations) * 100;
46 | if (currentIteration % 100000 === 0) {
47 | //console.log(progress)
48 | postMessage({ type: 'progress', progress });
49 | }
50 |
51 | // Check if the words are equal
52 | if (uniqueWords[i] !== uniqueWords[j]) {
53 | // If they are not equal, check if they are partial anagrams
54 | const uniqueLetters1 = [...new Set(uniqueWords[i].split(''))];
55 | const uniqueLetters2 = [...new Set(uniqueWords[j].split(''))];
56 |
57 | // Check if each unique letter in one word appears in the other word
58 | if (uniqueLetters1.every(letter => uniqueLetters2.includes(letter)) && uniqueLetters2.every(letter => uniqueLetters1.includes(letter))) {
59 | // Check if the pair has already been added to the result array
60 | if (!result.some(pair => pair.includes(uniqueWords[i]) && pair.includes(uniqueWords[j]))) {
61 | if (result.length === 0) {
62 | result.push([uniqueWords[i], uniqueWords[j]]);
63 | } else {
64 | let found = false;
65 | for (let k = 0; k < result.length; k++) {
66 | if (result[k].includes(uniqueWords[i]) || result[k].includes(uniqueWords[j])) {
67 | if (!(result[k].includes(uniqueWords[i]))) {
68 | result[k].push(uniqueWords[i]);
69 | }
70 | if (!(result[k].includes(uniqueWords[j]))) {
71 | result[k].push(uniqueWords[j]);
72 | }
73 | found = true;
74 | break;
75 | }
76 | }
77 | if (!found) {
78 | result.push([uniqueWords[i], uniqueWords[j]]);
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | console.timeEnd("Analyzing Time");
88 | console.time("Sorting Time"); // Start the timer with label "Sorting Time"
89 | const resultWithCounts = [];
90 |
91 | for (const group of result) {
92 | const groupWithCounts = group.map(word => {
93 | return { word: word, count: countWordOccurrences(inputString, word) };
94 | });
95 | resultWithCounts.push(groupWithCounts);
96 | }
97 |
98 | // Sort the result array by the largest list of partial anagrams first
99 | resultWithCounts.sort((a, b) => b.length - a.length);
100 | console.timeEnd("Sorting Time"); // End the timer with label "Sorting Time"
101 |
102 | return resultWithCounts;
103 | }
104 |
105 | export function anagramWorkerFunction(e) {
106 | if (e.data.type === 'computeAnagrams') {
107 | postMessage({ type: 'progress', progress: 0 }); // Initial progress message
108 | const result = findPartialAnagrams(e.data.input);
109 | postMessage({ type: 'result', result }); // Final result
110 | }
111 | }
112 |
113 | // eslint-disable-next-line no-restricted-globals
114 | self.onmessage = anagramWorkerFunction;
--------------------------------------------------------------------------------
/src/functions/chordGenerationWorker.js:
--------------------------------------------------------------------------------
1 | function generateChords(words, sliderValue, checkboxStates, chordLibrary) {
2 | const MIN_WORD_LENGTH = 2;
3 | const ALT_KEYS = ['LEFT_ALT', 'RIGHT_ALT'];
4 | const KEY_MIRROR_MAP_L = {
5 | ",": ";",
6 | "u": "s",
7 | "'": "y",
8 | ".": "j",
9 | "o": "n",
10 | "i": "l",
11 | "e": "t",
12 | "SPACE": "SPACE",
13 | "BKSP": "ENTER",
14 | "r": "a",
15 | "v": "p",
16 | "m": "h",
17 | "c": "d",
18 | "k": "f",
19 | "z": "q",
20 | "w": "b",
21 | "g": null,
22 | "x": null
23 | };
24 | const KEY_MIRROR_MAP_R = Object.fromEntries(Object.entries(KEY_MIRROR_MAP_L).map(([key, value]) => [value, key]));
25 | const KEY_FINGER_MAP = {
26 | "LH_PINKY": ['LEFT_ALT'],
27 | "LH_RING_1": [',', 'u', "'"],
28 | "LH_MID_1": ['.', 'o', 'i'],
29 | "LH_INDEX": ['e', 'r', 'SPACE', 'BKSP'],
30 | "LH_THUMB_1": ['m', 'v', 'k', 'c'],
31 | "LH_THUMB_2": ['g', 'z', 'w'],
32 | "RH_THUMB_2": ['x', 'b', 'q', 'DUP'],
33 | "RH_THUMB_1": ['p', 'f', 'd', 'h'],
34 | "RH_INDEX": ['a', 't', 'SPACE', 'ENTER'],
35 | "RH_MID_1": ['l', 'n', 'j'],
36 | "RH_RING_1": ['y', 's', ';'],
37 | "RH_PINKY": ['RIGHT_ALT']
38 | };
39 | const CONFLICTING_FINGER_GROUPS_DOUBLE = {
40 | "LH_PINKY": ['LEFT_ALT', 'LH_PINKY_3D'],
41 | "LH_RING_1": [',', 'u', "'", 'LH_RING_1_3D'],
42 | "LH_MID_1": ['.', 'o', 'i', 'LH_MID_1_3D'],
43 | "LH_INDEX": ['e', 'r', 'LH_INDEX_3D', 'SPACE', 'BKSP'],
44 | "LH_THUMB": ['m', 'v', 'k', 'c', 'LH_THUMB_1_3D', 'g', 'z', 'w', 'LH_THUMB_1_3D'],
45 | "RH_THUMB": ['x', 'b', 'q', 'DUP', 'RH_THUMB_1_3D', 'p', 'f', 'd', 'h', 'RH_THUMB_2_3D'],
46 | "RH_INDEX": ['a', 't', 'RH_INDEX_3D', 'SPACE', 'ENTER'],
47 | "RH_MID_1": ['l', 'n', 'j', 'RH_MID_1_3D'],
48 | "RH_RING_1": ['y', 's', ';', 'RH_RING_1_3D'],
49 | "RH_PINKY": ['RIGHT_ALT', 'RH_PINKY_3D']
50 | };
51 | const CONFLICTING_FINGER_GROUPS_TRIPLE = {
52 | "group_1": ['a', 'n', 'y'],
53 | "group_2": ['r', 'o', "'"]
54 | }
55 |
56 | const UNUSABLE_CHORDS = {
57 | "impulse_chord": ['DUP', 'i']
58 | };
59 |
60 | const settings = {
61 | useDupKey: checkboxStates.useDupKey,
62 | useMirroredKeys: checkboxStates.useMirroredKeys,
63 | useAltKeys: checkboxStates.useAltKeys,
64 | use3dKeys: checkboxStates.use3dKeys,
65 | sliderValue: sliderValue,
66 | chordLibrary: chordLibrary
67 | };
68 |
69 | const onlyChars = true;
70 | const useDupKey = settings.useDupKey;
71 | const useMirroredKeys = settings.useMirroredKeys;
72 | const useAltKeys = settings.useAltKeys;
73 | const use3dKeys = settings.use3dKeys;
74 | const minChordLength = settings.sliderValue[0];
75 | const maxChordLength = settings.sliderValue[1];
76 | let usedChords = {};
77 | let uploadedChords = new Map();
78 | let localWords = [...new Set(words.map(word => word.toLowerCase()))];
79 |
80 | const CHORD_GENERATORS_MAP = {
81 | 'onlyCharsGenerator': onlyCharsGenerator,
82 | 'useMirroredKeysGenerator': useMirroredKeysGenerator,
83 | 'useAltKeysGenerator': useAltKeysGenerator,
84 | 'use3dKeysGenerator': use3dKeysGenerator
85 | };
86 |
87 | const SETTINGS_MAP = {
88 | 'onlyChars': onlyChars,
89 | 'useMirroredKeys': useMirroredKeys,
90 | 'useAltKeys': useAltKeys,
91 | 'use3dKeys': use3dKeys
92 | };
93 |
94 | function wordsList() {
95 | return localWords.filter(word => word.length >= MIN_WORD_LENGTH);
96 | }
97 |
98 | function loadUploadedChords() {
99 | return new Promise((resolve, reject) => {
100 | try {
101 | if (!chordLibrary || !Array.isArray(chordLibrary)) {
102 | console.warn('chordLibrary is not properly initialized.');
103 | resolve();
104 | return;
105 | }
106 |
107 | chordLibrary.forEach((entry) => {
108 | if (entry.chordInput && entry.chordOutput) {
109 | const chord = entry.chordInput.trim().split(' + ');
110 | const word = entry.chordOutput.trim();
111 |
112 | if (uploadedChords.has(word)) {
113 | uploadedChords.get(word).push(chord);
114 | } else {
115 | uploadedChords.set(word, [chord]);
116 | }
117 | }
118 | });
119 | resolve();
120 | } catch (error) {
121 | reject(error);
122 | }
123 | });
124 | }
125 |
126 | function calculateChord(chars) {
127 | for (const generatorKey of Object.keys(CHORD_GENERATORS_MAP)) {
128 | const option = generatorKey.replace('Generator', '');
129 | if (SETTINGS_MAP[option]) {
130 | const chord = CHORD_GENERATORS_MAP[generatorKey](chars); // function call here
131 | if (chord) {
132 | const reversedChord = chord.slice().sort().reverse(); // Ensuring slice() so as not to mutate the original array
133 | return reversedChord;
134 | }
135 | }
136 | }
137 | return null;
138 | }
139 |
140 | function getChars(word) {
141 | const chars = word.split("").filter(str => str !== " ");
142 | let uniq_chars = [...new Set(chars)];
143 | if (uniq_chars.length < chars.length && useDupKey) {
144 | uniq_chars = [...new Set(chars), "DUP"]
145 | }
146 | const validChars = Object.values(KEY_FINGER_MAP).flat();
147 |
148 | return uniq_chars.filter(char => validChars.includes(char));
149 | }
150 |
151 | function assignChord(word, chord) {
152 | usedChords[word] = chord;
153 | }
154 |
155 | function onlyCharsGenerator(chars) {
156 | for (const chord of allCombinations(chars)) {
157 | if (validChord(chord)) return chord;
158 | }
159 | }
160 |
161 | function useMirroredKeysGenerator(chars) {
162 | for (const chord of allCombinations([...new Set([...chars, ...mirrorKeys(chars)])])) {
163 | if (validChord(chord)) return chord;
164 | }
165 | }
166 |
167 | function use3dKeysGenerator(chars) {
168 | for (const chord of allCombinations([...new Set([...chars, ...threeDKeys(chars)])])) {
169 | if (validChord(chord)) return chord;
170 | }
171 | }
172 |
173 | function useAltKeysGenerator(chars) {
174 | for (const chord of allCombinations([...new Set([...chars, ...ALT_KEYS])])) {
175 | if (validChord(chord)) return chord;
176 | }
177 | }
178 |
179 | function validChord(chord) {
180 | return !fingerConflict(chord) && !usedChord(chord) && !uploadedChord(chord);
181 | }
182 |
183 | const powerSetCache = {};
184 |
185 | function powerSet(chars) {
186 | const cacheKey = chars.join(',');
187 | if (powerSetCache[cacheKey]) return powerSetCache[cacheKey];
188 |
189 | const result = [[]];
190 | for (const value of chars) {
191 | const length = result.length;
192 | for (let i = 0; i < length; i++) {
193 | const subset = result[i];
194 | result.push(subset.concat(value));
195 | }
196 | }
197 | powerSetCache[cacheKey] = result;
198 | return result;
199 | }
200 |
201 | function allCombinations(chars) {
202 | return powerSet(chars)
203 | .filter(subset => subset.length >= minChordLength && subset.length <= maxChordLength)
204 | .sort((a, b) => a.length - b.length);
205 | }
206 |
207 | function fingerConflict(chord) {
208 | const sortedChord = [...chord].sort();
209 | if (hasDuplicates(chord)) return true;
210 | if (Object.values(CONFLICTING_FINGER_GROUPS_DOUBLE).some((fingerKeys) => fingerKeys.filter((key) => chord.includes(key)).length > 1)) return true;
211 | if (Object.values(CONFLICTING_FINGER_GROUPS_TRIPLE).some((fingerKeys) => fingerKeys.filter((key) => chord.includes(key)).length > 2)) return true;
212 | if (Object.values(UNUSABLE_CHORDS).some((fingerKeys) => JSON.stringify([...fingerKeys].sort()) === JSON.stringify(sortedChord))) return true;
213 |
214 | return false;
215 | }
216 |
217 | function hasDuplicates(chord) {
218 | return chord.length > new Set(chord).size;
219 | }
220 |
221 | function usedChord(chord) {
222 | const sortedChord = [...chord].sort();
223 | return Object.values(usedChords).some(usedChord => {
224 | const sortedUsedChord = [...usedChord].sort();
225 | return JSON.stringify(sortedUsedChord) === JSON.stringify(sortedChord);
226 | });
227 | }
228 |
229 | function uploadedChord(chord) {
230 | const sortedChord = [...chord].sort();
231 |
232 | for (const [, chords] of uploadedChords.entries()) {
233 | for (const uploadedChord of chords) {
234 | const sortedUploadedChord = [...uploadedChord].sort();
235 | if (JSON.stringify(sortedUploadedChord) === JSON.stringify(sortedChord)) {
236 | return true;
237 | }
238 | }
239 | }
240 | return false;
241 | }
242 |
243 | function mirrorKeys(chord) {
244 | return chord.map(char => KEY_MIRROR_MAP_L[char] || KEY_MIRROR_MAP_R[char]).filter(Boolean);
245 | }
246 |
247 | function threeDKeys(chord) {
248 | return chord.map(char => getThreeDKey(char)).filter(Boolean);
249 | }
250 |
251 | function getThreeDKey(char) {
252 | for (const [finger, chars] of Object.entries(KEY_FINGER_MAP)) {
253 | if (chars.includes(char)) return `${finger}_3D`;
254 | }
255 | return null;
256 | }
257 |
258 | function generate() {
259 | loadUploadedChords();
260 | const totalWords = wordsList().length;
261 |
262 | let currentIteration = 0
263 | let skippedWords = 0
264 | const failedWords = [];
265 |
266 | for (let index = 0; index < totalWords; index++) {
267 | const word = wordsList()[index];
268 | // Skip if the word is already in chordLibrary
269 | if (uploadedChords.has(word)) {
270 | //console.log(`Skipping '${word}' because it already in chordLibrary.`);
271 | skippedWords++;
272 | continue;
273 | }
274 |
275 | const chord = calculateChord(getChars(word));
276 | if (chord) {
277 | assignChord(word, chord.sort().reverse());
278 | } else {
279 | //console.log("Could not generate chord for", word);
280 | failedWords.push(word);
281 | }
282 |
283 | currentIteration++;
284 |
285 | const progress = (currentIteration / totalWords) * 100;
286 | postMessage({ type: 'progress', progress });
287 | }
288 | return {
289 | usedChords: usedChords,
290 | skippedWordCount: skippedWords,
291 | failedWords: failedWords
292 | };
293 | }
294 |
295 | let results = generate();
296 | return results;
297 | }
298 |
299 | // eslint-disable-next-line no-restricted-globals
300 | self.onmessage = function (e) {
301 | if (e.data.type === 'generateChords') {
302 | postMessage({ type: 'progress', progress: 0 }); // Initial progress message
303 | const result = generateChords(e.data.wordsArray, e.data.sliderValue, e.data.checkboxStates, e.data.chordLibrary);
304 | postMessage({ type: 'result', result }); // Final result
305 | }
306 | }
307 |
--------------------------------------------------------------------------------
/src/functions/createCsv.js:
--------------------------------------------------------------------------------
1 | import Papa from 'papaparse';
2 |
3 | export default function createCsv(data, filename = 'data.csv', includeHeader = true) {
4 | const config = {
5 | header: includeHeader
6 | };
7 | const csv = Papa.unparse(data, config);
8 |
9 | // Create a Blob from the CSV string
10 | const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
11 |
12 | // Create a hidden anchor element
13 | const link = document.createElement('a');
14 | const url = URL.createObjectURL(blob);
15 |
16 | link.setAttribute('href', url);
17 | link.setAttribute('download', filename);
18 | link.style.visibility = 'hidden';
19 |
20 | // Append, trigger the download, and remove the anchor element
21 | document.body.appendChild(link);
22 | link.click();
23 | document.body.removeChild(link);
24 | };
--------------------------------------------------------------------------------
/src/functions/wordAnalysis.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function calculateBigrams(text) {
4 | var words = text.split(' ');
5 | var wordCount = words.length;
6 |
7 | // Use the filter method and the indexOf method to remove any duplicates
8 | var uniqueWords = words.filter(function (word, index) {
9 | return words.indexOf(word) === index;
10 | });
11 |
12 | // Create a list of bigrams by iterating over each word in the array
13 | var bigrams = [];
14 | for (var i = 0; i < words.length; i++) {
15 | // Remove any leading or trailing spaces from the word
16 | var word = words[i].trim();
17 |
18 | // Skip over any empty words (which can happen if the text has multiple consecutive spaces)
19 | if (word.length >= 2) {
20 | // Check for line breaks in the word
21 | if (word.indexOf('\n') === -1) {
22 | for (var j = 0; j < word.length - 1; j++) {
23 | var lowered = word.toLowerCase()
24 | var bigram = lowered.substring(j, j + 2);
25 | const [letter1, letter2] = bigram.split('');
26 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) {
27 | // If either letter is not a lowercase letter, skip this iteration
28 | continue;
29 | }
30 | // sort the characters in the bigram so that permutations are counted together
31 | bigram = bigram.split('').sort().join('');
32 | var reversedbigram = bigram.split('').reverse().join('');
33 | bigram = bigram + " | " + reversedbigram;
34 | bigrams.push(bigram);
35 | }
36 | }
37 | }
38 | }
39 |
40 | // create an object to hold the frequency of each bigram
41 | var bigramCounts = {};
42 | for (let i = 0; i < bigrams.length; i++) {
43 | let bigram = bigrams[i];
44 | if (bigram in bigramCounts) {
45 | bigramCounts[bigram]++;
46 | } else {
47 | bigramCounts[bigram] = 1;
48 | }
49 | }
50 |
51 | return [bigramCounts, wordCount, uniqueWords.length];
52 | }
53 |
54 | export function createFrequencyTable(bigramCounts) {
55 | // Create a matrix of zeroes with 26 rows and 26 columns
56 | // (assuming you only have lowercase letters in your input)
57 | const matrix = Array(26)
58 | .fill()
59 | .map(() => Array(26).fill(0));
60 |
61 | const normalizedMatrix = Array(26)
62 | .fill()
63 | .map(() => Array(26).fill(0));
64 |
65 | // Loop through each key-value pair in the input object
66 | for (const [key, value] of Object.entries(bigramCounts)) {
67 | // Split the key into two letters
68 | const [letter1, letter2] = key.split(" ")[0].split("");
69 |
70 | // Check if either letter1 or letter2 is not a lowercase letter
71 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) {
72 | // If either letter is not a lowercase letter, skip this iteration
73 | continue;
74 | }
75 |
76 | // Convert the letters to their ASCII codes (a = 97, b = 98, etc.)
77 | const ascii1 = letter1.charCodeAt(0) - 97;
78 | const ascii2 = letter2.charCodeAt(0) - 97;
79 |
80 | matrix[ascii1][ascii2] = value;
81 | matrix[ascii2][ascii1] = value;
82 |
83 | // Normalize the value to be in the range 0-255
84 | const normalizedValue = (value / Math.max(...Object.values(bigramCounts))) * 255
85 |
86 | // Update the matrix with the value from the input object
87 | normalizedMatrix[ascii1][ascii2] = normalizedValue;
88 | normalizedMatrix[ascii2][ascii1] = normalizedValue; // (assuming you want the matrix to be symmetrical)
89 | }
90 | return [matrix, normalizedMatrix]
91 | }
92 |
93 | async function calcStats(text, chords, minReps, lemmatizeChecked) {
94 | const counts = await findUniqueWords(chords, text, lemmatizeChecked);
95 | return [counts.sortedWords, counts.uniqueWordCount, counts.chordWordCount];
96 | }
97 |
98 | export function findMissingChords(textToAnalyze, chordLibrary) {
99 | const chords = new Set();
100 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput));
101 | return calcStats(textToAnalyze, chords, 5, false);
102 | }
103 |
104 | async function processWords(words, lemmatize) {
105 | for (let i = 0; i < words.length; i++) {
106 | words[i] = words[i].toLowerCase().replace(/[^\w\s]/g, '');
107 | if (lemmatize) {
108 | // const doc = nlp(words[i])
109 | // doc.verbs().toInfinitive()
110 | // doc.nouns().toSingular()
111 | // const lemma = doc.out('text')
112 | // words[i] = lemma;
113 | }
114 | }
115 | return words;
116 | }
117 |
118 | async function findUniqueWords(chords, text, lemmatize) {
119 | // Split the text into an array of words
120 | var words = text.split(/\s+/);
121 | words = await processWords(words, lemmatize);
122 |
123 | var wordCounts = {};
124 | var uniqueWordCount = 0;
125 | var chordWordCount = 0;
126 | var countedChords = {};
127 |
128 | // Count the number of times each word appears in the text
129 | for (var i = 0; i < words.length; i++) {
130 | var word = words[i].trim();
131 | if (word === "") {
132 | continue;
133 | }
134 | if (word.length > 1) {
135 | if (!(word in wordCounts)) {
136 | wordCounts[word] = 1;
137 | uniqueWordCount++;
138 | } else {
139 | wordCounts[word]++;
140 | }
141 | }
142 | }
143 |
144 | // Create a dictionary of words that do not appear in the chords set
145 | var sortedWords = {};
146 | for (let i = 0; i < words.length; i++) {
147 | let word = words[i].trim();
148 | if (word === "") {
149 | continue;
150 | }
151 | if (word.length > 1) {
152 | if (!chords.has(word)) {
153 | if (!(word in sortedWords)) {
154 | sortedWords[word] = 1;
155 | } else {
156 | sortedWords[word]++;
157 | }
158 | } else {
159 | if (!(word in countedChords)) {
160 | countedChords[word] = true;
161 | chordWordCount++;
162 | }
163 | }
164 | }
165 | }
166 |
167 | var descSortedWords = Object.entries(sortedWords).sort((a, b) => b[1]*b[0].length - a[1]*a[0].length);
168 | return {
169 | sortedWords: descSortedWords,
170 | uniqueWordCount: uniqueWordCount,
171 | chordWordCount: chordWordCount
172 | };
173 | }
174 |
175 | export function getPhraseFrequency(text, phraseLength, minRepetitions, chordLibrary) {
176 | const chords = new Set();
177 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput));
178 |
179 | // Replace all new line and tab characters with a space character, and remove consecutive spaces
180 | const textWithSpaces = text.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' ');
181 |
182 | // Split the text into an array of words
183 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, ''));
184 | const words = origWords
185 | // Remove empty strings from the array
186 | .filter(word => word.trim().length > 0)
187 | // Convert all words to lower case
188 | .map(word => word.toLowerCase());
189 |
190 |
191 | // Create a dictionary to store the phrases and their frequency
192 | const phraseFrequency = {};
193 |
194 | // Iterate over the words array and add each phrase to the dictionary
195 | for (let i = 0; i < words.length; i++) {
196 | for (let j = 2; j <= phraseLength; j++) {
197 | // Get the current phrase by joining the next `j` words with a space character
198 | const phrase = words.slice(i, i + j).join(' ');
199 | // Check if the phrase fits within the bounds of the `words` array
200 | if (i + j <= words.length) {
201 | // Split the phrase into a list of words
202 | const phraseWords = phrase.split(' ');
203 | // Check if the phrase contains at least two words
204 | if (phraseWords.length >= 2) {
205 | // If the phrase is already in the dictionary, increment its frequency. Otherwise, add it to the dictionary with a frequency of 1.
206 | if (phrase in phraseFrequency) {
207 | phraseFrequency[phrase]++;
208 | } else {
209 | phraseFrequency[phrase] = 1;
210 | }
211 | }
212 | }
213 | }
214 | }
215 |
216 | // Filter the sorted phrase frequency dictionary by the minimum number of repetitions
217 | const filteredPhraseFrequency = {};
218 | Object.keys(phraseFrequency).forEach(phrase => {
219 | if (phraseFrequency[phrase] >= minRepetitions) {
220 | filteredPhraseFrequency[phrase] = phraseFrequency[phrase];
221 | }
222 | });
223 |
224 | // Remove entries from the filtered phrase frequency object that are already in the chords set
225 | const lowerCaseChords = new Set(Array.from(chords).map(phrase => phrase.trim().toLowerCase()));
226 | const filteredPhraseFrequencyWithoutChords = Object.keys(filteredPhraseFrequency)
227 | .filter(phrase => !lowerCaseChords.has(phrase))
228 | .reduce((obj, phrase) => {
229 | obj[phrase] = filteredPhraseFrequency[phrase];
230 | return obj;
231 | }, {});
232 |
233 | let sortableArray = Object.entries(filteredPhraseFrequencyWithoutChords);
234 | sortableArray.sort((a, b) => {
235 | const scoreA = a[0].length * a[1];
236 | const scoreB = b[0].length * b[1];
237 | return scoreB - scoreA;
238 | });
239 |
240 | return sortableArray;
241 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom/client';
3 | import CssBaseline from '@mui/material/CssBaseline';
4 | import { ThemeProvider } from '@mui/material/styles';
5 | import App from './App';
6 | import theme from './theme';
7 |
8 | const rootElement = document.getElementById('root');
9 | const root = ReactDOM.createRoot(rootElement);
10 |
11 | root.render(
12 |
13 |
14 |
15 | ,
16 | );
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/CCXDebugging.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Typography, Button, TextField } from '@mui/material';
3 | import List from '@mui/material/List';
4 | import ListItem from '@mui/material/ListItem';
5 | import { Snackbar, Alert } from '@mui/material';
6 |
7 | function CCX() {
8 | const [port, setPort] = useState(null);
9 | const [writer, setWriter] = useState(null);
10 | const [isTesting, setIsTesting] = useState(false);
11 | const [receivedData, setReceivedData] = useState("");
12 | const [openSnackbar, setOpenSnackbar] = useState(false);
13 | const [supportsSerial, setSupportsSerial] = useState(null);
14 |
15 | const handleOpenSnackbar = () => {
16 | setOpenSnackbar(true);
17 | };
18 |
19 | const handleCloseSnackbar = () => {
20 | setOpenSnackbar(false);
21 | };
22 |
23 | useEffect(() => {
24 | if (port !== null) {
25 | listenToPort();
26 | }
27 | }, [port]);
28 |
29 | useEffect(() => {
30 | if ('serial' in navigator) {
31 | setSupportsSerial(true)
32 | }
33 | else {
34 | setSupportsSerial(false)
35 | }
36 | })
37 |
38 | const connectDevice = async () => {
39 | try {
40 | const newPort = await navigator.serial.requestPort();
41 | await newPort.open({ baudRate: 115200 });
42 |
43 | const textEncoder = new TextEncoderStream();
44 | const writableStreamClosed = textEncoder.readable.pipeTo(newPort.writable);
45 | const newWriter = textEncoder.writable.getWriter();
46 |
47 | setWriter(newWriter);
48 | setPort(newPort);
49 | } catch {
50 | alert("Serial Connection Failed");
51 | }
52 | };
53 |
54 | const listenToPort = async () => {
55 | if (port === null) return;
56 |
57 | const textDecoder = new TextDecoderStream();
58 | const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
59 | const reader = textDecoder.readable.getReader();
60 |
61 | while (true) {
62 | const { value, done } = await reader.read();
63 | console.log(value);
64 |
65 | if (done) {
66 | reader.releaseLock();
67 | break;
68 | }
69 | setReceivedData((prev) => prev + value);
70 | }
71 | };
72 |
73 | const toggleTest = async () => {
74 | if (!isTesting) {
75 | if (writer) {
76 | await writer.write("VERSION\r\n");
77 | await writer.write("VAR B2 C1 1\r\n");
78 | }
79 | setIsTesting(true);
80 | } else {
81 | if (writer) {
82 | await writer.write("VAR B2 C1 0\r\n");
83 | }
84 | setIsTesting(false);
85 | copyToClipboard()
86 | }
87 | };
88 |
89 | const copyToClipboard = () => {
90 | if (navigator.clipboard) {
91 | navigator.clipboard.writeText(receivedData);
92 | } else {
93 | // Fallback for older browsers
94 | const textArea = document.createElement("textarea");
95 | textArea.value = receivedData;
96 | document.body.appendChild(textArea);
97 | textArea.focus();
98 | textArea.select();
99 | document.execCommand('copy');
100 | document.body.removeChild(textArea);
101 | }
102 | handleOpenSnackbar();
103 | };
104 |
105 | return (
106 |
107 | CharaChorder X Debugging
108 | {supportsSerial === null ? (
109 | "Checking for Serial API support..."
110 | ) : supportsSerial ? (
111 | <>
112 |
113 | In order to help the CharaChorder team debug why your
114 | keyboard may not be working with the CCX, please perform the following
115 | steps and then copy and send the output once you are done.
116 | Read all instructions first before starting with step 1.
117 |
118 |
119 |
120 | 0: Make sure your CCX is updated to CCOS 1.1.3 or a later version. Click here for instructions on how to update your device.
121 |
122 |
123 | 1: Unplug your keyboard from the CCX and make sure
124 | the CCX is plugged into your computer.
125 |
126 |
127 | 2: Click "Connect" and use Chrome's serial connection to
128 | choose your CCX. Click "Start Test" (you should see a
129 | "VERSION" and "VAR") line appear in the Serial Data box.
130 |
131 |
132 | 3: Plug in your keyboard to the CCX and wait a
133 | couple of seconds before moving to the next step.
134 |
135 | 4: Press and release the letter "a".
136 |
137 | 5: Next, press and hold the letter "a", press and hold the letter "s", and keep
138 | adding one letter at a time until you are pressing and holding all keys in "asdfjkl;" and
139 | then release all at once. Make sure to press the keys in order.
140 |
141 | 6: Press and release the "Left Shift" key.
142 |
143 | 7: Press the "Left Shift" key, press "Left Alt" key,
144 | and then release both.
145 |
146 |
147 | 8: Press the "a" key, press "Left Shift" key, and
148 | then release both.
149 |
150 |
151 | 9: Click Stop Test. The results are copied to your clipboard. You can
152 | send this to your CharaChorder support rep.
153 |
154 |
155 |
156 |
159 | Serial Data:
160 |
168 |
174 |
175 | Copied to clipboard!
176 |
177 |
178 | What is this test doing?
179 |
180 | When keyboard manufacturers make a keyboard that is plug and play,
181 | they have a few formatting options when it comes time to actually send what
182 | you, the user, type on your keyboard to your computer. There
183 | is a standard format that most keyboards use and the
184 | CCX works out of the box with that one (boot protocol); however,
185 | there are others that may be non-standard and when this
186 | happens, your CCX may not work properly.
187 | This test turns on a debugging command and then records
188 | the outputs from your device as you press various keys.
189 | The codes you see in the box are the codes that your
190 | keyboard is sending to the CharaChorder X and then the
191 | CharaChorder X has to interpet what it is, do its own
192 | processing/magic of chording and then send on to the
193 | computer the right text. If your keyboard isn't sending,
194 | for example, "KYBRPT 00 0000040000000000" for the
195 | letter "a", this test will let CharaChorder support see what it IS sending
196 | and then they may be able to infer/make a game plan to support
197 | what your keyboard is sending.
198 |
199 | >
200 | ) : (
201 |
202 | Your browser does not support the Serial API. Please use
203 | Google Chrome, Microsoft Edge, or another browser
204 | that supports the Serial API in order to use this tool.
205 |
206 | )}
207 |
208 | );
209 | }
210 |
211 | export default CCX;
--------------------------------------------------------------------------------
/src/pages/ChordTools.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useLocation, useNavigate } from 'react-router-dom';
4 | import Tabs from '@mui/material/Tabs';
5 | import Tab from '@mui/material/Tab';
6 | import Typography from '@mui/material/Typography';
7 | import Box from '@mui/material/Box';
8 | import ChordDiagnostic from '../components/ChordDiagnostic';
9 | import ChordStatistics from '../components/ChordStatistics';
10 | import CC1ChordGenerator from '../components/CC1ChordGenerator';
11 | import ChordViewer from '../components/ChordViewer';
12 | import ToleranceRecommender from '../components/ToleranceRecommender';
13 |
14 | function CustomTabPanel(props) {
15 | const { children, value, index, ...other } = props;
16 |
17 | return (
18 |
7 | Welcome to CharaChorder Utilites!
8 |
9 | This site contains a collection of tools for CharaChorder users.
10 |
11 |
12 | Most of the chord and practice tools require you to upload your exported chord library from Dot I/O. You can do that in the top right hand corner. After you have done it once, you will only need to load it again if you have added chords.
13 |
14 |
15 | Explore each of the pages and report any issues on the GitHub Page.
16 |
17 |
18 | Disclaimer: This site is not affiliated, associated, authorized, endorsed by, or in any way officially connected with CharaChorder. The official CharaChorder website can be found at CharaChorder.com.
19 |
20 |