├── 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 | ![Modos screenshot 1](https://i.imgur.com/4Z5gnlb.png) 8 | ![Modos screenshot 2](https://i.imgur.com/zifAJnW.png) 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 |
13 |
14 | 15 | 16 |

17 |
18 |

19 |
20 |
21 |
22 |
23 |
24 |

Press left key

25 |
26 | 27 | 28 |
29 |

Press right key

30 |
31 |
32 |
33 |
Save
34 |
Restore
35 | 36 | 37 | 38 |
39 |
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 | --------------------------------------------------------------------------------