├── config.json
├── assets
└── app-icons
│ ├── Brave.png
│ ├── Code.png
│ ├── Figma.png
│ ├── Chrome.png
│ ├── Notable.png
│ └── TickTick.png
├── README.md
├── utils.js
├── LICENSE
├── index.html
├── styles.css
└── App.js
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "modes": []
3 | }
--------------------------------------------------------------------------------
/assets/app-icons/Brave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/Brave.png
--------------------------------------------------------------------------------
/assets/app-icons/Code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/Code.png
--------------------------------------------------------------------------------
/assets/app-icons/Figma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/Figma.png
--------------------------------------------------------------------------------
/assets/app-icons/Chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/Chrome.png
--------------------------------------------------------------------------------
/assets/app-icons/Notable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/Notable.png
--------------------------------------------------------------------------------
/assets/app-icons/TickTick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basharovV/Modos-BTT/HEAD/assets/app-icons/TickTick.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Modos-BTT
2 |
3 | ## NOTE: This preset was started and kind of abandoned...for now.
4 |
5 | *Modos* is a BetterTouchTool preset for managing workspaces + windows.
6 |
7 | 
8 | 
9 |
10 | 🔧Developing...
11 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | export const loadJSON = (filePath) => {
2 | return new Promise((resolve, reject) => {
3 | try {
4 | var xobj = new XMLHttpRequest();
5 | xobj.overrideMimeType("application/json");
6 | xobj.open('GET', filePath, true);
7 | xobj.onreadystatechange = function () {
8 | if (xobj.readyState == 4) {
9 | resolve(xobj.responseText);
10 | }
11 | };
12 | xobj.send(null);
13 | } catch(e) {
14 | reject(e);
15 | }
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Vyacheslav Basharov
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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | WINDOW
3 | */
4 |
5 | html {
6 | width: 100%;
7 | height: 100%;
8 | font-family: -apple-system, Arial, Helvetica, sans-serif;
9 | }
10 |
11 | body {
12 | margin: 0;
13 | width: 100%;
14 | height: 100%;
15 | -moz-user-select: none;
16 | overflow: hidden;
17 | }
18 |
19 | /*
20 | TOOLBAR
21 | */
22 | #modos-toolbar {
23 | height: 28px;
24 | display: flex;
25 | flex-direction: row;
26 | padding: 5px 10px;
27 | border-radius: 5px;
28 | border-top: solid 1px rgba(102, 102, 102, 0.526);
29 | background-color: rgba(25, 25, 25, 0.8);
30 | color: white;
31 | transition: all 0.4s ease-in;
32 | }
33 |
34 | #modos-toolbar:hover {
35 | cursor: pointer;
36 | background-color: rgba(0, 0, 0, 0.841);
37 | }
38 |
39 | .modos-toolbar-selected {
40 | box-shadow: rgb(0, 113, 170) 2px 2px;
41 | margin: 23px 17px 0 17px;
42 | opacity: 1;
43 | }
44 |
45 | .modos-toolbar-default {
46 | box-shadow: rgb(27, 27, 27) 4px 4px;
47 | margin: 26px 15px 0 15px;
48 | opacity: 1;
49 | }
50 |
51 |
52 | /*
53 |
54 | LEFT SECTION - APP & WINDOW INFO
55 |
56 | */
57 |
58 | #app-section {
59 | display: flex;
60 | flex-grow: 1;
61 | flex-basis: 0;
62 | flex-direction: row;
63 | justify-content: flex-start;
64 | align-items: center;
65 | }
66 |
67 | #current-app-icon {
68 | width: 35px;
69 | height: auto;
70 | padding: 5px;
71 | box-sizing: border-box;
72 | }
73 |
74 | #current-window-name {
75 | overflow: hidden;
76 | text-overflow: ellipsis;
77 | display: -webkit-box;
78 | -webkit-line-clamp: 1; /* number of lines to show */
79 | -webkit-box-orient: vertical;
80 | margin-left: 10px;
81 | color: gray;
82 | font-weight: normal;
83 | max-width: 350px;
84 | }
85 |
86 | #current-app-window-count-container {
87 | margin-left: 5px;
88 | border-radius: 40px;
89 | padding: 3px 7px;
90 | background-color: rgba(128, 128, 128, 0.3);
91 | }
92 |
93 | #current-app-window-count {
94 | font-size: 8pt;
95 | color: rgb(182, 182, 182);
96 | }
97 |
98 | /*
99 | MIDDLE SECTION - MODES
100 | */
101 |
102 | #current-mode {
103 | opacity: 1;
104 | }
105 |
106 | #mode-list {
107 | list-style: none;
108 | display: inline-flex;
109 | padding: 0;
110 | margin: 0;
111 | pointer-events: all;
112 | }
113 |
114 | .mode-list-item {
115 | margin: 0;
116 | padding: 0;
117 | opacity: 0.3;
118 | }
119 |
120 | .mode-list-item:not(:first-child) {
121 | margin-left: 10px;
122 | padding: 0;
123 | }
124 |
125 | .mode-list-item:hover {
126 | opacity: 0.7;
127 | }
128 |
129 | #mode-list {
130 | color: white;
131 | }
132 |
133 | #mode-section {
134 | background-color: rgba(0, 0, 0, 0.149);
135 | border-radius: 15px;
136 | padding: 0 10px;
137 | box-sizing: border-box;
138 | display: flex;
139 | flex-grow: auto;
140 | flex-basis: 1;
141 | flex-direction: row;
142 | justify-content: center;
143 | align-items: center;
144 | transition: width 0.5s ease-in-out;
145 | }
146 |
147 | /** ON OFF **/
148 |
149 | #on-off {
150 | margin-left: 10px;
151 | }
152 |
153 | .on {
154 | opacity: 1;
155 | color: rgb(0, 162, 255);
156 | }
157 |
158 | .off {
159 | opacity: 0.3;
160 | }
161 |
162 | /*
163 | KEYBOARD CONTROLS (when toolbar is clicked)
164 | */
165 | .keyboard-controls {
166 | font-size: 9pt;
167 | }
168 |
169 | #keyboard-controls-left {
170 | margin-right: 10px;
171 | transition: opacity 0.5s ease-in;
172 | }
173 |
174 | #keyboard-controls-right {
175 | margin-left: 10px;
176 | transition: opacity 0.5s ease-in;
177 | }
178 |
179 | .show {
180 | opacity: 1;
181 | }
182 |
183 | .hide {
184 | opacity: 0;
185 | display: none !important;
186 | }
187 |
188 | .fade-in {
189 | opacity: 1;
190 | }
191 |
192 | .fade-out {
193 | opacity: 0;
194 | }
195 |
196 |
197 | /*
198 | RIGHT SECTION - ACTIONS
199 | */
200 |
201 | #action-section {
202 | display: flex;
203 | flex-grow: 1;
204 | flex-basis: 0;
205 | flex-direction: row;
206 | justify-content: flex-end;
207 | align-items: center;
208 | }
209 |
210 | .text-button {
211 | font-size: 10pt;
212 | color: gray;
213 | }
214 |
215 | #restore-button {
216 | margin-left: 10px;
217 | }
218 |
219 | #close-button {
220 | margin-left: 10px;
221 | color: gray;
222 | }
223 |
224 | #close-button:hover {
225 | color: white;
226 | }
227 |
228 | .text-button:hover {
229 | color: white;
230 | }
231 |
232 | h1, h2, h3, h4, h5, p {
233 | margin: 0;
234 | }
235 |
236 | #loading-spinner {
237 | color: white;
238 | font-size: 18pt;
239 | }
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import * as utils from './utils.js';
2 |
3 |
4 | /*
5 | Config settings for Modos.
6 | */
7 | var config;
8 | var configIsLoaded = false;
9 |
10 | const loadConfig = async () => {
11 | const configString = await utils.loadJSON('config.json');
12 | console.log(`Config: ${config}`);
13 | config = JSON.parse(configString);
14 | };
15 |
16 | loadConfig();
17 |
18 | /**
19 | * Returns the local path of the app icon.
20 | * @param {string} appName The app name
21 | */
22 | const getAppIcon = appName => {
23 | console.log(`Loading icon for ${appName}`)
24 | // if (!configIsLoaded) await loadConfig();
25 | // const iconPath =
26 | return `./assets/app-icons/${appName}.png`;
27 | }
28 |
29 | /**
30 | * The modes that should match BTT's configuration,
31 | * each one for a separate macOS space/desktop.
32 | *
33 | * CHANGE THIS TO MATCH YOUR SETUP.
34 | */
35 | const MODES = [
36 | "General",
37 | "Developer",
38 | "Designer",
39 | "Music"
40 | ];
41 |
42 | /**
43 | * How often (in milliseconds) to poll data about running app, windows, modes.
44 | */
45 | const REFRESH_RATE = 500;
46 |
47 | var currentAppName;
48 | var currentWindowTitle;
49 | var currentNumberOfWindows = 0;
50 | var currentMode = '';
51 | var enabled = false;
52 | var focused = false;
53 |
54 | /*
55 |
56 | POLLING INFO FROM APPLESCRIPTS
57 |
58 | */
59 |
60 | async function refreshCurrentAppInfo() {
61 |
62 | const spinner = document.getElementById("loading-spinner");
63 | if (spinner.classList.contains('show')) {
64 | spinner.classList.remove('show');
65 | spinner.classList.add('hide');
66 | }
67 |
68 | let appleScript = `
69 | set windowName to "..."
70 | set currentAppName to ""
71 | set currentAppTitle to "Loading..."
72 | set numberOfWindows to "0"
73 | try
74 | tell application "System Events"
75 | set currentApp to first application process whose frontmost is true
76 | set currentAppName to name of currentApp
77 | tell process currentAppName
78 | tell (1st window whose value of attribute "AXMain" is true)
79 | set windowName to value of attribute "AXTitle"
80 | end tell
81 | set currentAppTitle to title of currentApp
82 | set numberOfWindows to count of windows
83 | end tell
84 | end tell
85 | end try
86 |
87 | return {currentAppTitle, currentAppName, windowName, numberOfWindows}
88 | `;
89 |
90 | // @ts-ignore
91 | let result = await runAppleScript({ script: appleScript });
92 | let [appTitle, appName, windowTitle, numberOfWindows] = eval(result.replace('{', '[').replace('}', ']'));
93 |
94 | // What changed ?
95 | const appChanged = currentAppName !== appName;
96 | const windowChanged = currentWindowTitle !== windowTitle;
97 | const numberOfWindowsChanged = currentNumberOfWindows !== numberOfWindows;
98 |
99 | // Set new values
100 | currentAppName = appName;
101 | currentWindowTitle = windowTitle;
102 | currentNumberOfWindows = numberOfWindows;
103 |
104 | if (appChanged) {
105 | document.getElementById("current-app-name").textContent = `${appTitle}`;
106 | document.getElementById("current-app-window-count-container").classList.add('show');
107 | document.getElementById("current-app-window-count-container").classList.remove('hide');
108 | document.getElementById("current-app-window-count").textContent = numberOfWindows;
109 | document.getElementById("current-window-name").textContent = appTitle === windowTitle ? "" : windowTitle;
110 | document.getElementById("current-app-icon").setAttribute('src', getAppIcon(appTitle));
111 | } else if (windowChanged) {
112 | document.getElementById("current-app-window-count-container").classList.add('show');
113 | document.getElementById("current-app-window-count-container").classList.remove('hide');
114 | document.getElementById("current-app-window-count").textContent = numberOfWindows;
115 | document.getElementById("current-window-name").textContent = appTitle === windowTitle ? "" : windowTitle;
116 | } else if (numberOfWindowsChanged) {
117 | document.getElementById("current-app-window-count").textContent = numberOfWindows;
118 | }
119 | }
120 |
121 | /*
122 | AUTOMATIC UPDATES
123 | */
124 |
125 | /**
126 | * Refreshes the enabled/disabled status.
127 | */
128 | async function refreshEnabledStatus() {
129 | // @ts-ignore
130 | try {
131 | let result = await callBTT('get_string_variable', { variable_name: 'customVariable3' });
132 | let newEnabled = result === 'enabled';
133 | const changed = enabled !== newEnabled;
134 |
135 | if (changed) {
136 | enabled = newEnabled;
137 | // Update UI
138 | const onOff = document.getElementById('on-off');
139 | if (enabled) {
140 | onOff.classList.add('on');
141 | onOff.classList.remove('off');
142 | } else {
143 | onOff.classList.add('off');
144 | onOff.classList.remove('on');
145 | }
146 | }
147 | } catch(e) {
148 |
149 | }
150 |
151 | }
152 | /**
153 | * Refreshes the focused/default status.
154 | */
155 | async function refreshFocusedStatus() {
156 | // @ts-ignore
157 | try {
158 | let result = await callBTT('get_string_variable', { variable_name: 'customVariable2' });
159 | let newFocused = result === 'focus';
160 | const changed = focused !== newFocused;
161 |
162 | if (changed) {
163 | focused = newFocused;
164 | // Update UI
165 | const toolbar = document.getElementById('modos-toolbar');
166 | if (focused) {
167 | toolbar.classList.add('hide');
168 | toolbar.classList.remove('show');
169 | } else {
170 | toolbar.classList.add('show');
171 | toolbar.classList.remove('hide');
172 | }
173 | }
174 | } catch(e) {
175 |
176 | }
177 |
178 | }
179 |
180 | /**
181 | * Refreshes the current window preset mode.
182 | */
183 | async function refreshCurrentMode() {
184 | // @ts-ignore
185 | let newMode = await callBTT('get_string_variable', { variable_name: 'customVariable1' });
186 | const changed = currentMode !== newMode;
187 | if (changed) {
188 | currentMode = newMode;
189 | refreshModeList();
190 | }
191 | }
192 |
193 |
194 | function setModeList() {
195 | // Set the modes
196 | var modeList = document.getElementById("mode-list");
197 | MODES.forEach(mode => {
198 | let child = document.createElement('li');
199 | child.className = 'mode-list-item';
200 | let content = document.createElement('h3');
201 | content.innerText = mode;
202 | if (mode === currentMode) {
203 | child.id = 'current-mode';
204 | }
205 | child.appendChild(content);
206 |
207 | // Assign mode selector
208 | child.onclick = () => selectMode(mode);
209 |
210 | modeList.appendChild(child);
211 | });
212 | }
213 |
214 | function refreshModeList() {
215 | // Set the modes
216 | var modeList = document.getElementById("mode-list");
217 | MODES.forEach((mode, index) => {
218 | let child = modeList.children[index];
219 | if (mode === currentMode) {
220 | child.id = 'current-mode';
221 | } else {
222 | child.id = null;
223 | }
224 | });
225 | }
226 |
227 | /*
228 |
229 | BTT LIFECYCLE HOOKS
230 |
231 | */
232 |
233 | /* This is called after the webview content has loaded*/
234 | function BTTInitialize() {
235 |
236 | }
237 |
238 | /* This is called before the webview exits and destroys its content*/
239 | function BTTWillCloseWindow() {
240 |
241 | }
242 |
243 | /* This is called before the webview hides*/
244 | function BTTWillHideWindow() {
245 |
246 | }
247 |
248 | /* This is called when the webview becomes visible*/
249 | function BTTWindowWillBecomeVisible() {
250 |
251 | }
252 |
253 | /* This is called when a script variable in BTT changes. */
254 | function BTTNotification(note) {
255 | let data = JSON.parse(note);
256 | console.log(data.note, data.name);
257 | }
258 |
259 | /*
260 | SYSTEM ACTIONS
261 | */
262 |
263 | async function showNotification(title, subtitle, text) {
264 |
265 | let shellScript = `osascript -e 'display notification \"${text}\" with title \"${title}\" subtitle \"${subtitle}\" sound name "Pop"'`;
266 |
267 |
268 | let shellScriptWrapper = {
269 | script: shellScript, // mandatory
270 | launchPath: '/bin/bash', //optional - default is /bin/bash
271 | parameters: '-c', // optional - default is -c
272 | environmentVariables: '' //optional e.g. VAR1=/test/;VAR2=/test2/;
273 | };
274 |
275 | //@ts-ignore
276 | await runShellScript(shellScriptWrapper);
277 |
278 | }
279 |
280 | /*
281 |
282 | USER ACTIONS
283 |
284 | */
285 |
286 | async function selectMode(mode) {
287 | //@ts-ignore
288 | callBTT('set_string_variable', { variable_name: 'customVariable1', to: mode });
289 | }
290 |
291 | export async function savePreset() {
292 | console.log('Save preset');
293 |
294 | let actionDefinition = {
295 | "BTTPredefinedActionType": 105,
296 | "BTTPredefinedActionName": "Show BTT Preferences",
297 | };
298 |
299 | //@ts-ignore
300 | let result = await callBTT('trigger_action', { json: JSON.stringify(actionDefinition) });
301 | console.log(result);
302 | if (result === "success") {
303 | await showNotification("Modos", "BetterTouchTool", `${currentMode} window preset saved!`);
304 | }
305 | }
306 |
307 | export async function restorePreset() {
308 | console.log('Restore preset');
309 |
310 | let actionDefinition = {
311 | "BTTTriggerType": -1,
312 | "BTTTriggerClass": "BTTTriggerTypeOtherTriggers",
313 | "BTTPredefinedActionType": 268,
314 | "BTTPredefinedActionName": "Save \/ restore specific window layout",
315 | "BTTWindowLayoutName": currentMode
316 | };
317 |
318 | //@ts-ignore
319 | let result = await callBTT('trigger_named', { trigger_name: `Restore Layout` });
320 | console.log(result);
321 | if (result === "success") {
322 | await showNotification("Modos", "BetterTouchTool", `${currentMode} window preset restored!`);
323 | }
324 | }
325 |
326 | export async function closeWebView() {
327 | await callBTT('trigger_named', { trigger_name: 'test', closeFloatingWebView: 1 });
328 | }
329 |
330 | function showKeyboardControlsHint(show) {
331 | if (show) {
332 | // Show keyboard controls hint
333 | var keyboardControlsLeft = document.getElementById("keyboard-controls-left");
334 | var keyboardControlsRight = document.getElementById("keyboard-controls-right");
335 |
336 | keyboardControlsLeft.classList.add("show");
337 | keyboardControlsLeft.classList.remove("hide");
338 |
339 | keyboardControlsRight.classList.add("show");
340 | keyboardControlsRight.classList.remove("hide");
341 | } else {
342 | // Hide keyboard controls hint
343 |
344 | var keyboardControlsLeft = document.getElementById("keyboard-controls-left");
345 | var keyboardControlsRight = document.getElementById("keyboard-controls-right");
346 |
347 | keyboardControlsLeft.classList.add("hide");
348 | keyboardControlsLeft.classList.remove("show");
349 |
350 | keyboardControlsRight.classList.add("hide");
351 | keyboardControlsRight.classList.remove("show");
352 | }
353 | }
354 |
355 | // Check the current app every 2 seconds
356 | setInterval(async () => {
357 | console.log('Getting current app...')
358 | await refreshCurrentAppInfo();
359 | await refreshEnabledStatus();
360 | await refreshFocusedStatus();
361 | await refreshCurrentMode(); // It would be great if BTT could have reactive hooks so we don't have to poll.
362 | }, REFRESH_RATE);
363 |
364 | // Toolbar on click
365 | var toolbarSelected = false;
366 | var toolbarElement = document.getElementById("modos-toolbar");
367 | toolbarElement.classList.remove('fade-out');
368 | toolbarElement.classList.add('fade-in');
369 | toolbarElement.classList.add('modos-toolbar-default');
370 |
371 | toolbarElement.onclick = (mouseEvent) => {
372 | console.log('CLICK')
373 | toolbarSelected = !toolbarSelected;
374 | if (toolbarSelected) {
375 | // Show highlight
376 | toolbarElement.classList.add("modos-toolbar-selected");
377 | toolbarElement.classList.remove("modos-toolbar-default");
378 | showKeyboardControlsHint(true);
379 |
380 | } else {
381 | toolbarElement.classList.add('modos-toolbar-default');
382 | toolbarElement.classList.remove('modos-toolbar-selected');
383 | showKeyboardControlsHint(false);
384 | }
385 | }
386 |
387 | document.getElementById('save-button').onclick = savePreset;
388 | document.getElementById('restore-button').onclick = restorePreset;
389 | document.getElementById('close-button').onclick = closeWebView;
390 |
391 |
392 | // Key listeners
393 | document.onkeydown = checkKey;
394 |
395 | function checkKey(e) {
396 |
397 | e = e || window.event;
398 | if (toolbarSelected) {
399 | if (e.keyCode == '38') {
400 | // up arrow
401 | }
402 | else if (e.keyCode == '40') {
403 | // down arrow
404 | }
405 | else if (e.keyCode == '37') {
406 | // left arrow
407 | selectMode(MODES[Math.max(0, MODES.indexOf(currentMode) - 1)]);
408 | }
409 | else if (e.keyCode == '39') {
410 | // right arrow
411 | selectMode(MODES[Math.min(MODES.length - 1, MODES.indexOf(currentMode) + 1)]);
412 | }
413 | }
414 | }
415 |
416 | setModeList();
417 | showKeyboardControlsHint(false);
418 |
--------------------------------------------------------------------------------